A Unified Theory of Software Design, Architecture and Everything
It's probably an obvious symptom for my being on a downward spiral from software activist to theoretician, but I'll do it anyway. I'm going to present you with a naive unified theory of software design, which I will call DRE: Dependencies Rule Everything. I consider it a real pity that the most common use of this abbreviation until today is Digital Rectal Examination. This has to change.
I have been wondering for a while now why software design principles, heuristics and trade-offs are being described in so many different terms: coupling, cohesion, SOLID, LSP, DRY, LoD, you name it. It has always seemed obvious to me that there is only one concept that covers all the others: dependencies.
Here is my definition:
Party A depends on party B means: If party B changes one or more of its observable aspects, there is some probability (greater than zero) that party A will have to adapt.
Let's examine the building blocks of this definition and its implications more closely:
- I use the word "party" instead of some more code-centric expression like "module" or "component". Rationale: There are dependencies between pieces of my software (functions, classes, modules, subsystems, components etc) but there are also dependencies onto the outer world - the domain. For instance, within DRE I would consider the method who implements the logic for withdrawing money from an account as dependent on the domain rule how money should be withdrawn. Thus, DRE dependencies are a superset of static code dependencies.
- Change, the probability of the change and the probability of the change to affect the dependent together define the strength of a dependency. If the dependee can change (eg. disappear) without forcing the dependent to adapt there is no dependency worth mentioning. In that sense loose coupling has the goal to lower the probability of a change affecting the dependent and static typing usually raises the probability, e.g. by enforcing the number of arguments to a function call.
- I am only interested in the observables of a party like interface, timing behaviour, error conditions, availability, cost. To put it differently: I don't care about unobservable implementation details or anything that can change without the dependent noticing.
- A dependency between individual parts results in a dependency between the respective aggregates. The more and the stronger the dependencies between the parts, the stronger the dependency between the aggregates.
- Dependencies are directed and that's a good thing. On an atomic level there must not be bidirectional dependencies since otherwise change would result in an unstable state. On an aggregate level bidirectional dependencies arise much too easy if you are not careful.
- For most cases the dependency relation is not transitive. In some programming languages it seems like they are, but that's usually because public visibility of inner parts is the default and the Law of Demeter strikes. One notable exception to the rule is facilitated by C/C++ compilers, which enforce recompilation of statically dependent components and thus propagate change all the way up.
- There exist at least three different major species of dependencies: compile-time dependencies, runtime dependencies and domain-rule dependencies. Statically typed and dynamically typed languages do different trade-offs between compile-time and runtime dependencies; type information is one kind of explicit dependency, which is obviously preferable over implicit - i.e. potentially unknown - dependencies. Speaking of explicitness: unit tests and acceptance tests are a different way of making dependencies explicit. Many of the techniques that promise you a loosely coupled design do nothing else than going from explicit dependencies (a statically enforced method call) to implicit dependency by putting some obfuscating mechanism (e.g. XML serializing) in-between. This does not help you a thing, it just makes the system slower and more complex by introducing additional dependencies to libraries and structured documents. Unless, of course, if you really really really need it for cross-platform, cross-version, cross-process communication.
What is Good Design then?
A good design - or architecture for that matter - in DRE is one in which the overall of dependencies and their strengths is minimized. Since strength is defined as the probability that a change will hit you there is quite some fortune telling involved in finding "the best" of all designs. In other words: if you cannot agree on a probable future you cannot agree on a good design. That's why Agile designers use a simple heuristic to foretell the future: Everything will change but we don't know how. In DRE-terms that means: optimizing dependencies within the system and assuming that the world outside (the domain and its rules) has zero probability to change. In practice this leads to lots of design changes in early stages of a project until the typical domain changes have been incorporated in internal design elements and will only affect the outer-most "parties" (e.g. the adapter class, the configuration file, the business rules database).
What follows from that is: If you really know the future, the agile approach of evolutionary design is not the best. But be honest, who the hell does?
Reformulating OO Design Principles
For most heuristics of good OO design it's quite easy to see why they work at least as well in the DRE universe. I leave it to the astute reader to translate the SOLID principles into DRE speak. One central rule is not so easy to reconcile with DRE, namely Don't Repeat Yourself. I'll try it anyway:
Consider a simple case of duplication:
def calculationOne() { ... def salesTax = amount * 0.19 ... } def calculationTwo() { ... def salesTax = amount * 0.19 ... }
Given a probability of p
that the sales tax rate will change during the system's life cycle, you have two dependencies with a strength of p, so your total dependency number is 2p
. Let's now apply DRY in a straightforward matter:
def calculationOne() { ... def salesTax = calculateSalesTax(amount) ... } def calculationTwo() { ... def salesTax = calculateSalesTax(amount) ... } def calculateSalesTax(amount) { return amount * 0.19 }
Given a probability of r
that we will have to change the interface of calculateSalesTax
later on, the overall dependency number is now 2r + p
; if we have chosen our abstraction wisely, this figure will be lower than 2p
. Thus, the code with less duplication is better design in DRE theory. Quod erat demonstrandum.
So what?
There are two striking reasons why the unified theory appears attractive to me:
- It gives me the vocabulary to talk about the relation between different design approaches and thereby tackle questions like "What has loose coupling to do with the SOLID principles?" and "Does this decoupling technique really decouple anything?".
- It might provide me with a quantifiable means to compare different designs. Any tool vendors, contact me for licensing the approach!
Now it's up to you all to tell me why this is utter nonsense or perfectly useless or both. And I do appreciate congratulations for my future nobel prize on computer science. Shoot!
Update:
To make that clear, it was not my intention to suggest that simply adding up propabilities would make for a mathematically sound model for a "design quality number"; it was just the simplest thing to do and it felt intuitive enough.
Leave a comment