Software design patterns

2026-08-02 04:25 by Ian

I am an engineering generalist. But the bulk of my professional life has been the authorship of software. So if I had to pick a pigeon hole to live in, it would be "software/firmware".

Being that nearly everything has software at some level, I've had the blessed opportunity to work with many smart engineers in various disciplines. Of all the engineering disciplines I've touched, there is a quality that software has that (AFAICT) is only shared with the chemists. And I don't have a word for it.

Why we have software design patterns

Even my 10-year-old can tell that I am an amateur mechanical engineer. A child with no knowledge of how to use a drill press can see four drill holes and tell at a glance if they are not square.
Rockets explode when the aerospace guys make a mistake.
Unclassed submersibles made of carbon fiber are crushed at depth.
When a double-E (electrical engineer) makes a mistake, the magic smoke tells the whole story to a lay bystander.

But software? Our mistakes last forever because of the two things we have in common with the chemists:
1. The consequences of our praxis are invisible. The chemists because of how small molecules are, and us because of how fast state evolves and destroys its own precursors.
2. True understanding of the results of our work requires the eyes of another who is versed in our arcane methods. I know how to use a permanganate to test for iron. But I'm weird. And some equally weird chemist out there knows how to read the condition code register for an ARM CPU. Most engineers don't cross boundaries to that extent.

Each group of engineers copes with this as best as they can within their respective disciplines. Like the chemists, those of us in software have "recipes" that we can follow (and modify) when we want a given outcome under given constraints. But unlike the chemists, who have thermodynamics to anchor them to reality, programmers are often under the impression that there are many ways to skin a cat, and that all of them are equally good by some (usually unacknowledged) standard. Possibly more so than any other engineering discipline, software practitioners need to articulate their values. We call a subset of those recipes "design patterns".

Constraint

For an engineer (any engineer), constraint is the fundamental building material. The thing all forms of art have in common. The thing that dictates all downstream choices. Often the first thing discussed before the pen ever touches the bar napkin. The real world is brutally finite, and we must take resources from the world in order to change it.

Constraint is a blessing. It is the reason I prefer to write firmware. While too much constraint might make a given problem intractable, too little dilutes the impact you can have on the world you took resources from in the first place. For software, too little constraint usually ends up costing a team time.

From a abstract software perspective, programmers face two basic constraints in a real-world computer made of atoms.

Those two fundamental constraints (time and memory) are the basic trade-off within software. Each concern is fractal. Memory takes time to access, and CPU clocks are impacted by memory (specifically, ALU width). But those concerns are more nuanced than this post should attempt to cover. See also: Faster is not always Better.

Some of the recipes we use are associated with carving up time and/or memory (threads, co-routines, and any number of data structures). But since design patterns is the real topic at hand, I will spare us the review of undergrad CSE, and assume that anyone reading further knows a bit about those things already. I will also decline to rehash language choices in C++, as well as stylistic nuances. Everything that follows uses C++ as a vehicle, but the principles and patterns described are germane across all of software engineering.

Coding is one thing... Engineering is another.

From the perspective of a software designer (and most especially someone designing software in a team context), a project's constraints have the added dimensional depth of humanity's limitations applied on top of them:

This set of constraints is often summed up with the word "complexity". There are plenty of charlatans in software who make statements that conflate "simple" and "easy", but the mature opinions would have us think of complexity as a spendable resource. And in service of those ends, I very much prefer object-oriented polymorphism with what people today call SOLID.

Case studies for simple polymorphic interfaces

Shoveling buffers around is a very common thing to do in a program. BufferAccepter is a fairly pure exercise in polymorphic SOLID principles, and I have re-used it productively in a number of different products and teams. Its contract is laid out in the README, and adherence to the contract is unit tested with this pair of classes serving as a general-purpose test jig for any classes that might extend the interface. A test program is given here, with a base-64 codec as an example of a class-under-test.

The interface is pure virtual with no storage members, and so is nearly free of runtime costs and is short enough to fully paste into this post:

/*
* An interface class for accepting a buffer.
*
* A class would implement BufferAccepter to expose a means of accepting a
*   formless buffer from a source that doesn't need to know the specifics of
*   what is to be done with the buffer, nor how.
*/
class BufferAccepter {
  public:
    /**
    * Provides a heap-based buffer with fully-realized ownership management.
    *
    * @param is the pointer to the managed container for the content.
    * @return -1 to reject buffer, 0 to accept with partial claim, 1 to accept with full claim.
    */
    virtual int8_t pushBuffer(StringBuilder*) =0;

    /**
    * @return the number of bytes available in the next stage of buffering.
    */
    virtual int32_t bufferAvailable() =0;
};

Designing with this interface is exceptionally easy, since the contract clearly delineates memory handling concerns. It does one (and only one) thing, and forms the basis of numerous other buffer transform classes in C3P. See also, C3PConsole, UARTAdapter, and C3PTypePipe.

Any class conforming to this interface is interchangeable on the basis of its interface alone. This allows long pipelines to be built from simple composable steps that do complicated things to buffers while remaining predictable, easy to understand, and thus also, to maintain. Generality of interface also allows hardware drivers to present a uniform interface to any pipeline so constructed.

Another case of SOLID principles being productively employed in C3P is a GPIO shim for arbitrary platform implementations. A well-defined contract applied here makes possible the generalization of many common pin-handling tasks.

The BusOpCallback interface would be yet another SOLID-inspired class, although this one breaks discipline somewhat by implementing its polymorphism as a template. This was considered acceptable for this purpose, due to the fact that you would never want bus transactions for (say) i2c to be handled by a class for SPI traffic or cryptographic operations. Interfaces for those different kinds of asynchronous I/O operations are not mutually composable (on purpose).

Factories

C3PTypePipe deserves special attention, since it highlights a common consequence of SOLID practices put into real practice: The need to construct leaf-classes at runtime without foreknowledge of their specific identities. This is why factories exist.

There are probably at least five different ways to do this, but I like to have a static method of the base class that can be called to handle the allocation of an instance of the abstract interface without burdening the caller with the specifics of the object's nature.

Singletons

There are many cases in programming (and especially, firmware) where there must be a software construction created once, and only once. For firmware work, this is commonly a driver for a platform, but might also be a purely software resource (like a scheduler).

The factory pattern has the added benefit of lazy init. It can be constructed once on first-use, and never again.

Constructor concealment

Constructor delegation to a base class is a very common polymorphism tactic. BufferAccepter (detailed above) doesn't need an explicit constructor, since it has no storage members to initialize. But what happens when your interface is not pure virtual or has instance data in the base? To the extent that your team views this as acceptable, you can still enforce a policy of "no direct instantiation of the base" by making all constructors in the base class protected (or even private, if they are trivial).

Asides

An aside on memory "volume"

Software engineers speak often of memory as if it were a physical volume in 3-space, or an area. But it is neither of those things. It isn't even simply a line, as its address-ordering would suggest (unless it is very small). The physical reality of memory today is more like a book of "pages" that each contain a single (text-wrapped) line of uniform length. Most of the time, our hardware allows us to forget about the fact that it is paged, and we treat it as if it were a single enormous ordered line of bytes. Like a book printed entirely on a single roll of ticker tape.

Previous:
Next: