September 25, 2012

...Learn TDD with Codemanship

Revisiting Unified Principles of Dependency Management (In Lieu Of 100 Tweets)

Some years ago, I published a slide deck on OO design principles that's proven to be quite popular (about 50,000 downloads) on the web sites.

I'm ashamed to say, due to forgetfulness on my part, the metrics suggested for each principles have long fallen out favour on my own work.

SOLID has formed the basis of how we explain OO design principles for probably 15 or more years, and it's easy to forget that there's nothing scientific about SOLID. The principles are not a theoretically complete explanation, nor are they scientifically tested.

We also inherited (no pun intended) different design principles to think about dependencies at different levels of code organisation.

I went into the wilderness for a couple of years and really dug deep to try and get OO design principles straight in my own mind. I wanted to examine the mechanics of it - the "physics" of dependency management, if you like.

Network models have become popular in physics to explain certain kinds of phenomena, ranging from earthquakes to the runs on the financial markets. It occurred to me that any sound principles of OO design ought to be based on models of propagation through networks.

I built simulations to explore propagation scenarios in simplified models of code dependency networks, and from that formed a set of unified dependency management principles that, I believe, apply at any level of code organisation, and not just in OO programming.

My Four Principles of Dependency Management have an order of precedence.

1. Minimise Dependencies - the simpler our code, the less "things" we have referring to other "things"

2. Localise Dependencies - for the code we have to write, as much as possible, "things" should be packaged - in units of code organisation - together with the "things" they depend on

3. Stabilise Dependencies - of course, we can't put our entire dependency network in the same function (that would be silly). For starters, it's at odds with minimising our dependencies, since modularity is the mechanism for removing duplication, and modularisation inevitably requires some dependencies to cross the boundaries between modules (using the most general meaning of "module" to mean a unit of code reuse - which could be a function or could be an entire system in a network of systems). When dependencies have to cross those boundaries, they should point towards things that are less likely - e.g., harder - to change. This can help to localise the spread of changes across our network of dependencies, in much the same way that a run on the banks is less likely if banks only lend to other banks that are less likely to default.

4. Abstract Dependencies - when we have to depend on something, but still need to accomodate change into system somehow, the easiest way to that is to make things that are depended upon easier to substitute. It's for much the same reason that we favour modular computer hardware. We can evolve and improve our computer by swapping out components with newer ones. To make this possible, computer components need to communicate through standard interfaces. These industry abstractions make make it possible for me to swap out my memory with larger or faster memory, or my hard drive, or my graphics card. If ATI graphics cards had an ATI-specific interface, and NVidia cards had NVidia-specific interfaces, this would not be possible.

I've found it easier to apply these 4 principles at method, class, package and system level, and much easier to explain them. At each level of code organisation, we just need to substitute the right "things" into the formula.

Measuring how well our code follows these principles is easier, too.

1. Measuring the size or complexity of code at various levels of organisation is a doddle. Most tools will do that for you. e.g., method length, method cyclomatic complexity, class size (number of methods), package size (number of classes), and so on.

2. Let's take classes as an example: if classes have, on average, high internal cohesion - that is, the features of that class reference each other a lot - and low external coupling with features of other classes, it could be said that we have localised dependencies. It's the ratio between cohesion and coupling that paints that picture.

3. & 4. Are interrelated. Robert Martin's metrics for Abstractness, Instability and Distance From The Main Sequence are a good fit, once we've generalised them to make it possible to calculate A, I and D for methods, classes, packages and systems.

But what about Interface Segregation and Single Responsibility? The research I did for myself strongly suggested that if your code is simple, cohesive and loosely coupled and your dependencies tend to point in the right direction, these things are of little consequence. They are all sort of covered by the underlying mechanics of code dependencies and therefore these four principles. An interface that only includes methods used by a specific client is, in my opinion, more abstract than an interface that includes methods the client doesn't use. And we tend to find that when we scatter responsibilities across classes, or have classes that do too much, that's covered by 1. and 2.

Posted 9 years, 9 months ago on September 25, 2012