How do you decide what methods or functions to write as you’re coding? Here’s a technique that I learned about years ago that guides you towards software with great reusability and easier testability.

As far as practical, I try to write methods and functions that conform to one of three specific method archetypes: Queries, Commands, or Orchestrations.

Aside: I no longer remember where I learned this approach, and a protracted online search has uncovered no leads for me to followup. If you know who came up with this idea, please let me know so that I can give them credit.

Definitions

A Query returns information about current state.

Calling a query method makes no observable changes to the internal state of the system, and will return the same value if called multiple times in a row. If you’re calling multiple query methods on an instance, you’re free to reorder those calls without running any risk of changing the semantics of the code.

(The qualification above about observable state changes is important - sometimes a query method will update some internal state. Potential reasons for this include caching, logging, and tracing,.)

Commands are about making changes to the state of the system. Typically will have no return value - or possibly only an error. Sometimes it’s useful to make these commands idempotent.

An Orchestration is a method that coordinates queries and commands with a specific goal. Often these are short and declarative, with complex logic encapsulated away in other methods.

Motivation

The separation between queries and commands makes things more explicit in consuming code - it’s obvious where state changes are being triggered. This helps the predictability of code, making it easier for consuming developers to make required changes without tripping over hidden dependencies or unanticipated changes.

I’ve also found that the separation tends to make testing easier - once an object has been set up, using the query methods to verify is in the desired state is safe.

Stacks

A good example of these archetypes in actions can be seen by considering the classic stack data structure.

A minimal definition of a stack consists of a data type with just two methods: Push() adds something onto the stack, and Pop() pulls it off again.

Conventionally, we tend to also include Peek() to read the top item without modifying the stack and IsEmpty() to check whether there’s anything on the stack in the first place.

This API may be very familiar, but when you look closely it becomes clear that it’s a little odd.

  • We have two different ways to access the item at the top of the stack, both of which would return the same item.
  • If we’re writing a multi-threaded system, there’s an inherent race condition: If we check for IsEmpty() == false, our Pop() can still blow up if some else pops the item first.

With method archetypes, we can do better.

Peek() is a classic query - returns the top item off the stack without modifying the stack in any way.

If we’re willing to throw or panic when called on an empty stack, we can write the method signature very simply:

// C#
public T Peek() { ... }
// Go
func (s Stack[T]) Peek() T { ... }

(In both languages, I’m assuming a generic stack that contain any item of type T.)

Alternatively, we could return additional information to let our clients know if there was an item to retrieve:

// C#
public (T, bool) Peek() { ... }
// Go
func (s Stack[T]) Peek() (T, bool) { ... }

Our original Pop() method is a problem, because it combines the functionality of our existing Peek() query along with an additional state change.

Let’s break out that state changes as a separeate command Discard().

To blindly discard the top item, we’d declare as:

// C#
public void Discard() { ... }
// Go
func (s Stack[T]) Discard() { ... }

If we desire some idempotency (which can be useful in multi-threaded or multi-processing cases), we can pass the item we’re wanting to discard. We then need to know whether the item was successfully discarded or not:

// C#
public bool Discard(T top) { ... }
// Go
func (s Stack[T]) Discard(top T) bool { ... }

Our stack is now fully operational - but if we still want a Pop() method, we can write one as an orchestration between Peek() and Discard():

// C#
public (T, bool) Pop(T top) 
{
    var result, ok = Peek()
    if (ok) 
    {
        Discard()
    }

    return result, ok
}
// Go
func (s Stack[T]) Pop(top T) (T, bool) {
    if result, ok := s.Peek(); ok {
        s.Discard()
        return result, ok 
   }

   var zero T
   return zero, false
}

One of the emergent properties of our slightly revised stack definition is that it can be trivially made safe for threading use; we don’t have any temporal coupling between methods, allowing atomic operation to be added purely through internal locks.

Conclusion

This is a very simple introduction to method archetypes, and one that doesn’t really do them justice. I’ve found them to be a useful technique - give it a go and let me know how you get on.

Comments

blog comments powered by Disqus
Next Post
An Inconvenient API  18 Feb 2023
Prior Post
A bash puzzle, solved  02 Jul 2022
Related Posts
Browsers and WSL  31 Mar 2024
Factory methods and functions  05 Mar 2023
Using Constructors  27 Feb 2023
An Inconvenient API  18 Feb 2023
A bash puzzle, solved  02 Jul 2022
A bash puzzle  25 Jun 2022
Improve your troubleshooting by aggregating errors  11 Jun 2022
Improve your troubleshooting by wrapping errors  28 May 2022
Keep your promises  14 May 2022
When are you done?  18 Apr 2022
Archives
September 2022
2022