The Bridge Pattern

CpSc 872 Software Engineering


Abstract

Design patterns, in object oriented programming, have been increasingly adopted as desirable design aids, and tools. The Bridge pattern is used to decouple abstractions from implementations. This paper presents the Bridge pattern, discusses it's use as a pure design pattern, and compares it's structure to that of the Adapter pattern.

Table of Contents


The Bridge Pattern

Intent, Motivation, and Applicability

The intent of the Bridge pattern is to "Decouple an abstraction from its implementation so that the two can vary independently." (Gamma, 1995, pg 151) This means that the bridge pattern provides a separation of some or all of an abstractions interface from the abstraction itself.

The Bridge pattern is motivated by the the desire to separate the interface, and subsequently the implementation of the interface, from an abstraction. The example motivation given by Gamma (Gamma, 1995, pg 151) is based on the implementation of a Window graphical component, which is desired to be used across multiple platforms.

Suppose you have a Window abstraction that starts off with implementations for X-windows (Unix) and Microsoft Windows. For each type of window, an icon window, a picture window, etc, you need to create two subclasses, one for each platform of implementation. In fact any subclass of the Window abstraction needs this further dual implementation. This problem is magnified if you want to make the Window abstraction work on additional platforms, such as the Macintosh. For each existing subclass of Window, an implementation must be created and added to the inheritance hierarchy. This illustrates the lack of flexibility of a simple inheritance structure, and thus is the motivation for the Bridge pattern.

With the Bridge pattern, the Window abstraction becomes easier to implement. On the abstraction side, we have a Window hierarchy of all the abstract window types, icon window, picture window, etc. The implementations of these windows, or rather the interfaces, are provided by a separate hierarchy of implementations. The link is provided by a reference to an implementation object, maintained in the Window hierarchy. Thus, any implementation, or platform specific methods are delegated to the implementation object, which is an instance of the specific implementation currently in use. This link and separation is the essence of the Bridge pattern.

Gamma describes several situations when the Bridge pattern may be used. (Gamma, 1995, pg 151)

Desire to avoid a permanent binding between an abstraction and implementation
There are some situations where it is beneficial to avoid a static, permanent binding to implementations. For example, if it is desired to change the implementation of window appearances at run-time in a graphical user interface. This behavior can be observed in used in Windows 95, the Macintosh OS, as well as the Java Swing components of the Java AWT.

When abstractions and implementations should be extensible through subclassing.
If there is a case where there is potential for extending abstractions, or the possibility of new or additional implementations, then the Bridge pattern may be used to provide this flexibility for future designs.

When clients should be protected from Implementation changes.
If you are developing a class library, then any updates to the implementation code should still work with existing code (backwards compatibility). Through the use of the Bridge pattern, this ensures that the clients will not even need to be recompiled. Any implementation updates are completely transparent to and have no impact on the client.

When the implementation should be completely hidden from the client.
In some languages, such as C++, the implementation of a class is inadvertently available to the client. With the Bridge pattern, the implementation can be completely separate from the abstraction, and so can be provided as simply a precompiled object file to the client, completely hiding the implementation details.

If there is a proliferation of classes.
As observed from the motivation, there can be cases where an abstraction as many different extensions and a potentially large inheritance hierarchy. By separating the implementations, this hierarchy can be generalized to allow for a more easily managed set of classes.

If, unknown to the client, implementations are shared among objects.
There are some situations where it is desirable to share objects within a system, yet hide this fact from the client. That is, there may be some heavy weight but static objects in a system, which make more sense for the client to maintain a simple reference to. However, you want the client to think that they are dealing with an individual object of the given abstraction. Thus, the implementation would simply return a reference to an existing object and could then maintain a reference count for the shared object. This counting mechanism would be completely hidden from the client, since the client simply deals with the abstraction interface.

Structure, Participants, and Collaborations


figure 1 - Structure of Bridge Pattern.

The Bridge pattern, as illustrated in figure 1, is composed of four main participants, the Abstraction and it's RefinedAbstraction(s), and the Implementor along with it's ConcreteImplementation(s).

The Abstraction defines the interface that the client uses for interaction with this abstraction. That is, the client makes requests directly to the Abstraction object, as that is the only interface known to the client. The Abstraction object also maintains a reference to an Implementor object. The collaboration between the objects in this pattern is such that the clients requests are forwarded by the Abstraction to the Implementor through this reference. A RefinedAbstraction, then, is simply any and all extensions to the Abstraction class. The Implementor defines the interface for any and all implementations of the Abstraction. There is no requirement that the Abstraction interface and the Implementor interface have a 1 to 1 correspondence. This is one of the main reasons for the additional flexibility gained from this pattern. For example, the Abstraction could be a vehicle with a draw() method, and the Implementor interface may have the methods drawWheel() and drawBody(). draw(), then, could make one call to drawBody() and four calls to drawWheel(). As Gamma states, "Typically the Implementor interface provides only primitive operations, and Abstraction defines higher-level operations based on these primitives." (Gamma, 1995, pg 154) As may be guessed, the ConcreteImplementor simply implements the interface defined by the Implementor class. That is, it defines a concrete implementation of the Abstraction.

Consequences

Gamma outlines three main consequences of the Bridge pattern; there is a decoupling of the interface and implementation, there is improved extensibility, and there is a hiding of the implementation details from clients. (Gamma, 1995, pg 154)

The most obvious consequence of the Bridge pattern is the decoupling of the implementation from the interface. This in beneficial in many ways, including the addition of the ability to dynamically select and change an implementation. This dynamic implementation selection leads to more easily modifiable code bases, and in a graphical user interface can be leveraged to enhance the user experience by providing higher user level control over system behavior. The decoupling of the interface provides a level of protection at the code level, because implementation changes do not force recompilations of the abstractions. Gamma also asserts that "this decoupling encourages layering (which) can lead to a better structured system."

The other consequences of the Bridge pattern includes the improved extensibility of classes and the additional capability for hiding implementation details from clients. The extensibility is improved because additional refined abstractions can be created without the need for adding any additional implementations. Likewise, extra concrete implementations can be developed which can be used transparently with the existing and future abstractions. Further, these implementations are effectively hidden from the client, allowing the client to focus on just the behavior desired from the abstraction.

In addition to Gamma's three consequences, the Bridge pattern, unlike some other design patterns, is best leveraged as a pure design-time pattern. That is, it is most applicable during the design of a system. This consequence is discussed further in the section Design Use Of The Bridge Pattern.

Implementation Issues

Gamma outlines four issues to consider when applying the Bridge pattern; abstractions that have only one implementation, creating the right Implementor, sharing implementors, and the use of multiple inheritance. (Gamma, 1995, pp 155 - 156)

In some cases, there might only be one implementation for an abstraction. In these cases, the need for the abstract Implementor class is not obvious, and so only a concrete implementation is used. The temptation to not even separate the implementation from the abstraction exists, but Gamma supports the use of the Bridge pattern in this situation if, additionally, "the implementation of a class must not affect its existing clients". (Gamma, 1995, pg 155)

The Bridge pattern does not describe how, where, or when to create the correct Implementor, and so this is a decision to be made during implementation. Gamma suggests three possible approaches; set the Implementor upon construction based on parameters sent the the Abstracts constructor, use a default Implementor and change later as needed, or let a completely different object configure the Bridge. The first two approaches imply that Abstraction would know about all possible ConcreteImplementations. That is, for a class to take advantage of additional ConcreteImplementations (ie extensions to the Implementor), the AbstractClass, and possibly the RefinedAbstractions would need to be updated. This appears to be in slight disagreement with the decoupling and improved extensibility consequences of using the Bridge pattern. However, the implementation details would still be hidden from the client. The third suggested approach is probably to best way to ensure a complete decoupling of the interface and implementation. This is achieved through the use of a factory object (Abstract Factory Gamma, 1995, pp 87 - 95). By letting a separate object configure the bridge, a total decoupling of the Abstractions and Implementors is achieved. Further, if the Implementor is extended, then only the factory object needs to be updated, and not the abstractions. Likewise, any changes or additions to the Abstractions would induce only potential changes to the factory object, while the Implementor and ConcreteImplementations can remain unaware of the Abstractions, thus maintaining the increased extensibility benefits of the Bridge pattern.

One potential strategy which can be used in a design is to plan to share implementations among several objects. That is, an implementation, when used by an abstraction, keeps track of the number of abstractions using it. This leads to reduced resource requirements for a system, as it promotes object sharing within the system. For example, in a graphical windowing system, there will be multiple windows open at any given time. It would be wasteful of resources to use a new implementor for every window, and so the implementors could be shared among the open windows.

The final issue mentioned by Gamma concerns the use of multiple inheritance to implement Bridged objects in C++. In languages which support multiple inheritance, it would be possible for an object to inherit from both an abstraction and an implementation. The reason for doing this is would be to combine an interface with its implementation. However, this can lead to some disagreement with the intent of the Bridge pattern, which is to decouple the abstraction from the interface to allow them to vary independently. If the inheritance were not a static inheritance, then objects such as these could adequately reflect the intent of the Bridge pattern, as the implementation would not be bound permanently to the interface. However, "because this approach relies on static inheritance ... you can't implement a true Bridge with multiple inheritance, at least not in C++." (Gamma, 1995, pg 156)

Known Uses & Related Patterns

The Bridge pattern is used extensively in graphical windowing systems as well as application libraries and development kits. Gamma provides a large list of known uses. (Gamma, 1995, pp 160 - 161) A more recent example can be shown through a cursory examination of the Abstract Window Toolkit which has been developed along with the development of the Java language.

One of the features of the Java language is that it is designed to run on a virtual machine, which may be implemented across multiple platforms, allowing true cross platform development. The AWT provides a consistent interface for development and provides for all the common classes usually found in a graphical windowing environment. The developer writes all software to use the AWT interfaces. However, when Java programs are run on a host system, the actual graphical objects that get drawn are the native components for that platform. This is a direct use of the Bridge pattern, in fact it matches the window abstraction example described earlier in the Intent, Motivation, and Applicability section. That is, clients interact (send requests) specifically with the AWT interface (Abstraction). These requests get handled by the AWT Components associated peer object through the peer interface (ConcreteImplementation) which is native to the platform running the application. (Zukowski, 1997, pp 497 - 500) A further example of the Bridge pattern in Java is with the addition of the pluggable look and feel capabilities included in the Swing components. (JavaSoft, 1998) These capabilities allow for dynamic modification of the appearance of buttons, window frames, and other graphical interface objects. This is done with the introduction of the user interface manager, from which the Swing components get the specific implementation details. This is essentially the Abstract Factory approach described previously in the Implementation Issues section.

The Bridge pattern is a fairly powerful and encompassing pattern. As such, Bridge shares some attributes and can make use of other design patterns. As mentioned previously in the Implementation Issues section, a Bridge may make use of an Abstract Factory for creation and configuration. In addition to this, the structure of the Bridge pattern is very similar to the structure found in one form of the Adapter pattern. This relationship is discussed further in the Comparison To Adapter Pattern.


Design Use Of The Bridge Pattern

One potential activity with a design pattern is to use it as a basis for creating a class library or template for further use. This can be, and is, done with many patterns. For example, the Collection framework of Java's JDK 1.2 implements the Iterator pattern among others. (Gamma, 1995, pg 257, Jones, 1998) However, not all patterns are suited for implementation in such a manner. Rather, some patterns are purely design patterns. The Bridge pattern is one example of a purely design pattern.

Consider a simple Java implementation of the Bridge pattern:

// new exception class for when problems arise
public class NoImplementorException extends Exception {
    public NoImplementorException(){ super();}
    public NoImplementorException(String s){ super(s);}
}

//  Implementor interface
public interface Implementor(){
    public Object OperationImp();
    public Object OperationImp(Object o);
}

// Abstraction as a class
public abstract class Abstraction{
    protected Implementor imp;

    public Abstraction(){
        imp = null;
    }

    public Abstraction(Implementor i){
        imp = i;
    }

    public Object Operation() throws NoImplementorException {
        if(imp == null) throw new NoImplementorException();
        return imp.OperationImp();
    }

    public Object Operation(o) throws NoImplementorException {
        if(imp == null) throw new NoImplementorException();
        return imp.OperationImp(o);
    }
}

// Abstraction as an interface
public interface Abstraction{
    public Object Operation() throws NoImplementorException;
    public Object Operation(Object o) throws NoImplementorException;
    protected Implementor getImplementor() throws
NoImplementorException;
}
The simple implementation is obviously lacking in usefulness, simply because it is overly general. This generality is a consequence of the lack of knowledge about the specific domains in which the pattern may be applied. It is quite easy to imagine implementations where you will require more than one or two methods to be separated from an abstraction! Further, this implies that there is a 1 to 1 correspondence between the methods of the Abstraction, and the methods of the Implementor. This is not always the case, and does not allow for the possibility of Abstraction methods which call basic elemental operations in the Implementor. For example, consider an abstraction Square with the method draw() and it's corresponding Implementor SquareImplementor with the method drawLine(). The draw() method may simply call drawLine() of the implementor four times in order to draw the square.

One potential way to increase the usefulness of a Bridge library object, would be to provide a mechanism to allow the dynamic creation and registration of methods with both the Abstraction and Implementor. Although this could be done using the unique method update capabilities of object based languages (Abadi & Cardelli, 1996, pg 37), it would still involve some level of overhead, and is very difficult to accomplish in class based languages which lack native support for dynamic methods such as function pointers in C++. In any case, there would still be overhead incurred through the dynamic management of the methods. Essentially, the actual implementation of the Bridge pattern is very much domain dependent. That is, every application of the pattern heavily depends on what the Abstraction is, and how it is related to its Implementors. Thus, Bridge is best "used up-front in a design to let abstractions and implementations vary independently." (Gamma, 1995, pg 161)


Comparison To Adapter Pattern

As mentioned previously, one of the patterns related to Bridge is the Adapter pattern. The Adapter pattern is used to convert or remap interfaces, which "lets classes work together that couldn't otherwise because of incompatible interfaces." (Gamma, 1995, pg 139) The Adapter pattern has two general forms, a class adapter, and an object adapter. The class adapter makes use of multiple inheritance, with the Adapter inheriting from both a Target class, and the Adaptee class. The implementations of the Targets methods by the adapter simply map to the appropriate methods of the Adaptee class. The object adapter form uses composition (aggregation) rather than multiple inheritance. That is, the Adapter inherits from the Target class as before, but maintains a reference to an Adaptee object, rather than inheriting from Adaptee. The methods of the Adapter, then, delegate requests to the appropriate methods of the Adaptee object. It is this object version of Adapter that has a structural similarity to the Bridge pattern. Figures 2 and 3 illustrate both versions of the Adapter pattern.


figure 2 - Structure of Adapter Pattern (class version ).


figure 3 - Structure of Adapter Pattern (object version ).

From figure 3, consider the Adapter and Adaptee objects. This is exactly the same structure found between the Abstraction and the Implementor in the Bridge pattern. The biggest difference is that the Client deals directly with a Target object which is the super-class of the Adapter, while in the Bridge pattern, the Client generally only uses the interface from the Abstraction. There are cases with a Bridge pattern where a refined Abstraction defines additional composite methods which use the Implementors basic operations, but the link to the Implementor is still through the general Abstraction.

Because these patterns are structurally similar, the difference between them is not always obvious. The difference lies in where and when the pattern is actually applied. As discussed earlier, the Bridge pattern is best used as a purely design pattern. That is, it is used and applied before a system has been implemented, and while the designer has control over the classes in the system. The Adapter pattern obviously needs an Adaptee, and this implies that it is used after initial system design, or as a strategy for designing a system which needs to interface to existing classes. Essentially, "An Adapter is meant to change the interface of an existing object." (Gamma, 1995, pg 149)


References

Abadi, M., L. Cardelli (1996). A Theory Of Objects. Springer-Verlag New York, Inc., New York.
Gamma, E., R. Helm, R. Johnson, J. Vlissides (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison Wesley Longman, Inc., Reading.
JavaSoft (1998) The PL&F Papers. http://java.sun.com/products/jfc/swingdoc-current/plaf_report.html
Jones, B. (1998) The Iterator Design Pattern: as implemented in Java's JDK 1.2. (iterator.html)
Zukowski, J. (1997) Java AWT Reference. O'Reilly & Associates, Inc., Sebastopol.

Graduate Course in Software Engineering