Follow method archetypes to constrain your method design to avoid surprises and simplify your design. This is another in my series of posts on Code Gardening - the practice of making small improvements as you see them while you are working.
The Single Responsibility Principle (SRP) tells us that each class we create should have a single reason for existing, a single reason for changing. The same is true for the methods that we write; each should have a single clear purpose for existence.
The subtle problem with multipurpose methods is that they often (almost always) violate the Principle of Least Suprise by doing unexpected things when they are reused. Consider this example (unfortunately, recreated from real life code I saw half a lifetime ago):
The surprise factor here stems from the method trying to do two things at once - reporting back whether a document exists while also silently deleting empty files. It’s compounded when someone innocently reuses the method in another context and finds that they are getting
AccessDenied errors because (without their knowledge) this method is trying to delete empty files without having permission to do so.
Using method archetypes (also known as operation archetypes) are one way to avoid inadvertently creating these kinds of surprises. There are three archetypes to consider - Queries, Commands, and Orchestrations.
Queries are methods that return information about the state of the object.
They never change that object, so calling them twice in a row return consistent results (providing the state of the object isn’t changed by an outside force). Crucially, because they don’t change the underlying object, it doesn’t matter what order they are accessed, avoiding all the bugs that stem from any temporal coupling.
Commands are methods that change the state of an object.
Typically commands don’t have a return value as such, though sometimes they will return another object, e.g. to allow for monitoring of an asynchronous process.
Orchestrations are used to coordinate activity between other methods, combining queries and commands together.
Similar to commands, orchestrations typically don’t return any value.
Let’s look at how method archetypes can subtly shape the methods we write by looking at that classic data structure, the stack.
Peek properties are classic query methods, returning information about the current state of the stack.
Push() is clearly a command, adding a new value to the stack.
Pop() method is hybrid, returning both a value (like a query) and altering the object (like a command).
One solution is to replace the
Pop() method with an alternative command that just makes the required change - let’s call this
Discard(), giving this interface:
Note how this simplified API makes it simpler to understand - now there is only one way to read an item from the top of the stack, instead of two.
Lastly, it’s worth observing that adherence to method archetypes can make it easier to create an asynchronous variation …
… or an immutable variation …
In this case, the changes implied by the use of method archetypes are subtle; in other cases they can be far more widely impacting.