Last active
March 15, 2021 19:26
-
-
Save fnbk/1397db4db8816cced51b06f8b1fefa03 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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