I’ve been spending a bit of time thinking about what it is I do when designing interfaces. I definitely follow a few rules that I think are common:

  • immutable by default
  • composition over inheritance
  • dependency injection
  • single responsibility

But these don’t seem to capture my decision-making entirely, as other people don’t replicate my instructions with just these rules.

After spending some time thinking about it, I’ve decided to name some new rules that I follow.

Mental Offloading

Mental Offloading is the process by which you remove the need to consider what code is doing as you write it.

Methods should have clear inputs and outputs, be self-contained, and make no mutations to system state.

This last part of this statement is obviously impossible in the strictest sense, but it’s a goal rather than an outcome. The system state must be modifiable to make any program progress, so instead, consider that the function must not alter the behavior of the system after it has been called.

  • A print function should always print the same way.
  • A state machine must always consume it’s full language.
  • A class instance must be initialized completely before using it.
  • A class must allow for any order of initialization, and any order of method calls after initilization.
  • Don’t handle every case, make the cases you don’t want impossible instead.

The important takeaway is that the programmer should not have to have a mental model of program state on each line of code. Pass off work to subroutines liberally, but have strict minimal interfaces for those subroutines. Rather than writing a function with lots of parameters and tests and variability on output, write a function that does things one way, always the same way, is easy to test and verify; then when you consume it, the callsite should contain your mutations into and out of form.

Stateless classes, Stateful functions

As an extension of the above rule, tracking system state is the hardest part of software development. So make it easy on yourself and everyone else:

Constrain all state mutations to be within a single function scope.

You should consider your program as a series of idempotent “entry points” rather than a continuous state graph. Each entry point should progress the program state completely. Consider the principles of REST, where each HTTP verb has an explicit meaning, and server methods are idempotent.

  • Get - retrieve a document that exists
  • Put - update an existing document and return the result
  • Post - create a new document and return the result
  • Delete - destroy a document

Each of these methods maintain a consistent behavior no matter how many times called, or what order they are called in. Each method progresses the system state in some way, but no method causes the system to modify it’s behavior for any other invocation.

Your goal should be to write functions that either finish what they are doing or do nothing at all.

  • Detect error conditions in your function as early as possible and bail out without mutating the data.
  • Pre-calculate all parameters for state transforms ahead-of-time and only use at the last possible moment.
  • Mutate copies of data if there is any potential for error during execution, and apply the mutation later.

An example

Consider a class that does some stateful work:

class foo_t
{
public:
	void init (measurement_t const &measurement_);

	void updateModel (double timestamp_);
	void addMeasurements (vector<measurement_t> const &measurements_);
	void recalculateScores ();
	void calculateAverage ();

	measurement_t average () const;
};

The mental model of this class is complex, as you have to remember which order to call it’s methods in, what their side effects are, and whether or not you can use each method at a certain time. And while it may still fit inside of “SOLID” principles, dependency injection, composition over inheritnace, and immutable by default, the class is difficult to use.

Consider an updated class:

class foo_t
{
public:
	void init (measurement_t const &measurement_);

	void observe (double timestamp_, vector<measurement_t> const &measurements_)
	{
		updateModel (timestamp_);
		addMeasurements (measurements_);
		recalculateScores ();
		calculateAverage ();
	}

	measurement_t average () const;

private:
	void updateModel (double timestamp_);
	void addMeasurements (vector<measurement_t> const &measurements_);
	void recalculateScores ();
	void calculateAverage ();
};

Now, we have 3 operations:

  • initialize
  • mutate
  • query

As long as your write these operations to execute in any order, the programmer that uses these methods doesn’t need to track how they work, only that they will have an outcome. You have effectively offloaded the mental effort necessary to use your code to the code itself.