11. The Dependency-Inversion Principle (DIP)

image

© Jennifer M. Kohnke

Nevermore Let the great interests of the State depend Upon the thousand chances that may sway A piece of human frailty

—Sir Thomas Noon Talfourd (1795–1854)


The Dependency-Inversion Principle

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstractions.


Over the years, many have questioned why I use the word inversion in the name of this principle. The reason is that more traditional software development methods, such as structured analysis and design, tend to create software structures in which high-level modules depend on low-level modules and in which policy depends on detail. Indeed, one of the goals of these methods is to define the subprogram hierarchy that describes how the high-level modules make calls to the low-level modules. The initial design of the Copy program in Figure 7-1 is a good example of such a hierarchy. The dependency structure of a well-designed object-oriented program is “inverted” with respect to the dependency structure that normally results from traditional procedural methods.

Consider the implications of high-level modules that depend on low-level modules. It is the high-level modules that contain the important policy decisions and business models of an application. These modules contain the identity of the application. Yet when these modules depend on the lower-level modules, changes to the lower-level modules can have direct effects on the higher-level modules and can force them to change in turn.

This predicament is absurd! It is the high-level, policy-setting modules that ought to be influencing the low-level detailed modules. The modules that contain the high-level business rules should take precedence over, and be independent of, the modules that contain the implementation details. High-level modules simply should not depend on low-level modules in any way.

Moreover, it is high-level, policy-setting modules that we want to be able to reuse. We are already quite good at reusing low-level modules in the form of subroutine libraries. When high-level modules depend on low-level modules, it becomes very difficult to reuse those high-level modules in different contexts. However, when the high-level modules are independent of the low-level modules, the high-level modules can be reused quite simply. This principle is at the very heart of framework design.

Layering

According to Booch, “all well structured object-oriented architectures have clearly-defined layers, with each layer providing some coherent set of services through a well-defined and controlled interface.”1 A naive interpretation of this statement might lead a designer to produce a structure similar to Figure 11-1. In this diagram, the high-level Policy layer uses a lower-level Mechanism layer, which in turn uses a detailed-level Utility layer. Although this may look appropriate, it has the insidious characteristic that the Policy layer is sensitive to changes all the way down in the Utility layer. Dependency is transitive. The Policy layer depends on something that depends on the Utility layer; thus, the Policy layer transitively depends on the Utility layer. This is very unfortunate.

Figure 11-1. Naive layering scheme

image

Figure 11-2 shows a more appropriate model. Each upper-level layer declares an abstract interface for the services it needs. The lower-level layers are then realized from these abstract interfaces. Each higher-level class uses the next-lowest layer through the abstract interface. Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers. Not only is the transitive dependency of PolicyLayer on UtilityLayer broken; so too is the direct dependency of the PolicyLayer on MechanismLayer.

Figure 11-2. Inverted layers

image

Ownership Inversion

Note that the inversion here is one of not only dependencies but also interface ownership. We often think of utility libraries as owning their own interfaces. But when DIP is applied, we find that the clients tend to own the abstract interfaces and that their servers derive from them.

This is sometimes known as the Hollywood principle: “Don’t call us; we’ll call you.”2 The lower-level modules provide the implementation for interfaces that are declared within, and called by, the upper-level modules.

Using this inversion of ownership, PolicyLayer is unaffected by any changes to MechanismLayer or UtilityLayer. Moreover, PolicyLayer can be reused in any context that defines lower-level modules that conform to the PolicyService-Interface. Thus, by inverting the dependencies, we have created a structure that is simultaneously more flexible, durable, and mobile.

In this context, ownership simply means that the owned interfaces are distributed with the owning clients and not with the servers that implement them. The interface is in the same package or library with the client. This forces the server library or package to depend on the client library or package.

Of course, there are times when we don’t want the server to depend on the client. This is especially true when there are many clients but only one server. In that case, the clients must agree on the service interface and publish it in a separate package.

Dependence on Abstractions

A somewhat more naive, yet still very powerful, interpretation of DIP is the simple heuristic: “Depend on abstractions.” Simply stated, this heuristic recommends that you should not depend on a concrete class and that rather, all relationships in a program should terminate on an abstract class or an interface.

• No variable should hold a reference to a concrete class.

• No class should derive from a concrete class.

• No method should override an implemented method of any of its base classes.

Certainly, this heuristic is usually violated at least once in every program. Somebody has to create the instances of the concrete classes, and whatever module does that will depend on them.3 Moreover, there seems no reason to follow this heuristic for classes that are concrete but nonvolatile. If a concrete class is not going to change very much, and no other similar derivatives are going to be created, it does very little harm to depend on it.

For example, in most systems, the class that describes a string is concrete. In C#, for example, it is the concrete class string. This class is not volatile. That is, it does not change very often. Therefore, it does no harm to depend directly on it.

However, most concrete classes that we write as part of an application program are volatile. It is those concrete classes that we do not want to depend directly on. Their volatility can be isolated by keeping them behind an abstract interface.

This is not a complete solution. There are times when the interface of a volatile class must change, and this change must be propagated to the abstract interface that represents the class. Such changes break through the isolation of the abstract interface.

This is the reason that the heuristic is a bit naive. If, on the other hand, we take the longer view that the client modules or layers declare the service interfaces that they need, the interface will change only when the client needs the change. Changes to the classes that implement the abstract interface will not affect the client.

A Simple DIP Example

Dependency inversion can be applied wherever one class sends a message to another. For example, consider the case of the Button object and the Lamp object.

The Button object senses the external environment. On receiving the Poll message, the Button object determines whether a user has “pressed” it. It doesn’t matter what the sensing mechanism is. It could be a button icon on a GUI, a physical button being pressed by a human finger, or even a motion detector in a home security system. The Button object detects that a user has either activated or deactivated it.

The Lamp object affects the external environment. On receiving a TurnOn message, the Lamp object illuminates a light of some kind. On receiving a TurnOff message, it extinguishes that light. The physical mechanism is unimportant. It could be an LED on a computer console, a mercury vapor lamp in a parking lot, or even the laser in a laser printer.

How can we design a system such that the Button object controls the Lamp object? Figure 11-3 shows a naive model. The Button object receives Poll messages, determines whether the button has been pressed, and then simply sends the TurnOn or TurnOff message to the Lamp.

Figure 11-3. Naive model of a Button and a Lamp

image

Why is this naive? Consider the C# code implied by this model (Listing 11-1). Note that the Button class depends directly on the Lamp class. This dependency implies that Button will be affected by changes to Lamp. Moreover, it will not be possible to reuse Button to control a Motor object. In this model, Button objects control Lamp objects and only Lamp objects.


Listing 11-1. Button.cs

public class Button
{
  private Lamp lamp;
  public void Poll()
  {
    if (/*some condition*/)
      lamp.TurnOn();
  }
}


This solution violates DIP. The high-level policy of the application has not been separated from the low-level implementation. The abstractions have not been separated from the details. Without such a separation, the high-level policy automatically depends on the low-level modules, and the abstractions automatically depend on the details.

Finding the Underlying Abstraction

What is the high-level policy? It is the abstraction that underlies the application, the truths that do not vary when the details are changed. It is the system inside the system—it is the metaphor. In the Button/Lamp example, the underlying abstraction is to detect an on/off gesture from a user and relay that gesture to a target object. What mechanism is used to detect the user gesture? Irrelevant! What is the target object? Irrelevant! These are details that do not impact the abstraction.

The model in Figure 11-3 can be improved by inverting the dependency upon the Lamp object. In Figure 11-4, we see that the Button now holds an association to something called a ButtonServer, which provides the interfaces that Button can use to turn something on or off. Lamp implements the ButtonServer interface. Thus, Lamp is now doing the depending rather than being depended on.

Figure 11-4. Dependency inversion applied to Lamp

image

The design in Figure 11-4 allows a Button to control any device that is willing to implement the ButtonServer interface. This gives us a great deal of flexibility. It also means that Button objects will be able to control objects that have not yet been invented.

However, this solution also puts a constraint on any object that needs to be controlled by a Button. Such an object must implement the ButtonServer interface. This is unfortunate, because these objects may also want to be controlled by a Switch object or some kind of object other than a Button.

By inverting the direction of the dependency and making the Lamp do the depending instead of being depended on, we have made Lamp depend on a different detail: Button. Or have we?

Lamp certainly depends on ButtonServer, but ButtonServer does not depend on Button. Any kind of object that knows how to manipulate the ButtonServer interface will be able to control a Lamp. Thus, the dependency is in name only. And we can fix that by changing the name of ButtonServer to something a bit more generic, such as SwitchableDevice. We can also ensure that Button and SwitchableDevice are kept in separate libraries, so that the use of SwitchableDevice does not imply the use of Button.

In this case, nobody owns the interface. We have the interesting situation whereby the interface can be used by lots of different clients, and implemented by lots of different servers. Thus, the interface needs to stand alone without belonging to either group. In C#, we would put it in a separate namespace and library.4

The Furnace Example

Let’s look at a more interesting example. Consider the software that might control the regulator of a furnace. The software can read the current temperature from an I/O channel and instruct the furnace to turn on or off by sending commands to a different I/O channel. The structure of the algorithm might look something like Listing 11-2.

image


Listing 11-2. Simple algorithm for a thermostat

const byte TERMOMETER = 0x86;
const byte FURNACE = 0x87;
const byte ENGAGE = 1;
const byte DISENGAGE = 0;

void Regulate(double minTemp, double maxTemp)
{
  for(;;)
  {
    while (in(THERMOMETER) > minTemp)
      wait(1);
    out(FURNACE,ENGAGE);

    while (in(THERMOMETER) < maxTemp)
      wait(1);
    out(FURNACE,DISENGAGE);
  }
}


The high-level intent of the algorithm is clear, but the code is cluttered with lots of low-level details. This code could never be reused with different control hardware.

This may not be much of a loss, since the code is very small. But even so, it is a shame to have the algorithm lost for reuse. We’d rather invert the dependencies and see something like Figure 11-5.

Figure 11-5. Generic regulator

image

This shows that the Regulate function takes two arguments that are both interfaces. The Thermometer interface can be read, and the Heater interface can be engaged and disengaged. This is all the Regulate algorithm needs. Now it can be written as shown in Listing 11-3

This has inverted the dependencies such that the high-level regulation policy does not depend on any of the specific details of the thermometer or the furnace. The algorithm is nicely reusable.


Listing 11-3. Generic regulator

void Regulate(Thermometer t, Heater h,
      double minTemp, double maxTemp)
{
  for(;;)
  {
    while (t.Read() > minTemp)
      wait(1);
    h.Engage();

    while (t.Read() < maxTemp)
      wait(1);
    h.Disengage();
  }
}


Conclusion

Traditional procedural programming creates a dependency structure in which policy depends on detail. This is unfortunate, since the policies are then vulnerable to changes in the details. Object-oriented programming inverts that dependency structure such that both details and policies depend on abstraction, and service interfaces are often owned by their clients.

Indeed, this inversion of dependencies is the hallmark of good object-oriented design. It doesn’t matter what language a program is written in. If its dependencies are inverted, it has an OO design. If its dependencies are not inverted, it has a procedural design.

The principle of dependency inversion is the fundamental low-level mechanism behind many of the benefits claimed for object-oriented technology. Its proper application is necessary for the creation of reusable frameworks. It is also critically important for the construction of code that is resilient to change. Since abstractions and details are isolated from each other, the code is much easier to maintain.

Bibliography

[Booch96] Grady Booch, Object Solutions: Managing the Object-Oriented Project, Addison-Wesley, 1996.

[GOF95] Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.

[Sweet85] Richard E. Sweet, “The Mesa Programming Environment,” SIGPLAN Notices, 20(7) July 1985: 216–229.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.237.186.170