© Jennifer M. Kohnke
Politicians are the same all over.
They promise to build a bridge even where there is no river.
—Nikita Khrushchev
In the mid-1990s I was deeply involved with the discussions that coursed through the comp.object newsgroup. Those of us who posted messages on that newsgroup argued furiously about various strategies of analysis and design. At one point, we decided that a concrete example would help us evaluate one another’s positions. So we chose a very simple design problem and proceeded to present our favorite solutions.
The design problem was extraordinarily simple. We determined to show how we would design the software inside a simple table lamp. The table lamp has a switch and a light. You could ask the switch whether it was on or off, and you could tell the light to turn on or off: nice, simple problem.
The debate raged for months. Some people used a simple approach of only a switch and a light object. Others thought there ought to be a lamp object that contained the switch and the light. Still others thought that electricity should be an object. One person suggested a power cord object.
Despite the absurdity of most of those arguments, the design model is interesting to explore. Consider Figure 33-1. We can certainly make this design work. The Switch
object can poll the state of the actual switch and can send appropriate turnOn
and turnOff
messages to the Light
object.
What don’t we like about this design? Two of our design principles are being violated by this design: the Dependency-Inversion Principle (DIP) and the Open/Closed Principle. (OCP) The violation of DIP is easy to see; the dependency from Switch
to Light
is a dependency on a concrete class. DIP tells us to prefer dependencies on abstract classes. The violation of OCP is a little less direct but is more to the point. We don’t like this design, because it forces us to drag a Light
along everywhere we need a Switch
. Switch
cannot be easily extended to control objects other than Light
.
You might be thinking that you could inherit a Switch
subclass that would control something other than a light, as in Figure 33-2. But this doesn’t solve the problem, because FanSwitch
still inherits the dependency on Light
. Wherever you take a FanSwitch
, you’ll have to bring Light
along. In any case, that particular inheritance relationship also violates DIP.
To solve the problem, we invoke one of the simplest of all design patterns: ABSTRACT SERVER (see Figure 33-3). By introducing an interface between the Switch
and the Light
, we have made it possible for Switch
to control anything that implements that interface. This immediately satisfies both DIP and OCP.
As an interesting aside, note that the interface is named for its client. It is called Switchable
rather than Light
. We’ve talked about this before, and we’ll probably do so again. Interfaces belong to the client, not to the derivative. The logical binding between the client and the interface is stronger than that between the interface and its derivatives. The logical binding is so strong that it makes no sense to deploy Switch
without Switchable
, and yet it makes perfect sense to deploy Switchable
without Light
. The strength of the logical bonds is at odds with the strength of the physical bonds. Inheritance is a much stronger physical bond than association is.
In the early 1990s, we used to think that the physical bond ruled. Reputable books recommended that inheritance hierarchies be placed together in the same physical package. This seemed to make sense, since inheritance is such a strong physical bond. But over the past decade, we have learned that the physical strength of inheritance is misleading and that inheritance hierarchies should usually not be packaged together. Rather, clients tend to be packaged with the interfaces they control.
This misalignment of the strength of logical and physical bonds is an artifact of statically typed languages, such as C#. Dynamically typed languages, such as Smalltalk, Python, and Ruby, don’t have the misalignment, because they don’t use inheritance to achieve polymorphic behavior.
A problem with the design in Figure 33-3 is the potential violation of the Single-Responsibility Principle (SRP). We have bound together two things, Light
and Switchable
, that may not change for the same reasons. What if we can’t add the inheritance relationship to Light
? What if we purchased Light
from a third party and don’t have the source code? What if we want a Switch
to control a class that we can’t derive from Switchable
? Enter the ADAPTER.1
Figure 33-4 shows how an Adapter
can be used to solve the problem. The adapter derives from Switchable
and delegates to Light
. This solves the problem neatly. Now we can have any object that can be turned on or off controlled by a Switch
. All we need to do is create the appropriate adapter. Indeed, the object need not even have the same turnOn
and turnOff
methods that Switchable
has. The adapter can be adapted to the interface of the object.
Adapters don’t come cheap. You need to write the new class, and you need to instantiate the adapter and bind the adapted object to it. Then, every time you invoke the adapter, you have to pay for the time and space required for the delegation. So clearly, you don’t want to use adapters all the time. The ABSTRACT SERVER solution is quite appropriate for most situations. In fact, even the initial solution in Figure 33-1 is pretty good unless you happen to know that there are other objects for Switch
to control.
The LightAdapter
class in Figure 33-4 is known as an object-form adapter. Another approach, known as the class-form adapter, is shown in Figure 33-5. In this form, the adapter object inherits from both the Switchable
interface and the Light
class. This form is a tiny bit more efficient than the object form and is a bit easier to use but at the expense of using the high coupling of inheritance.
Consider the situation in Figure 33-6. We have a large number of modem clients all making use of the Modem
interface. The Modem
interface is implemented by several derivatives, including HayesModem
, USRoboticsModem
, and ErniesModem
. This is a pretty common situation. It conforms nicely to OCP, LSP, and DIP. Clients are unaffected when there are new kinds of modems to deal with. Suppose that this situation were to continue for several years. Suppose that there were hundreds of modem clients all making happy use of the Modem
interface.
Now suppose that our customers have given us a new requirement. Certain kinds of modems, called dedicated modems,2 don’t dial. but sit at both ends of a dedicated connection. Several new applications use these dedicated modems and don’t bother to dial. We’ll call these the DedUsers
. However, our customers want all the current modem clients to be able to use these dedicated modems, Telling us that they don’t want to have to modify the hundreds of modem client applications. Those modem clients will simply be told to dial dummy phone numbers.
If we had our druthers, we might want to alter the design of our system as shown in Figure 33-7. We’d make use of ISP to split the dialing and communications functions into two separate interfaces. The old modems would implement both interfaces, and the modem clients would use both interfaces. The DedUsers
would use nothing but the Modem
interface, and the DedicatedModem
would implement only the Modem
interface. Unfortunately, this requires us to make changes to all the modem clients, something that our customers forbade.
So what do we do? We can’t separate the interfaces as we’d like, yet we must provide a way for all the modem clients to use DedicatedModem
. One possible solution is to derive DedicatedModem
from Modem
and to implement the Dial
and Hangup
functions to do nothing, as follows:
class DedicatedModem : Modem
{
public virtual void Dial(char phoneNumber[10]) {}
public virtual void Hangup() {}
public virtual void Send(char c)
{...}
public virtual char Receive()
{...}
}
Degenerate functions are a sign that we may be violating LSP. The users of the base class may be expecting Dial
and Hangup
to significantly change the state of the modem. The degenerate implementations in DedicatedModem
may violate those expectations.
Let’s presume that the modem clients were written to expect their modems to be dormant until Dial
is called and to return to dormancy when Hangup
is called. In other words, they don’t expect any characters to be coming out of modems that aren’t dialed. DedicatedModem
violates this expectation. It will return characters before Dial
has been called and will continue to return them after Hangup
has been called. Thus, DedicatedModem
may crash some of the modem clients.
You might suggest that the problem is with the modem clients. They aren’t written very well if they crash on unexpected input. I’d agree with that. But it’s going to be difficult to convince the folks who have to maintain the modem clients to make changes to their software because we are adding a new kind of mode. Not only does this violate OCP, but also it’s just plain frustrating. Besides, our customer has explicitly forbidden us to change the modem clients.
We can simulate a connection status in Dial
and Hangup
of DedicatedModem
. We can refuse to return characters if Dial
has not been called or after Hangup
has been called. If we make this change, all the modem clients will be happy and won’t have to change. All we have to do is convince the DedUsers
to call dial
and hangup
. See Figure 33-8.
You might imagine that the folks who are building the DedUser
s find this pretty frustrating. They are explicitly using DedicatedModem
. Why should they have to call Dial
and Hangup
? However, they haven’t written their software yet, so it’s easier to get them to do what we want.
Months later, when there are hundreds of DedUser
s, our customers present us with a new change. It seems that all these years, our programs have not had to dial international phone numbers. That’s why they got away with the char[10]
in dial
. Now, however, our customers want us to be able to dial phone numbers of arbitrary length. They have a need to make international calls, credit card calls, PIN-identified calls, and so on.
Clearly, all the modem clients must be changed. They were written to expect char[10]
for the phone number. Our customers authorize this change because they have no choice, and hordes of programmers are put to the task. Just as clearly, the classes in the modem
hierarchy must change to accommodate the new phone number size. Our little team can deal with that. Unfortunately, we now have to go to the authors of the DedUser
s and tell them that they have to change their code! You might imagine how happy they’ll be about that. They aren’t calling dial
because they need to. They are calling Dial
because we told them they have to. And now they are going through an expensive maintenance job because they did what we told them to do.
This is the kind of nasty dependency tangle that many projects find themselves in. A kludge in one part of the system creates a nasty thread of dependency that eventually causes problems in what ought to be a completely unrelated part of the system.
We could have prevented this fiasco by using ADAPTER to solve the initial problem, as shown in Figure 33-9. In this case, DedicatedModem
does not inherit from Modem
. The modem clients use DedicatedModem
indirectly through the DedicatedModemAdapter
. This adapter implements Dial
and Hangup
to simulate the connection state. The adapter delegates send
and recieve
calls to the DedicatedModem
.
Note that this eliminates all the difficulties we had before. Modem clients are seeing the connection behavior that they expect, and DedUser
s don’t have to fiddle with dial
or hangup
. When the phone number requirement changes, the DedUser
s will be unaffected. Thus, by putting the adapter in place, we have fixed both LSP and OCP violations.
Note that the kludge still exists. The adapter is still simulating connection state. You may think that this is ugly, and I’d certainly agree with you. However, note that all the dependencies point away from the adapter. The kludge is isolated from the system, tucked away in an adapter that barely anybody knows about. The only hard dependency on that adapter will likely be in the implementation of some factory3 somewhere.
There is another way to look at this problem. The need for a dedicated modem has added a new degree of freedom to the Modem
type hierarchy. When the Modem
type was conceived, it was simply an interface for a set of different hardware devices. Thus, we had HayesModem
, USRModem
, and ErniesModem
deriving from the base Modem
class. Now, however, it appears that there is another way to cut at the modem
hierarchy. We could have DialModem
and DedicatedModem
deriving from Modem
.
Merging these two independent hierarchies can be done as shown in Figure 33-10. Each of the leaves of the type hierarchy puts either a dialup or dedicated behavior onto the hardware it controls. A DedicatedHayesModem
object controls a Hayes modem in a dedicated context.
This is not an ideal structure. Every time we add a new piece of hardware, we must create two new classes: one for the dedicated case and one for the dialup case. Every time we add a new connection type, we have to create three new classes, one for each of the different pieces of hardware. If these two degrees of freedom are at all volatile, we could soon wind up with a large number of derived classes.
We can solve this problem by applying the BRIDGE pattern. This pattern often helps when a type hierarchy has more than one degree of freedom. Rather than merge the hierarchies, we can separate them and tie them together with a bridge.
Figure 33-11 shows the structure. We split the modem
hierarchy into two hierarchies. One represents the connection method, and the other represents the hardware.
Modem users continue to use the Modem
interface. ModemConnectionController
implements the Modem
interface. The derivatives of ModemConnectionController
control the connection mechanism. DialModemController
simply passes the dial
and hangup
method to dialImp
and hangImp
in the ModemConnectionController
base class. Those methods then delegate to the ModemImplementation
class, where they are deployed to the appropriate hardware controller. DedModemController
implements dial
and hangup
to simulate the connection state. It passes send
and receive
to sendImp
and receiveImp
, which are then delegated to the ModemImplementation
hierarchy as before.
Note that the four imp
functions in the ModemConnectionController
base class are protected; they are to be used strictly by derivatives of ModemConnectionController
. No one else should be calling them.
This structure is complex but interesting. We are able to create it without affecting the modem users, yet it allows us to completely separate the connection policies from the hardware implementation. Each derivative of ModemConnectionController
represents a new connection policy. That policy can use sendImp
, receiveImp
, dialImp
, and hangImp
to implement that policy. New imp
functions could be created without affecting the users. ISP could be used to add new interfaces to the connection controller classes.
This could create a migration path that the modem clients could slowly follow toward an API that is higher level than dial
and hangup
.
One might be tempted to suggest that the real problem with the Modem
scenario is that the original designers got the design wrong. They should have known that connection and communication were separate concepts. Had they done a little more analysis, they would have found this and corrected it. So, it is tempting to blame the problem on insufficient analysis.
Poppycock! There is no such thing as enough analysis. No matter how much time you spend trying to figure out the perfect software structure, you will always find that the customer introduces a change that violates that structure.
There is no escape from this. There are no perfect structures. There are only structures that try to balance the current costs and benefits. Over time, those structures must change as the requirements of the system change. The trick to managing that change is to keep the system as simple and as flexible as possible.
The ADAPTER solution is simple and direct. It keeps all the dependencies pointing in the right direction, and it’s very simple to implement. The BRIDGE solution is quite a bit more complex. I would not suggest embarking down that road until you had very strong evidence that you needed to completely separate the connection and communication policies and that you needed to add new connection policies.
The lesson here, as always, is that a pattern is something that comes with both costs and benefits. You should find yourself using the ones that best fit the problem at hand.
[GOF95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.