Skip to content

Instantly share code, notes, and snippets.

@fnbk
Last active March 15, 2021 19:26
Show Gist options
  • Save fnbk/1397db4db8816cced51b06f8b1fefa03 to your computer and use it in GitHub Desktop.
Save fnbk/1397db4db8816cced51b06f8b1fefa03 to your computer and use it in GitHub Desktop.
#
# CQS - Command Query Separation
#
# Definition
* Methods should either perform an action (command) or return data (query), but not both.
* Asking a question should not change the answer - Bertrand Meyer
We should aim to write our code in such a way that methods either do someting or give something back, not both:
* Command: perform an action (change the state of the system; write operations; with side effects)
* Query: query the state of the system (no state changes; read operations; no side effects)
# Benefits
* This is because we can use queries in many situations with much more confidence, introducing them anywhere, and changing their order. With commands we have to be more careful.
* This has a simplifying effect on a program, making its states more comprehensible.
# Key takeaways
* follow this principle when you can
* be prepared to break it for more convenient methods or in race conditions
#
# Version 1 - Bad
#
* UpdateUsers() does two things: changes state and returns values
* developers may be tempted to add small changes later, adding additional complexity and bloat the function
public void InitUsers()
{
var userData = ...;
var users = UpdateUsers(userData);
}
public List<User> UpdateUsers(List<UserData> userData) {
// get users
var userIds = userData.Keys.ToList();
var users = _dbContext.Users.Where(u => userIds.Contains(u.Id)).ToList();
// change users
foreach (var user in users)
{
user.Rating = userData[user.Id].Rating;
user.CardDebt = userData[user.Id].CardDebt;
}
_dbContext.UpdateRange(users);
_dbContext.SaveChanges(users);
return users;
}
#
# Version 2 - Better
#
# UpdateUsers() has been split into two functions:
* UpdateUsers() - changes state but does not return values.
* GetUsers() - returns values but does not change state.
# each function:
* has less reason to change (SRP)
* is shorter and thus results in shorter tests
* encourages you to keep your side-effecting code separate
* you end up with more pure functions which are easy to understand/test
public void InitUsers()
{
var userData = ...;
var userIds = userData.Keys.ToList();
UpdateUsers(userData); // command
var users = GetUsers(userIds); // query
}
public void UpdateUsers(UserData[] userData) {
_dbContext
.Users
.Where(x => userIds.Contains(x.Id))
.UpdateFromQuery(u => new User{Id: u.Id, Rating: userData[u.Id].Rating, CardDebt: userData[u.Id].CardDebt})
}
public List<User> GetUsers(List<string> userIds) {
return _dbContext
.Users
.Where(u => userIds.Contains(u.Id))
.ToList();
}
#
# Exception to the rule
#
# Stack
* push() - command
* peek() - query
* pop() - command and query
# Conclusion
* pop() is a convenience function and follows know conventions of the Stack structure.
* It could be separated into two functions (command and query), but that would certainly create more code and more explaining to others.
* Let's try to find the right balance. Prefer Command-Query separation where possible.
// FILO: first in last out
var stack = new Stack(99);
stack.push(13); stack.push(21); stack.push(34); stack.push(55);
// Option A) strict
int item = stack.peek(); // query
stack.removeTop() // command
// Option B) exception - "command + query" on one function
int item = stack.pop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment