This morning my daughters fought over a kitchen stool. A box of fruit snacks from the nearby shelf hit the floor and spilled. “I didn’t do it, she did!” “It wasn’t me, it was you!”
“It fell because of both of you together,” I told them. “Causality is rarely singular.“
In our biological world, events don’t have just one cause. Like my daughters, we wish it was that simple. We want to say “A → B” but in a complex system, each event has many causes. It’s more like
and each of these have many causes, including each other.
We want causality to be straightforward, but it is not.
Fortunately, we’re programmers. The computer is that rare, delightful world where causality works. In the computer, we can say “It did that because I told it to.” Rare is the occurrence that can’t be traced to some pieces of code or wiring.
In software we have the opportunity to keep causality simple. But we’re humans, and as much as we wish we worked that way, we don’t. Our neurons are interconnected, and our software grows that way too.
Programming principles and paradigms aim to stop this encroachment of biological-style networks of dependencies. We want to break these interconnected causalities. Make our programs less complicated.
architecture delineates boundaries, much like the original intent of SOA and OOP. We use functional principles like “no side effects” to restrict the influence of parts on other parts. Even the prime directive, the Single Responsibility Principle, helps keep causality direct.
Yet these principles only help when we follow the spirit of simplicity — or when we’re forced to. A human in a hurry will grab the duct tape and glom all kinds of pieces together. A human not in a hurry, who is aware of commonalities in the internals of two components, will take advantage of these synergies and feel clever for it. Meanwhile they add to a dark web of dependencies that haunts the team later.
The more human-proof we can make these barriers, the better they work. OOP was invented for this — objects were supposed to communicate only by messages, hiding their internals. But it is easy to pass the whole world and yourself in a message to another object. OOP at its best enables a less-coupled design, but the language does not enforce it.
Then there’s SOA. Process barriers between applications can enforce a stronger separation — but only if the message format is a simple one. RPC or Java serialization doesn’t help! A giant ESB shuffling messages among Java apps doesn’t simplify; it adds itself as a dependency to each one without breaking their dependencies on each other.
Consistent implementations of service providers and consumers can destroy the benefit of enforced isolation. Instead, let the teams developing each part choose languages and tools, let them communicate in JSON or straightforward XML, and the components will be less coupled. This is message-oriented, and it’s hard to screw it up with duct tape and cleverness.
Components that pass simple messages are independently testable. Problems can be isolated to the system at fault by checking the messages.
Within a single process, prevent coupling by restricting side effects and environmental access. When a function is “data in, data out” then its interface is clearly stated and testable. Haskell, with strong typing and isolated I/O, enforces “data in, data out.” In hybrid languages like F# or Scala, it is up to us to follow these rules and keep our application simple.
Functional style — or more precisely, “expression-oriented
” style — helps at the very small level. OO works well for library or module interfaces when few objects are exposed for use. Messaging helps between application components. At each level, keep your functions, objects, and components as small and simple as possible, and keep them composable. Complex software should be assembled, not woven.
Our natural human tendency is to increase complexity, but software is one world where we don’t have to. In the computer, we are God. But while we have the opportunity to know everything about our creation, we’re still humans with limited working memory. To hold more in our heads, we have to chunk it. We have to compose small bits into larger parts. Principles of expression- and message-oriented programming enforce the lines between those parts, letting us scale up and scale down as needed to hold the interesting parts of the world in our head.
Work to overcome our biological origins. Build a system that defies the limitations of our working memory. Enforce decoupling. Relish diversity. Keep it simple.