Command-Query Separation
- 20 October 2016
- Ruby Design
- Command-Query Separation
Today's post will be more theoretical rather than practical. But it's worth reading because Command-Query Separation allows you to improve design of methods. So today we will discuss CQS.
Let's see how Wikipedia describes it:
It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, Asking a question should not change the answer. More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.
Very simple and reasonable idea, but people usually forget about it during development. So each method should be either a Command which make some action or Query which returns a value.
Following this principle you will never break another very useful rule - Tell Don't Ask. Because you will either send commands (Tell) or queries (Ask).
For example we can go through simple CRUD (Create, Read, Update, Delete) in Rails application.
We can see three commands here: Create, Update and Delete. They change state of the system. Read - is a typical query, it shouldn't affect our system. It should have no side effects.
The main benefit from using this approach is a guarantee that calling Query you can be sure that the state of system will not change. It has no side effects. After a calling Command, state of a system will change in an expected way.
Another benefit that such code is much easier to test.
Following this principle you will be able to name methods more properly. For Queries it will be nouns. For example: users
, weekly_report
, email
. In case of Command - it should be verbs: download_report
, update_user
, set_direction
.
Names of these methods show us their purpose.
As usually I want to show you CQS on example. It shows how important to pick proper names for methods and separate them by Command and Query.
We have User
class with two methods:
class User
def validate_email(email)
@email = email if /^long_regexp_to_check_email$/.match(email)
end
def email
@email || 'No email'
end
end
It's a bad code. Let's try to understand why. First of all name of method validate_email
doesn't correspond to its behavior. Besides validation this code has unexpected side-effect: it sets @email
variable. Second bad thing that if email is not valid - it will not set @email
variable and it will have nil
value. That's why email
method has to check it every time and return default value if it's nil
.
Also, we can't reuse method validate_email
if we really just want to validate email. Because it sets @email
variable every time when method is being called.
Let's rewrite that code using CQS:
class User
attr_reader :email
def set_email(email)
@email = valid_email?(email) ? email : 'No email'
end
private
def valid_email?(email)
!!(/^long_regexp_to_check_email$/.match(email))
end
end
This code is much better. Method names are self-explanatory: set_email
, valid_email?
. We see that set_email
it's a Command that will set @emails
variable. valid_email?
it's a Query which returns true
or false
.
Also we can easily re-use valid_email?
inside class without any unexpected side-effects.
Since @email
variable will be set only after calling set_email
, we can easily pull it out into attr_reader
.
Even on such simple example we see profit of using Command-Query Separation (CQS). In real project this approach will allow you to improve your code and make it more readable. Also one of the biggest benefits of CQS is side-effect free methods - Queries.
Happy coding!