Friday, September 11, 2009

SOLID part 5: Dependency Inversion Principle

This is the fifth post in the SOLID principles series. You might want to checkout the previous entries.

High level classes should not depend on low level classes. Both should depend upon abstractions. Details should depend upon abstractions. Abstractions should not depend upon details.
The purpose of this principle is to enable decoupling of software modules. Managing dependencies is the key for isolate components to reuse later; decoupling is also important for maintenance and evolution since it stops changes in a cohesive piece of software from spreading all over an application.

The problem with dependency is the transitivity: a class depends not only on the other classes which uses directly, but also on these classes dependencies, and so on. It needs them to compile (in languages which require this process) and to work correctly; some kind of dependency between components is present in every software since object needs to work together and to know something about what methods they are going to invoke.
Let's see an example of what transitive dependency means. An object of the class Car depends on an GasEngine to move its passengers where they want to go. The GasEngine itself depends on a Gas class which models the fuel used.
The problem is that a Car should not have to use a forced fuel, like an object of the Gas class. In reality, we have electric cars or LPG ones. A Car depends upon a detail (GasEngine), while it should depend on abstraction. GasEngine is also on a lower level, so this dependency infringes both parts of the DIP.
A feasible refactoring is to introduce an interface (an abstraction) Engine which GasEngine implements and Car declares as a property. This is a powerful step since it break the dependency chain between the high level component (Car) and the low level one (GasEngine).

The Dependency Inversion in this example improves the software design since now Car and GasEngine, which are the details the principle is speaking of, depend on the abstraction Engine. Though, other issues arise when defining an abstraction: how should a Car have a reference to a GasEngine, which needs to work, while it only declares a field to contain an Engine?
There's more than one way to solve this construction problem:
  • Service Locator approach: classes like Car needs a singleton or a static registry where they pull the object they need. For instance, a method Registry::getEngine() returns an Engine whose concrete class needs to be chosen by configuration. This approach is
  • Dependency Injection: this more sophisticated, but simple at the same time, approach let Car declare its dependencies and have them injected by constructor or setters. This is the most widely used technique nowadays to achieve Inversion of Control.
Note that in the Service Locator case, Car would be totally insulated from the concrete GasEngine at compile time since the method invoked at construction includes an Engine in the signature and not a GasEngine. Though, nearly every class has a reference to a global object or static class, lying about its dependencies. Moreover, the Service Locator needs to be transported where the class are being reused.
Constructor Dependency Injection or Setter Dependency Injection is a cleaner choice since business classes have no idea of the framework or Factory which will construct the objects. Car simply declares its constructor:
public function __construct(Engine $engine);
In turn Engine will declars its needed collaborators in the constructor and the developer (or the DI framework) will learn from this signature what he must provide to build a complete object.

As always, let's do an analysis of testatiblity, confronting classes which respect or do not respect the DIP.
The initial Car class is not testable in isolation at all: it builds a GasEngine in its __construct() method and there's no way to replace it for testing purposes, expect reflection. The only thing we can test is a whole Car object, but imagine if tests were done this way in the real world... No one would know why a Car does not work when a problem arises.
The Car which uses a Service Locator is unit testable, since before running a test method we can configure the Service Locator to return a fake/mock Engine instance which follows a canned behavior. By the way, we have the hassle to configure it which can be a tedious work.
The Car which uses Dependency Injection is naturally testable: simply build a fake Engine and pass it in Car when the system under test is created. There is no need to configure other systems, which in this case get in the way instead of helping the developer.

I hope you have enjoyed this series on the five SOLID principles and that your perspective on designing a good application has shifted thanks to these pillars. New posts on object-oriented development will come in the future, you may want to subscribe to the feed to be informed of that.

The image at the top of the article is the hood open of a Ferrari F430 Spider. It depends on an Engine - a FerrariEngine maybe - and not on a GasEngine, since it can mount an ethanol one.

No comments:

ShareThis