Learning the Bridge Pattern: Deriving It
Now that you have been through the problem, we are in a position to derive the Bridge pattern together. Doing the work to derive the pattern will help you to understand more deeply what this complex and powerful pattern does.
Let’s apply some of the basic strategies for good object-oriented design and see how they help to develop a solution that is very much like the Bridge pattern. To do this, I will be using the work of Jim Coplein4 on commonality and variability analysis.
It is almost axiomatic with object-oriented design methods that the designer is supposed to look at the problem domain, identify the nouns present, and create objects representing them. Then the designer finds the verbs relating to those nouns (that is, their actions) and implements them by adding methods to the objects. This process of focusing on nouns and verbs typically leads to larger class hierarchies than we would like. I suggest that using commonality and variability analysis as a primary tool in creating objects is a better approach than looking at just nouns and verbs. (Actually, I believe this is a restatement of Jim Coplein’s work.)
There are two basic strategies to follow in creating designs to deal with the variations:
- Find what varies and encapsulate it.
- Favor aggregation over inheritance.
In the past, developers often relied on extensive inheritance trees to coordinate these variations. However, the second strategy says to try aggregation when possible. The intent of this is to be able to contain the variations in independent classes, thereby allowing for future variations without affecting the code. One way to do this is to have each variation contained in its own abstract class and then see how the abstract classes relate to each other.
Follow this process for the rectangle-drawing problem.
First identify what it is that is varying. In this case, it is different types of shapes and different types of drawing programs. The common concepts are therefore shapes and drawing programs. I represent this in Figure 10-9. (Note that the class names are shown in italics because the classes are abstract.)
Figure 10-9 What is varying?
At this point, I intend Shape to encapsulate the concept of the types of shapes that I have. Shapes are responsible for knowing how to draw themselves. Drawing objects, on the other hand, are responsible for drawing lines and circles. I represent these responsibilities by defining methods in the classes.
The next step is to represent the specific variations that are present. For Shape, I have rectangles and circles. For drawing programs, I will have a program that is based on DP1 (V1Drawing) and one based on DP2 (V2Drawing), respectively. I show this in Figure 10-10.
Figure 10-10 Represent the variations.
At this point, the diagram is simply notional. I know that V1Drawing will use DP1 and V2Drawing will use DP2, but I have not said how. I have simply captured the concepts of the problem domain (shapes and drawing programs) and have shown the variations present.
Given these two sets of classes, I need to ask how they will relate to one another. I do not want to come up with a new set of classes based on an inheritance tree because I know what happens if I do that. (Look at Figures 10-3 and 10-7 to refresh your memory.) Instead, I want to see whether I can relate these classes by having one use the other (that is, follow the mandate to favor aggregation over inheritance). The question is, which class uses the other?
Consider these two possibilities: either Shape uses the Drawing programs or the Drawing programs use Shape.
Consider the latter case first. If drawing programs could draw shapes directly, they would have to know some things about shapes in general: what they are, what they look like. But this violates a fundamental principle of objects: An object should only be responsible for itself.
It also violates encapsulation. Drawing objects would have to know specific information about Shapes (that is, the kind of Shape) in order to draw them. The objects are not really responsible for their own behaviors.
Now consider the first case. What if I have Shapes use Drawing objects to draw themselves? Shapes wouldn’t need to know what type of Drawing object they used because I could have Shapes refer to the Drawing class. Shapes also would be responsible for controlling the drawing.
This looks better to me. Figure 10-11 shows this solution.
Figure 10-11 Tie the classes together.
In this design, Shape uses Drawing to manifest its behavior. I left out the details of V1Drawing using the DP1 program and V2Drawing using the DP2 program. In Figure 10-12, I add this as well as the protected methods drawLine and drawCircle (in Shape), which calls Drawing’s drawLine and drawCircle respectively.
Figure 10-12 Expanding the design.
Figure 10-13 illustrates the separation of the Shape abstraction from the Drawing implementation.
Figure 10-13 Class diagram illustrating separation of abstraction and implementation.
From a method point of view, this looks fairly similar to the inheritance-based implementation (such as shown in Figure 10-3). The biggest difference is that the methods are now located in different classes.
I said at the beginning of this chapter that my confusion over the Bridge pattern was due to my misunderstanding of the term implementation. I thought that implementation referred to how I implemented a particular abstraction.
The Bridge pattern let me see that viewing the implementation as something outside of my objects, something that is used by the objects, gives me much greater freedom by hiding the variations in implementation from my calling program. By designing my objects this way, I also noticed how I was containing variations in separate class hierarchies. The hierarchy on the left side of Figure 10-13 contains the variations in my abstractions. The hierarchy on the right side of Figure 10-13 contains the variations in how I will implement those abstractions. This is consistent with the new paradigm for creating objects (using commonality/variability analysis) that I mentioned earlier.
It is easiest to visualize this when you remember that there are only three objects to deal with at any one time, even though there are several classes (see Figure 10-14).
Figure 10-14 There are only three objects at a time.
Example 10-3 shows a reasonably complete Java code example.
Example 10-3 Java Code Fragments
public class Client { static public void main () { Shape myShapes[]; Factory myFactory= new Factory(); // get rectangles from some other source myShapes= myFactory.getShapes(); for (int i= 0; i < myShapes.length; i++) { myShapes[i].draw(); } } } abstract public class Shape { protected Drawing myDrawing; abstract public void draw(); Shape (Drawing drawing) { myDrawing= drawing; } protected void drawLine ( double x1,double y1, double x2,double y2) { myDrawing.drawLine(x1,y1,x2,y2); } protected void drawCircle ( double x,double y,double r) { myDrawing.drawCircle(x,y,r); } } public class Rectangle extends Shape { private double _x1, _y1, _x2, _y2; public Rectangle (Drawing dp, double x1, double y1, double x2, double y2) { super( dp); x1= x1; _y1= y1; _x2= x2; _y2= y2; } public void draw() { drawLine( _x1, _y1, _x2, _y1); drawLine( _x2, _y1, _x2, _y2); drawLine( _x2, _y2, _x1, _y2); drawLine( _x1, _y2, _x1, _y1); } protected void drawLine(double x1, double y1, double x2, double y2) { myDrawing.drawLine( x1, y1, x2, y2); } } public class Circle extends Shape { private double _x, _y, _r; public Circle (Drawing dp, double x, double y, double r) { super(dp); x= x; _y= y; _r= r; } public void draw() { myDrawing.drawCircle( _x, _y, _r); } } abstract public class Drawing { abstract public void drawLine(double x1, double y1, double x2, double y2); abstract public void drawCircle( double x, double y, double r); } public class V1Drawing extends Drawing { public void drawLine ( double x1,double y1, double x2,double y2) { DP1.draw_a_line(x1,y1,x2,y2); } public void drawCircle ( double x,double y,double r) { DP1.draw_a_circle(x,y,r); } } public class V2Drawing extends Drawing { public void drawLine ( double x1,double y1, double x2,double y2) { // arguments are different in DP2 // and must be rearranged DP2.drawLine(x1,x2,y1,y2); } public void drawCircle ( double x, double y,double r) { DP2.drawCircle(x,y,r); } }