The
Strategy Pattern
Overview
This
chapter continues the discussion of design patterns with the Bridge
pattern. The Bridge pattern is quite a bit more complex than the
other patterns you have learned. It is also much more useful.
This
chapter
-
Provides
an example to help you derive the Bridge pattern. I go into great
detail to help you learn this pattern.
-
Presents
the key features of the pattern.
-
Presents
some observations on the Bridge pattern from my own practice.
Introducing
the Bridge Pattern
According
to the Gang of Four, the intention of the Bridge pattern is to
“decouple an abstraction from its implementation so that the two
can vary independently.”
I
remember exactly what my first thoughts were when I read this: Huh?
And
then: how come I understand every word in this sentence, but I have
no idea what it means?
I
knew that
-
Decouple
means to have things behave independently from each other or at
least explicitly state what the relationship is.
-
Abstraction
is how different things are related to each other conceptually.
And
I thought that implementation were the way to build the abstractions;
but I was confused about how I was supposed to separate abstractions
from the specific ways that implemented them.
It
turns out that much of my confusion was due to misunderstanding what
implementations meant. Implementations here mean the objects that the
abstract class and its derivations use to implement themselves (not
the derivations of the abstract class, which are called concrete
classes). To be honest, even if I had understood it properly, I am
not sure how much it would have helped. The concept expressed in this
sentence is just hard to understand at first.
If
you have also confused about the Bridge pattern at this point, that
is okay. If you understand the stated intent, you are that much
ahead.
It
is a challenging pattern to learn because it is so powerful.
The
Bridge pattern is one of the toughest patterns to understand in part
because it is so powerful and applies to so many situations. It also
goes against a common tendency to handle special cases with
inheritance. However, it is also an excellent example of following
two of the mandates of the design pattern community: “Find what
varies and encapsulate it” and “Favor aggregation over class
inheritance” (as you will see).
Learning
the Bridge Pattern: An Example
To
help you understand the thinking behind the Bridge pattern and what
it is trying to do, I will work through an example from scratch.
Starting with requirements, I will derive the pattern and then see
how to apply it.
Perhaps
this example will seem basic. But look at the concepts discussed in
this example and then try to think of situations that you have
encountered that are similar, having
-
Variations
in abstractions of a concept.
-
Variations
in how these concepts are implemented.
You
will see that this example has many similarities to the CAD/CAM
problem discussed earlier. Rather than give you all the requirements
up front, however, I am going to give them a little at a time, just
as they were given to me. You can’t always see the variations at
the beginning of the problem.
Button
line: during requirements definition, explore for variations early
and often!
Suppose
I have been given the task of writing a program that will draw
rectangles with either of two drawing programs. I have been told that
when I instantiate a rectangle, I will know whether I should use
drawing program 1 (DP1) or drawing program 2 (DP2).
The
rectangles are defined as two pairs of points, as represented in
Figure 10-1. The differences between the drawing programs are
summarized in Table 10-1.
Figure
10-1 positioning the rectangle.
Table
10-1 Different Drawing Programs
|
DP1
|
DP2
|
Used
to draw a line
|
draw_a_line
( x1, y1, x2, y2)
|
drawLine
( x1, x2, y1, y2)
|
Used
to draw a circle
|
draw_a_circle
( x, y, z)
|
drawCircle
(x, y, z)
|
Our
analysis specifies that we don’t want the code that draws the
rectangles to worry about what type of drawing program it should use.
It occurs to me that because the rectangles are told what drawing
program to use when instantiated, I can have two different kinds of
rectangle objects: one that uses DP1 and one uses DP2.
Each would have a draw method but would implement it differently.
Figure 10-2 shows this.
Figure
10-2 design for rectangles and drawing programs (DP1 and DP2).
By
having an abstract class Rectangle, I take advantage of the
fact that the only difference between the different types of
Rectangles is how they implement the drawLine
method. The V1Rectangle is implemented by having a reference
to a DP1 object and using that object’s draw_a_line
method. The V2Rectangle is implemented by having a reference to a DP2
object and using that object’s drawLine method. However, by
instantiating the right type of Rectangle, I no longer have to worry
about this difference.
Example
10-1 Java Code Fragments
abstract
public
class
Rectangle {
private
double
_x1 , _y1 , _x2 , _y2 ;
public
Rectangle (
double
x1 ,
double
x2 ,
double
y1 ,
double
y2 ) {
_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) ;
}
abstract
protected
void
drawLine (
double
x1 ,
double
y1 ,
double
x2 ,
double
y2 ) ;
}
Now
suppose that after completing this code, one of the inevitable
three (death, taxes, and changing requirements) comes my way. I
am asked to support another kind of shape---this time, a circle.
However, I am also given the mandate that the collection object does
not want to know the difference between Rectangles and
Circles.
It
occurs to me that I can just extend the approach I’ve already
started by adding another level to my class hierarchy. I only need to
add a new class, called Shape, from which I will derive the
Rectangle and Circle classes. This way, the Client
object can just refer to Shape objects without worrying about
what kind of Shape it has been given.
As
a beginning object-oriented analyst, it might seem natural to
implement these requirements using only inheritance. For example, I
could start out with something like Figure 10-2, and then, for each
kind of Shape, implement the shape with each drawing program,
deriving a version of DP1 and a version of DP2 for Rectangle and
deriving a version of DP1 and a version of DP2 one for Circle. I
would end up with Figure 10-3.
Figure
10-3 A straightforward approach: implementing two shapes and two
drawing programs.
I
implement the Circle class the same way that I implemented the
Rectangle
class.
However, this time, I implement
draw
by using
drawCircle
instead of
drawLine
.
Example
10-2 Java Code Fragments
abstract
class
Shape {
abstract
public
void
draw();
}
//
the only change to Rectangle is
abstract
class
Rectangle
extends
Shape {
//
//
V1Rectangle and V2Rectangle don't change
abstract
public
class
Circle
extends
Shape {
protected
double
_x, _y, _r;
public
Circle (
double
x ,
double
y,
double
r ) {
_x
=
x ; _y
=
y ; _r
=
r ;
}
public
void
draw () {
drawCircle () ;
}
abstract
protected
void
drawCircle ();
}
public
class
V1Circle
extends
Circle {
public
V1Circle (
double
x ,
double
y ,
double
r ) {
super
( x , y , r ) ;
}
protected
void
drawCircle () {
DP1.draw_a_circle ( _x , _y , _r );
}
}
public
class
V2Circle
extends
Circle {
public
V2Circle (
double
x ,
double
y ,
double
r ) {
super
( x , y , r ) ;
}
protected
void
drawCircle () {
DP2.drawCircle ( _x , _y , _r ) ;
}
}
To
understand this design, let's walk through an example. Consider what
the
draw
method of a
V1Rectangle
does.
-
Rectangle
's
draw
method
is the same as before (calling
drawLine
four times as needed).
-
drawLine
is implemented by calling
DP1
's
draw_a_line
.
In
action, this looks like Figure 10-4.
Figure
10-4 Sequence Diagram when have a V1Rectangle.
Even
though the class diagram makes it look as if there are many objects,
in reality I am only dealing with three objects (see Figure 10-5):
-
The
Client
using the
rectangle.
-
The
V1Rectangle
object.
-
The
DP1
drawing program.
When
the
Client
object
sends a message to the
V1Rectangle
object (called
myRectangle
)
to perform
draw
,
it calls
Rectangle
's
draw
method
resulting in Steps 2 through 9.
Reading
a Sequence Diagram
|
As
I discussed in Chapter 2, “The UML---The Unified Modeling
Language,” the diagram in Figure 10-4 is a special kind of
interaction diagram called a sequence diagram. It is a common
diagram in the UML. Its purpose is to show the interaction of
objects in the system.
-
Each
box at the top represents an object. It may be named or not.
-
If
an object has a name, is is given to the left of the colon.
-
The
class to which the object belongs is shown to the right of the
colon. Thus, the middle object is named myRectangle and
is an instance of V1Rectangle.
You
read the diagram from the top down. Each numbered statement is a
message sent from one object to either itself or to another
object.
-
The
sequence starts out with the unnamed Client object
calling the draw method of myRectangle.
-
This
method calls its own drawLine method four times
(shown in Steps 2, 4, 6, 8). note the arrow pointing back to the
myRectangle in the lifeline.
-
drawLine
calls DP1's draw_a_line. This is shown in
Step 3, 5, 7 and 9.
|
Figure
10-5 The Objects present.
Unfortunately,
this approach introduces new problems. Look back at Figure 10-3 and
pay attention to the third row of classes. Consider the following:
-
The
classes in the row represent the four specific types of
Shape
s
that I have.
-
What
happens if I get another drawing program---- that is, another
variation in implementation? I will have six different kinds of
Shape
s (two
Shape
concepts times three drawing programs).
-
Imagine
what happens if I then get another type of
Shape
,
another variation in concept, I will have nine different types of
Shape
s (three
Shape
concepts times three drawing programs).
The
class explosion problem arises because in this solution the
abstraction (the kinds of
Shape
s)
and the implementation (the drawing programs) are tightly coupled.
Each type of shape must know what type of drawing program it is
using. I need a way to separate the variations in abstraction from
the variations of implementation so that the number of classes only
grows linearly (see Figure 10-6).
This
is exactly the intent of the Bridge pattern: “[to] decouple an
abstraction from its implementation so that the two can vary
independently.”
Figure
10-6 The Bridge pattern separates variations in abstraction and
implementation.
Before
showing a solution and deriving the Bridge pattern, I want to mention
a few other problems (beyond the combinatorial explosion).
Looking
at Figure 10-3, ask yourself what else is poor about this design.
-
Does
there appear to be redundancy?
-
Would
you say things have strong cohesion or weak cohesion?
-
Are
things tightly or loosely coupled?
Would
you want to have to maintain this code?
The
Overuse of Inheritance
|
As
a beginning object-oriented analyst, I had a tendency to solve
the kind of problem I have seen here by using special cases,
taking advantage of inheritance. I loved the idea of inheritance
because it seemed new and powerful, I used it whenever I could.
This seems to be normal for many beginning analysts., but it is
naive: Given this new “hammer'” everything seems like a nail.
Unfortunately, many approaches to teaching object-oriented design
focus on the approach of handling variation through
specialization, deriving new classes from existing classes. With
an overfocus on the “is-ness” of objects, it has programmers
create objects in monolithic hierarchies that work reasonably
well at first but become more difficult to maintain as time goes
on (as discussed in Chapter 9, “The Strategy Pattern”).
As
I became an experienced object-oriented designer, I was still
stuck in this paradigm of designing based on inheritance---that
is, looking at the characteristics of my classes based on their
“is-ness” without regard for how complex the structures were
becoming.
Thinking
with design patterns eventually led me out of this mess. I
learned to think about objects in terms of their responsibilities
rather than in terms of their structure.
Experienced
object-oriented analysts have learned to use inheritance
selectively to realize its power. Using design patterns will help
you move along this learning curve more quickly. It involves a
transition from using a different specialization for each
variation (inheritance) to moving these variations into used or
owned objects (aggregation).
|
When
I first looked at these problems, I thought that part of the
difficulty might have been that I simply was using the wrong kind of
inheritance hierarchy. Therefore, I tried the alternative hierarchy
shown in Figure 10-7.
Figure
10-7 An alternative implementation.
I
still have the same four classes representing all of my possible
combinations. However, by first deriving versions for the different
drawing programs, I eliminated the redundancy between the DP1 and DP2
classes.
Unfortunately,
I am unable to eliminate the redundancy between the two types of
Rectangles and the two types of Circles, each pair of which has the
same draw method.
In
any event, the class explosion that was present before is still
present here.
The
sequence diagram for this solution is shown in Figure 10-8.
Figure
10-8 Sequence diagram for new approach.
Although
this may be an improvement over the original solution, it still has a
problem with scaling. It also still has some of the original cohesion
and coupling problems.
Button
line: I do not want to have to maintain this version either! There
must be a better way.
Look
For Alternatives in Initial Design
|
Although
my alternative design here was not significantly better than my
original design, it is worth pointing out that finding
alternatives to an original design is a good practice. Too many
developers take what they first come up with and go with that, I
am not endorsing an in-depth study of all possible alternatives
(another way of suffering “paralysis by analysis”). However,
stepping back and looking at how we can overcome the design
deficiencies in our original design is a great practice. In fact,
it was just this stepping back, a refusal to move forward with a
known, poor design, that led me to understanding the powerful
methods of using design patterns that this entire book is about.
|
An
Observation About Using Design Patterns
when
people begin to look at design patterns, they often focus on the
solutions the patterns offer. This seems reasonable because design
patterns are advertised as providing good solutions to the problems
at hand.
However,
this is starting at the wrong end. Before trying to apply a solution
to a problem, you should understand the problem. Taking an approach
that looks for where you can apply patterns tells you what to do but
not when to do or why to do it.
I
find it much more useful to focus on the context of the pattern---the
problem it is trying to solve. This lets me know the when and the
why. It is more consistent with the philosophy of Alexander's
patterns:”Each pattern describes a problem which occurs over and
over again in our environment, and then describes the core of the
solution to that problem...”
What
I have done so far in this chapter is a case in point. What is the
problem being solved by the Bridge pattern?
The
Bridge pattern is useful when you have an abstraction that has
different implementations. It allows the abstraction and the
implementation to vary independently of each other.
The
characteristics of the problem fit this nicely. I can know that I
ought to be using the Bridge pattern even though I do not know yet
how to implement it. Allowing for the abstraction to vary
independently from the implementation would mean I could add new
abstractions without changing my implementations and vice versa.
The
current solution does not allow for this independent variation. I can
see that it would be better if I could create an implementation that
would allow for this.
The
bottom line: It is very important to realize that, without even
knowing how to implement the Bridge pattern, you can determine that
it would be useful in this solution. You will find that this is
generally true of design patterns. That is, you can identify when to
apply them to your problem domain before knowing exactly how to
implement them.
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
Coplein on commonality and variability analysis.
Design
Patterns Are Solutions That Occur Again and Again
|
Design
patterns are solutions that have recurred in several problems and
have therefore proven themselves over time to be good solutions.
The approach I am taking in this book is to derive the pattern to
teach it so that you can understand its characteristics.
In
this case, I know the pattern I want to derive---the Bridge
pattern----because I was shown it by the Gang of Four and have
seen how it works in my own problem domains. It is important to
note that patterns are not really derived. By definition, they
must be recurring---having been demonstrated in at least three
independent cases----to be considered patterns. What I men by
“derive” is that we will go through a design process where
you create the pattern as if you did not know it. This is to
illustrate some key principles and useful strategies. It also
demonstrates that it is at least equality important to know these
principles as it is to know patterns because the principles
always apply, whereas the patterns are found only in certain
circumstances.
|
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 large 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 tis own abstract class and then
see how the abstract classes relate to each other.
Reviewing
Encapsulation
|
Most
object-oriented developers learned that “encapsulation” is
data hiding. Unfortunately, this is a very limiting definition.
True, encapsulation does hide data, but it can be used in many
other ways. If you look back at Figure 7-2, you will see
encapsulation operates at many levels. Of course, it works at
hiding data for each of the particular Shapes. However,
notice that the Client object is not aware of the
particular kinds of Shapes. That is, the Client
object has no idea that the Shapes it is dealing with are
Rectangles or(
注:原文是
and
,我认为
or
更合适。
By
xiaosilent) Circles. Thus, the concrete classes that
Client deals with are hidden (or encapsulated) from
Client. This is the kind of encapsulation that the Gang of
Four is talking about when they say “Find what varies and
encapsulate it.” They are finding what varies and encapsulating
it “behind” an abstract class (see Chapter 6, “Expanding
Our Horizons”).
Figure 7-2 Points,Lines,and Squares are types of Shapes.
|
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 a
program that is based on
DP2
(
V2Drawing
),
respectively. I show this in Figure 10-10.
Figure 10-10 Represent the Variations.
At
his 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 Figure 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 type 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
Shape
s
(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
Shape
s
use
Drawing
objects
to draw themselves?
Shape
s
wouldn't need to know what type of
Drawing
object they used because I could have
Shape
s
refer to the
Drawing
class.
Shape
s 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.
One
Rule, One Place
|
A
very important implementation strategy to follow is to have only
one place where you implement a rule. In other words, if you have
a rule how to do things, only implement that once. This typically
results in code with a greater number of smaller methods. The
extra cost is minimal, but it eliminates duplication and often
prevents many future problems. Duplication is bad not only
because of the extra work in typing things multiple times, but
also because of the likelihood of something changing in the
future and then forgetting to change it in all of the required
places.
Although
the draw method or Rectangle could directly call the drawLine
method of whatever Drawing object the Shape has, I can improve
the code by continuing to follow the one rule, one place strategy
and have a drawLine method in Shape that calls the drawLine
method of its Drawing object.
I
am not a purist (at least not in most things), bu if there is one
place where I think it is important to always follow a rule, it
is here. In the following example, I have a drawLine method in
Shape because that describes my rule of drawing a line with
Drawing. I do the same with drawCircle for circles. By following
this strategy, I prepare myself for other derived objects that
might need to draw lines and circles.
|
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 {
public
static
void
main (){
Shape myShapes[];
Factory myFactory
=
new
Factory();
//
get rectangles from some other source
myShapes
=
myFactory.getShapes();
for
(Shape shape : myShapes){
shape.draw();
}
}
}
abstract
public
class
Shape{
protected
Drawing myDrawing;
abstract
public
void
draw();
Shape (Drawing drawing){
myDrawing
=
drawing;
}
protected
vodi 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);
}
}
public
abstract
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){
DP2.drawLine(x1,x2,y1,y2);
}
public
void
drawCircle(
double
x,
double
y,
double
r){
DP2.drawCircle(x,y,r);
}
}
The
Bridge Pattern in Retrospect
Now
that you've seen how the Bridge pattern woks, it is worth looking at
it from a more conceptual point of view. As shown in Figure 10-13,
the pattern has an abstraction part (with its derivations) and an
implementation part. When designing with the Bridge pattern, it is
useful to keep these two parts in mind. The implementation's
interface should be designed considering the different derivations of
the abstract class that it will have to support. Note that a designer
shouldn't necessarily define an interface that will account for all
conceivable derivations of the abstract class (yet another possible
route to paralysis by analysis). Only those derivations that actually
are being built need to be supported. Time and time again, the
authors have seen that the mere consideration of flexibility at this
point often greatly improve a design.
Note:
In C++, the Bridge pattern's implementation must be implemented with
an abstract class defining the public interface. In C# and Java,
either an abstract class or an interface can be used. The choice
depends on whether implementations share common traits that abstract
class can take advantage of.
Filed
Notes: Using the Bridge Pattern
Print
drivers are perhaps the class example of the Bridge. They are also
the easiest to see when to apply the Bridge. The real power of the
Bridge Pattern, in my mind, however, is that is helps me see when to
abstract out the implementations that are present in my problem
domain. In other words, sometimes I'll have an entity X that uses a
system S and an entity Y that uses system T. I may think that X
always and only comes with S and Y always and only comes with T and
link them (couple) them together. The Bridge reminds me that I may be
better off abstracting out the differences between S and T (the
implementations of X and Y) and allowing X and Y to use either S and
T. In other words, the Bridge is most useful when I might not have
decoupled my abstraction form my implementation unless I consider
whether the Bridge pattern applied.
Note
that the solution presented in Figure 10-12 and 10-13 integrates the
Adapter pattern with the Bridge pattern. I do this because I was
given the drawing programs that I must use. These drawing programs
have preexisting interfaces with which I must work. I must use the
Adapter to adapt them so that they can be handled in the same way.
Although
it is very common to see the Adapter pattern used with the Bridge
pattern, the Adapter pattern is not part of the Bridge pattern.
When
two or more patterns are tightly integrated (like my Bridge and
Adapter), the result is called a compound design pattern. It is now
possible to talk about patterns of patterns!
Another
thing to notice is that the objects representing the abstraction (the
Shapes) were given their implementation while being instantiated.
This is not an inherent part of the pattern, but it is very common.
The
Bridge Pattern: Key Feature
|
Intent
|
Decouple
a set of implementations from the set of objects using them.
|
Problem
|
The
derivations of an abstract class must use multiple
implementations without causing an explosion in the number of
classes.
|
Solution
|
Define
an interface for all implementations to use and have the
derivations of the abstract class use that.
|
Participants
and collaborators
|
Abstraction
defines the interface for the objects being implemented.
Implementor defines the interface for the specific implementation
classes. Classes derived from Abstraction use classes derived
from Implementor without knowing which particular
ConcreteImplementor is in use.
|
Consequence
|
The
decoupling of the implementations from the objects that use them
increases extensibility. Client objects are not aware of
implementation issues.
|
Implementation
|
-
Encapsulate
the implementations in an abstract class.
-
Contain
a handle to it in the base class of the abstraction being
implemented. Note: In Java, you can use interfaces instead of an
abstract class for the implementation.
|
Figure
10-15 Generic structure of the Bridge pattern.
|
Now
that you understand the Bridge pattern, it is worth reviewing the
Gang of Four's implementation section in their description of the
pattern. They discuss different issues relating to how the
abstraction creates and / or uses the implementation.
Sometimes
when using the Bridge pattern, I will share the implementation
objects across several abstraction objects.
-
In
C# and Java, this is no problem; when all the abstraction objects go
away, the garbage collector will realize that the implementation
objects are no longer needed and will clean them up.
-
In
C++, I must somehow manage the implementation objects. There are
many ways to do this; keeping a reference counter or even using the
Singleton pattern are possibilities. It is nice, however, not to
have to consider this effort. This illustrates another advantage of
automatic garbage collection.
Although
the solution I developed with the Bridge pattern is far superior to
the original solution, it is not perfect. One way of measuring the
quality of a design is to see how well it handles variation. Handling
a new implementation is very easy with a Bridge pattern in place. The
programmer just needs to define a new concrete implementation class
and implement it. Nothing else changes.
However,
things may not go so smoothly if I get a new concrete example of the
abstraction. I may get a new kind of Shape that can be
implemented with the implementations already in the design. However,
I may also get a new kind of Shape that requires a new drawing
function. For example, I may have to implement an eclipse. The
current Drawing class does not have the proper method to do
eclipses. In this case, I have to modify the implementations.
However, even if this occurs, at least I have a well-defined process
for making changes: Modify the interface of the Drawing class
or interface and then modify each Drawing derivative
accordingly. This process localizes the impact of the change and
lowers the risk of an unwanted side effect.
I
am left with a fine solution, even if it is not “perfect”. And
the Bridge pattern has given me a handle on the problem if I need to
consider a more general implementation. Design patterns help me think
more abstractly and more generally about my solutions. Whether I want
to implement this more general solution is up to me; the pattern
doesn't mandate that.
Bottom
line: Patterns do not always give perfect solutions. Because patterns
represent the collective experience of many designers over the years,
however, they are often better than the solutions you or I might come
up with on our own in the often limited time we have.
Follow
one rule,one place to help with refactoring.
In
the real world, I do not always start out with multiple
implementations. Sometimes, I know that new ones are possible, but
they show up unexpectedly. One approach is to prepare for multiple
implementations by always using abstractions. You get a very generic
application.
But
I do not recommend this approach. It leads to an unnecessary increase
in the number of classes you have. It is important to write code in
such a way that when multiple implementations do occur (which they
often will), it is not difficult to modify the code to incorporate
the Bridge pattern. Modifying code to improve its structure without
adding function is called refactoring. As defined by Martin Fowler,
“Refactoring is the process of changing a software system in such a
way that it does not alter the external behavior of the code yet
improves its internal structure.”.
When
designing code, I was always attending to the possibility of
refactoring by following the one rule, one place mandate. The
drawLine method was a good example of this. Although the place
the code was actually implemented varied, moving it around was fairly
easy.
Refactoring
|
Refactoring
is commonly used in object-oriented design. However, it is not
strictly an object-oriented thing.... it is modifying code to
improve its structure without adding function.
|
While
deriving the pattern, I took the two variations present (shapes and
drawing programs) and encapsulated each in its own abstraction class.
That is, the variations of shapes are encapsulated in the Shape
class, the variations of drawing programs are encapsulated in the
Drawing class.
Stepping
back and looking at these two polymorphic structures. I should ask
myself, “What do these abstract classes represent?” For the
shapes, it is pretty evident that the class represents different
kinds of shapes. The Drawing abstract class represents how I
will implement the Shapes. The pattern is about the
relationship between these different abstractions. Thus, in the case
where I described how new requirements for the Drawing class
may arise (say, if I need to implement eclipses) there is a clear
relationship between the classes that tells me how to implement it.
Summary
While
reviewing the Bridge pattern, I looked at a problem where there were
two variations in the problem domain---shapes and drawing programs.
In the problem domain, each of these varied. The challenge came when
trying to implement a solution based on all the special cases that
existed. The initial solution, which naively used inheritance too
much, resulted in a redundant design that had tight coupling and weak
cohesion, and was thus difficult to maintain.
You
learned the Bridge pattern by following the basic strategies for
dealing with variation:
-
find
what varies and encapsulate it.
-
Favor
aggregation over inheritance.
Finding
what varies is always a good step in learning about the problem
domain. In the drawing program example, I had one set of variations
using another set of variations. This indicates that the Bridge
pattern will probably be useful.
In
general, you should identify which patterns to consider by matching
them with the characteristics and behaviors in the problem domain. By
understanding the whys and whats of the patterns in
your repertoire, you can be more effective in picking the ones that
will help you. You can select patterns to consider before deciding
how the pattern's implementation will be done.
Consideration
vs. Use
|
I
used the word consider rather than use in the prior
paragraph deliberately. Actually, you should “use” patterns
by “considering” the issues they imply and the body of
knowledge about them. Unfortunately, when people hear the word
use regarding a pattern, they tend to think about “using
the implementation” of the pattern. The word consider
helps people realize that they need to use the pattern as a
guideline, a list of considerations.
|
If
you use the Bridge pattern, the design and implementation are more
robust and better able to handle changes in the future.
Although
I focused on the Bridge pattern during the chapter, it is worth
pointing out several object-oriented principles that are used in the
Bridge pattern.
Content
|
Discussion
|
Objects
are responsible for themselves
|
I
had different kinds of Shapes, but all drew themselves
(via the draw method). The Drawing classes
were responsible for drawing elements of objects.
|
Abstract
class
|
I
used abstract classes to represent the concepts. I actually had
rectangles and circles in the problem domain. The concept “shape”
is something that lives strictly in our head, a device to bind
the two concepts together; therefore , I represent it in the
Shape class as an abstract class. Shape will never
get instantiated because it never exists in the problem domain
(only Rectangles
and Circles do). The same thing is true with drawing
programs.
|
Encapsulation
via an abstract class
|
I
have two examples of encapsulation through the used of an
abstract class in this problem:
-
A
client dealing with the Bridge pattern will have only a
derivation of Shape visible to it. However, the client
will not know what type of Shape it has. (it will be just
a Shape to the client). Thus, I have encapsulated this
information. The advantage of this is if a new type of Shape
is needed in the future, it does not affect the client object.
-
The
Drawing class hides the different drawing derivations
from the Shapes.
|
One
rule, one place
|
The
abstract class often has the methods that actually use the
implementation objects. The subclasses of the abstract class call
these methods. This allows for easier modification if needed, as
allows for a good starting point even before implementing the
entire pattern.
|
Testability
|
Imagine
writing tests for the shapes and drawing programs with both our
original solution and our later solution. For example, suppose
you have N shapes and M implementations. The first solution would
require N*M tests. The second solution only requires M+N tests:
First test the M implementations, and then test the N shapes with
arbitrarily chosen implementations (because all of the shapes
work with all of the implementations in exactly the same way).
|
posted on 2006-11-12 12:27
xiaosilent 阅读(1717)
评论(3) 编辑 收藏 所属分类:
设计模式