Command-Query Separation

Command-query separation (or CQS for short) is a design principle devised by Bertrand Meyer. CQS states that every method should either be a command that performs an action (changes state), or a query that returns answer to the caller (without changing the state or causing side-effects), but not both.

In other words, functions should return a value only if they are pure (don't make any visible state changes). This convention, if followed consistently, can simplify programs, making them easier to understand and reason about.

Following method violates CQS principle, it does too much:

func SaveUser(name string, age int) *Result {
    if name == "" {
        return NewError("Name is empty")
    }
    if age <= 0 || age >= 100 {
        return NewError("Age is out of range")
    }
    users = append(users, NewUser(name, age))
    return nil
}

We can simplify this code by breaking it into two methods. Command:

func CreateUser(name string, age int) {
    users = append(users, NewUser(name, age))
}

and query:

func ValidateUser(name string, age int) *Result {
    if name == "" {
        return NewError("Name is empty")
    }
    if age <= 0 || age >= 100 {
        return NewError("Age is out of range")
    }
    return nil
}

We can reuse and test these methods separately or compose them. For example, to print validation errors:

func InputChanged(name string, age int) {
    var result = ValidateUser(name, age)
    if !result.Valid {
        RenderErrors(result)
    } else {
        RenderSaveButton()
    }
}

Saving user while safe-guarding against invalid input:

func SaveButtonClicked(name string, age int) {
    var result = ValidateUser(name, age)
    if !result.Valid {
        panic(result)
    }
    SaveUser(name, age)
}

Exceptions

CQS works best when it we treat it as a design principle. If used consistently, it helps people understand code better and faster.

Yet, there always are exceptional edge cases, where it could be wise to step away from such principle:

  • Operations related to concurrency, where it is important to both mutate state and get the result back: Interlocked.Increment or sync.Add.

  • Well-established data structures, where functions with result and side-effect are common: queue.Dequeue or Stack.Pop

It is possible to modify all of these methods to follow CQS principle. Yet, that would introduce additional complexity and deviate from the expected behavior.

It might be better to accept the deviation and note it explicitly (e.g. in documentation, inline comments or method names).

CQS in Languages

CQS is prominent in Functional Programming. Languages supporting it often have this principle baked right into the language design itself.

Languages following Object-Oriented Programming approach don't enforce CQS by default. Yet, following these principles can lead to simpler code that is more predictable and easy to reason about.

Some ecosystems offer additional tooling to make CQS principle more explicit to the developers. For example, Microsoft Code Contracts introduces Pure Attribute to indicate methods that don't have visible side effects.

References

  1. Command-query separation - Wikipedia
  2. Pure Attribute

- by .