0
0
0
0
0
javascript
typescript
principles
patterns
code-quality
practices
clean-code

Remember, design and architectural patterns and principles should never be forced! Use them naturally, only if they solve a problem without creating new issues.

Command Query Separation (CQS) Principle

Command Query Separation (CQS) is a valuable principle, much like DRY or KISS, but more concrete. Although less known, it is worth understanding and adding to your developer skillset. It is completely language-independent.

This principle makes code easier to understand and maintain, with a slight performance boost by default. Additionally, it forms the basis of CQRS (Command Query Responsibility Segregation), an architectural pattern (which we won't cover in this article). Considering all these factors, I can confidently say - "Let's dive into the topic!".

The Power of CQS

Separation in real life is valuable - imagine frontend developers focusing on frontend work and backend developers on backend work. Budget constraints often lead to hiring full-stack developers, whose expertise in frontend or backend might be less deep. This is a generalization, not a strict rule.

The same applies to software engineering. Imagine a class method that does two things - updates and reads (look at pop and push).

// Stack class utility.
class Stack<T> {
  private items: T[] = [];

  private sort(a: T, b: T): number {
    // Complex sorting logic.
    return 1;
  }

  push(item: T): T[] {
    this.items.push(item);
    return this.items.sort(this.sort);
  }

  pop(): T[] {
    if (this.items.length === 0) {
      return [];
    }
    this.items.pop();
    return this.items.sort(this.sort);
  }
}
// App logic.
interface ToDoItem {
  id: number;
  content: string;
}

const toDoApp = () => {
  const stack = new Stack<ToDoItem>();

  stack.push({ id: 0, content: `Play Football` });
  const items = stack.push({ id: 1, content: `Buy Wegetables` });

  if (items.length === 2) {
    alert(`You've added your first tasks`);
  }
};

It is problematic for the following reasons:

  1. A bug in push can break alert logic, which is weird and uncommon.
  2. The code is hard to understand - why are push and pop sorting anything?
  3. In asynchronous operations, the items returned by push or and pop may be outdated.
  4. Sorting is triggered every time something is modified.
  5. It breaks the Single Responsibility Principle.
  6. The pop and push methods have low cohesion; we want high cohesion for reusability.

Cohesion is how well the parts of a module, class, or function work together for a single purpose. High cohesion means it focuses on one task, while low cohesion means it does multiple unrelated tasks, which should be avoided.

How to improve? We can utilize the following separation: update/read.

class Stack<T> {
  private items: T[] = [];

  private sort(a: T, b: T): number {
    // Complex sorting logic.
    return 1;
  }

  push(item: T) {
    this.items.push(item);
  }

  pop() {
    if (this.items.length === 0) {
      return;
    }

    this.items.pop();
  }

  sorted() {
    return this.items.sort(this.sort);
  }

  hasLength(value: number): boolean {
    return this.items.length === value;
  }
}

const toDoApp = () => {
  const stack = new Stack<ToDoItem>();

  stack.push({ id: 0, content: `Play Football` });
  stack.push({ id: 1, content: `Buy Wegetables` });

  if (stack.hasLength(2)) {
    alert(`You've added your first tasks`);
  }
};

Congratulations! You've applied CQS (Command Query Separation). Now, the sort function is not triggered unnecessarily, as it's not required in this scenario. Additionally, we've separated the update and read code into distinct, clearly visible chunks. This ensures that changes in the update logic won't affect the sorting mechanism.

Now it's time for the definition:

CQS is a principle that separates logic into two categories: commands, which modify, and queries, which read. Each should be separated into different entities such as classes, methods/functions, or modules, depending on the language you're using.

Pros and Cons

The mentioned principle will definitely improve the performance of your code and decrease the risk of bugs caused by read-to-update logic and vice versa.

However, it will introduce a bit more boilerplate. It's beneficial if used wisely. You can easily overcomplicate things, just like with the overuse of the DRY principle, which can lead to unnecessary abstractions and runtime overhead. Therefore, it's best to be minimal and only add code that is truly needed.

Be like Gordon Ramsay in "Hell's Kitchen" - add just the required amount of salt to your meals, no more.

Pros

  1. Clearer code.
  2. Easier to test.
  3. Easier to maintain.
  4. Simplifies debugging.
  5. Provides a consistent API design for developers.
  6. Performance boost due to lazy evaluation - code is executed only when truly needed.

Cons

  1. Slightly more complex code - this is manageable as long as it doesn’t become overly complicated.

Summary

It's a really easy-to-understand principle, but does it mean you should always use it? Of course not! A lot of built-in code in the JavaScript language does not follow it at all - think about the APIs you use on a daily basis (like splice, for example).

let array = [1, 2, 3, 4, 5];

// Splice modifies the array and returns the removed elements.
let removedElements = array.splice(2, 2);

console.log(array); // Output: [1, 2, 5] - array is modified.
console.log(removedElements); // Output: [3, 4] - elements that were removed.

This approach is handy and useful, but only when you encounter a real problem. In the provided example, unnecessary operations were performed all the time, which is why it was beneficial to separate the read/write logic into distinct chunks.

Author avatar
About Authorpraca_praca

👋 Hi there! My name is Adrian, and I've been programming for almost 7 years 💻. I love TDD, monorepo, AI, design patterns, architectural patterns, and all aspects related to creating modern and scalable solutions 🧠.