33. Abstract Server, Adapter, and Bridge

image

© 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.

Figure 33-1. Simple table lamp

image

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.

Abstract Server

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.

Figure 33-2. A bad way to extend Switch

image

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.

Figure 33-3. ABSTRACT SERVER solution to the table lamp problem

image

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.

Adapter

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.

Figure 33-4. Solving the table lamp problem with ADAPTER

image

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 Class Form of Adapter

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.

Figure 33-5. Solving the table lamp problem with ADAPTER

image

The Modem Problem, Adapters, and LSP

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.

Figure 33-6. Modem problem

image

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.

Figure 33-7. Ideal solution to the modem problem

image

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.

A kludge solution

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.

Figure 33-8. Kludging DedicatedModem to simulate connection state

image

You might imagine that the folks who are building the DedUsers 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.

A tangled web of dependencies

Months later, when there are hundreds of DedUsers, 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 DedUsers 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.

Adapter to the rescue

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.

Figure 33-9. Solving the modem problem with ADAPTER

image

Note that this eliminates all the difficulties we had before. Modem clients are seeing the connection behavior that they expect, and DedUsers don’t have to fiddle with dial or hangup. When the phone number requirement changes, the DedUsers 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.

Bridge

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.

Figure 33-10. Solving the modem problem by merging type hierarchies

image

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.

Figure 33-11. BRIDGE solution to the modem problem

image

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.

Conclusion

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.

Bibliography

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

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

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