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
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
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 AbstractionWhat 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 systemit 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
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]
|