Any discussion of the Dependency Inversion Principle should start by answering the question: What, exactly, is being inverted?
A lot of object-oriented systems start with classes mapping to the higher-level requirements or entities; these get broken down and individual capabilities get drawn out into other classes, and so on. The tendency is to wind up with a pyramidal dependency structure, where any change in the lower reaches of the pyramid tends to bubble up, touching higher and higher-level components.
As an example, let’s think about the
Marshaller classes I discussed in my earlier post on the Single Responsibility Principle. It would be very easy to start writing the service class, decide to break out the
Endpoint class, and do so in a way that made assumptions that you were calling an HTTP web service – for example, you might assume that all responses from the service would have a valid HTTP response code, or that parameters had to be packaged as a URL-encoded query string.
So what happens if your requirements change such that you must directly call a remote SQL store using a different protocol? You’re going to have to change at least two classes, because of assumptions you made about the nature of your data source.
With the Dependency Inversion Principle, we are told that first, we should not write high-level code that depends on low-level concretions – we should connect our components via abstractions; and second, that these abstractions should not depend on implementation details, but vice versa. I’ve seen the “inversion” part of DIP explained a few different ways, but what I see being inverted is the naïve design’s primacy of implementation over interface.
When you start thinking about how to break down subcomponents, take a step back and think about the interfaces between components, and do your best to sanitize them – remove anything that might bite you if implementation details change.
In the case of the
Endpoint, that might mean writing an interface that takes a dictionary of parameter names and values, with no special encoding, and providing for success and failure callbacks. A success callback could give you some generic string or binary representation of the data you requested (which can be passed to a parser/marshaller next). The arguments to the failure callback would be a generic error representation (most platforms have one), with an appropriate app-specific error code and message – not an HTTP status code, or anything else dependent on your data source.
DIP is a key way of limiting technical risk; in this example, after we have changed the interface to be generic with respect to the data source being called, a change to the
Endpoint class requirements necessitates little or no corresponding change to the
Service class, and vice versa.
The Obligatory Recap
Over these past five posts, I’ve covered five principles for building resilient object-oriented systems, with resiliency being defined as resistance to common classes errors, low cost of change, and high comprehensibility (i.e., well-managed complexity).
Here are all five once more, not with their canonical formulations (you could get that from the Wikipedia page on SOLID), but with my own distillation of the core lesson (IMHO) from each:
- Single Responsibility Principle: Give each class one thing to do, and no more.
- Open/Closed Principle: Extend components, rather than modifying them.
- Liskov Substitution Principle: Stay aware of the promises your classes make, and don’t break those promises in subclasses.
- Interface Segregation Principle: Classes should advertise capabilities discretely and generically.
- Dependency Inversion Principle: Details should depend on abstractions, never the other way around.
Go forth, and write some awesome software.
Next, I’ll post something lighter and non-technical. Promise.