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:
- A bug in
push
can break alert logic, which is weird and uncommon. - The code is hard to understand - why are
push
andpop
sorting anything? - In asynchronous operations, the
items
returned bypush
or andpop
may be outdated. - Sorting is triggered every time something is modified.
- It breaks the Single Responsibility Principle.
- The
pop
andpush
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
- Clearer code.
- Easier to test.
- Easier to maintain.
- Simplifies debugging.
- Provides a consistent API design for developers.
- Performance boost due to lazy evaluation - code is executed only when truly needed.
Cons
- 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.