Commands and Queries
By the end of this lesson, students should be able to...
- Differentiate between commands and queries in software systems
- Explain how splitting methods into commands and queries can help us write better software
As you write more and more object-oriented programs, and pay more and more attention to object interfaces, you may begin to notice a pattern emerging around how you write methods. Some methods are designed to change their object, while others are meant to return a result without changing it. We can formalize this idea by giving these patterns names:
- Command: change the state of the system
- Query: return a result, without changing anything
Here is an example in code:
people = ['Ada', 'Katherine', 'Grace'] # .length is a query. It returns a result (the length # of the array) without changing the it. puts people.length # .push is a command. It changes the array by adding # a new element. people.push('Maryam') # .pop is a command. It removes and returns the last # element in the array. Even though it has a return value, # changing the array makes it a command. first_person = people.pop puts first_person
Question: Are the following methods queries or commands?
The distinction between commands and queries is another idiom of software engineering. There's nothing in the Ruby language definition to imply a difference between queries and commands, but thinking about our methods in this way can be very helpful to us as engineers. For example:
- If you know a method is a query, calling it is "free" in terms of cognitive load. You don't need to worry about unexpected side-effects.
- If you call several query methods in a row, changing the order won't change the result.
- If you know you're writing a query or a command, you can work that into the name of the method. For example, Ruby methods ending in a
!are always commands.
Most of these are benefits for the code that's calling your method, not for the method itself. The distinction is very useful for reading, understanding, and debugging code. In fact, figuring out whether methods are queries or commands is one of the first things many engineers do when learning a new codebase.
Example: Solar System
Here are some examples from the Solar System project:
|Method||State change||Return value||Command or Query?|
||none||the name of the planet||query|
||none||a one-line summary of the planet||query|
||none||the name of the star||query|
||add a planet to the list||none||command|
From this table, we can tell the following
- Asking for a
summary, or asking a
find_planet_by_name, is "safe". Calling these methods won't change the state of the program.
- Calling the
distance_betweenmethod won't change anything about either of the
- If we're calling
add_planet, we need to be a little more intentional
Those observations may seem a little obvious - of course calling
Planet#name shouldn't change anything. However, having it explicitly spelled out like this makes the process of connecting the pieces just a little easier, and the bigger the project, the more important that is.
Command Line Actions
The distinction between queries and commands applies to other aspects of software engineering too. For example, when a user is interacting with a program's command line interface, it can be useful to divide the available actions into these categories. Again returning to the Solar System project:
|Action||State change||Program output||Command or Query?|
||none||list of planets||query|
||none||details about a planet||query|
||new planet||ask for details||command|
Now a "state change" refers to the state of the entire program, not a single object.
exit is a particularly interesting case here, since it quits the program. Quite a state change! As developers, we can use this table to reason about how our program might behave.
You could create a table like this for any interface that includes multiple actions. Other examples include:
- Interactions with the filesystem from the terminal
- Git commands
Exercise: Commands and Queries in the Filesystem
Complete the following table of terminal actions that interact with the filesystem.
|Action||State change||Output||Command or Query?|
Commands, Queries and Testing
Categorizing methods as commands or queries can help us write tests for those methods.
For a query, the important part is the method's return value. The focus of testing should be that the return is correct, even given strange parameters or an odd initial state. For example, the query
SolarSystem#find_planet_by_name might have the following tests:
- Can it find and return a planet that exists?
- Does it return
nilif asked for a planet that doesn't exist?
For a command, the important part is the method's effect on the state of the object. The focus of testing should be that the object's state has changed in the correct way. For example, the command
SolarSystem#add_planet might have the following tests:
- Is the planet added to the end of the list?
- Assuming that adding a duplicate planet produces an exception, if you attempt to add a duplicate planet, in addition to getting an error, what happens to the list?
Exercise: Commands and Queries in this week's project
With a partner, revisit the requirements for the project, and do the following:
- Make a table similar to the one we made for SolarSystem above for the different actions the user can take
- Make a similar table for each public instance method on each of your classes
- Is your test coverage appropriate for these commands and queries?
- Make a similar table for the endpoints you're using from your current project (if they exist)
- The distinction between commands and queries is an important part of designing clean interfaces
- Commands make a change to the system
- Queries return a result without changing anything
- Martin Fowler on Command/Query Separation
- Microsoft Azure documentation on Command and Query Responsibility Segregation (CQRS) pattern - might make more sense after the first couple weeks of Rails