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):

public bool DocumentExists(string path)
{
    if (FileLength(path) == 0)
    {
        DeleteFile(path);
        return false;
    }

    return FileExists(path);
}

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

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

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

Orchestrations are used to coordinate activity between other methods, combining queries and commands together.

Similar to commands, orchestrations typically don’t return any value.

A Stack

Let’s look at how method archetypes can subtly shape the methods we write by looking at that classic data structure, the stack.

interface IStack<T>
{
    bool Empty { get; }
    T Peek { get; }
    void Push(T value);
    T Pop();
}

The Empty and 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.

Unfortunately, the 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:

interface IStack<T>
{
    bool Empty { get; }
    T Peek { get; }
    void Push(T value);
    void Discard();
}

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 …

interface IAsyncStack<T>
{
    bool Empty { get; }
    T Peek { get; }
    Task PushAsync(T value);
    Task DiscardAsync();
}

… or an immutable variation …

interface IImmutableStack<T>
{
    bool Empty { get; }
    T Peek { get; }
    IImmutableStack<T> Push(T value);
    IImmutableStack<T> Discard();
}

In this case, the changes implied by the use of method archetypes are subtle; in other cases they can be far more widely impacting.

Prior post in this series:
What is it with Booleans?

Comments

blog comments powered by Disqus