桥式
(The
Bridge Pattern)
概述
这一章我们将会通过
Bridge
模式来继续我们设计模式的学习。
Bridge
模式比我们以前所讲到的
Facade
、
Adapter
、
Strategy
这些模式都更有用,但也更复杂。
这一章
-
我将给出一个实际的例子,并进行细致的讲解,来帮助你学习
Bridge
模式。
-
我还会把
Bridge
模式的功能,实现方法之类的很关键性的东西通过一个表列出来。
-
我还会加入一点我自己对
Bridge
模式的看法。
Bridge
模式简介
按照
GoF
的说法,我们使用
Bridge
模式的目的是“把抽象从它的具体实现中分离出来,以使二者可以独立变化”。
我还记得我第一次看到这句话时是多么的惊讶,(原以为会是多么高级,原来就是把实现和抽象分离而已,貌似作者当时很失望)。我很清楚这句话里每一个单词的具体含义,然而,我想我还是没有理解到这句话。
我知道:
-
“分离”就是说使事物的行为和其他事物无关,或者至少说要把他们之间的关系弄明白。
-
“抽象”是指不同事物在某种概念上的一致性,或者说是相关性。
我以为,要建立抽象的方法就是“实现”,当我看到
GoF
说要把抽象从实现中分离出来的时候,我就不能不困惑了。
看起来,我的困惑都是因为我误解了实现的含义。这里的“实现”是指那些抽象类以及其派生类用来实现它们自身的对象。老实说,如果一开始我就能明白到这一点,那肯定要省事很多,然而这个句子的确很难让人一下子就明白其真正含义。
如果你现在和我当时一样对
Bridge
模式仍然充满了困惑,没关系,相信通过后面的讲解,你会对
Bridge
模式有一个非常清楚的认识;如果你现在就已经很清楚
Bridge
模式的目的了的话,那么后面你将会很轻松。
Bridge
模式可以算是众多晦涩的模式中的一个,因为它功能强大,应用范围又很广,还因为它的处理方式和通常用继承/派生方式处理问题有点背道而驰的感觉。然而,它仍然是一个很好的例子,一个遵循设计模式两大要求的例子。它们是:“找到什么是变化的并封装它”
和 “尽量考虑使用聚合而不是继承/派生”,随后,我们将会看到
Bridge
模式是如何符合这两点的。
通过例子学习
Bridge
模式
我将通过从头开始讲解一个例子的方式来帮助你理解
Bridge
模式的思想和它能做什么。我将从最初的系统要求开始讲解这个例子,然后逐步引入
Bridge
模式并把它应用到这个例子当中。
也许这个例子看起来过于基础。但还是请看看本例所讨论的一些概念,再想想你所遇到过的和下面相似的情形:
-
被“抽象”的概念在变化。
-
抽象的实现方法也在变化。
你将会发现这和我们先前讨论过的
CAD/CAM
问题有些相似。我将会逐渐增加这个例子里的需求,就像我们实际遇到的那样,而不是一次就把所有的需求都提出来。你总不可能在问题一开始就预见所有可能的变化吧。
这样看来,我们的底线就是:在系统的需求没有定型之前,尽可能多尽可能早的预见可能出现的变化。
假设,我接到一个任务,可以用两个不同的程序来画矩形。而且要求,在初始化一个矩形的时候,我要知道我是使用第一个绘图程序
(DP!)
还是第二个绘图程序
(DP2)
。
如图
10-1
所示,通过对角线上的两个点来定义一个矩形。这两个绘图程序的不同之处也已经总结在表
10-1
里。
图
10-1
定位一个矩形
表
10-1
两个绘图程序的不同之处
-
|
DP1
|
DP2
|
画线
|
draw_a_line(
x1 , y1 , x2 , y2 )
|
drawline(
x1, x2, y1, y2)
|
画圆
|
draw_a_circle
(x , y , r )
|
drawcircle(
x, y, r)
|
根据分析,我不希望绘制矩形的代码知道具体是使用的
DP1
还是
DP2
。但是,前面又有要求说在矩形初始化的时候就要明确使用的到底是
DP1
还是
DP2
,这样的话,我可以通过用两种矩形来解决:一种用
DP1
,一种用
DP2
。它们都有一个
draw()
方法,但是各自的实现方式不同。如图
10-2
所示。
图
10-2
Design for rectangles and drawing programs (DP1 and DP2).
由于这两种矩形的唯一不同之处就在于他们使用的绘图程序,所以,通过抽象类
Rectangle
,让这两种矩形分别实现
drawLine()
方法就可以了。
V1Rectangle
通过包含一个
DP1
的对象,并调用该对象的
draw_a_line()
方法来实现,
V2Rectangle
则通过包含一个
DP2
的对象,并调用该对象的
drawLine()
方法来实现。这样,在初始化
Rectangle
的时候,我就不必再理会这些不同了。
例
10-1
Java
代码片段
abstract
public
class
Rectangle {
private
double
_x1, _y1, _x2, _y2;
public
Rectangle (
double
x1,
double
y1,
double
x2,
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);
}
现在我们已经完成了上面的代码以及
V1Rectangle
和
V2Rectangle
的代码。但是,我又被要求程序能够支持另一种除了矩形以外的形状,圆。而且,同样要求,保存这些“圆”的容器不需要知道它保存的到底是圆
(Circle)
还是矩形
(Rectangle)
。
看起来,我可以在我现有的实现基础上扩展一下就可以了。我加入一个新的
Shape
类,让
Rectangle
和
Circle
都去继承它。这样,客户端对象可以只保存
Shape
类的对象,而不用起考虑到底具体是什么类型的
Shape(Rectangle
或是
Circle)
。
在一个面向对象分析的新手的眼里,用继承来实现这些要求,看起来是很自然的事情。例如,我可以像图
10-2
所演示的那样做。通过继承再继承,每个形状都分别派生出使用
DP1
和
DP2
的类,最后,得到图
10-3
这样的结果。
Figure
10-3 A straightforward approach: implementing two shapes and two
drawing programs.
我用实现
Rectangle
的方法来实现
Circle
。然而,这次,实现
draw()
这个方法的时候是用
drawCircle()
而不再是
Rectangle
里的
drawLine()
。
例
10-2
Java
代码片段
public
abstract
class
Shape {
abstract
public
void
draw ();
}
//
the only change to Rectangle is
abstract
class
Rectangle
extends
Shape {
}
//
V1Rectangle and V2Rectangle don't change
public
abstract
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
V2Cricle (
double
x,
double
y,
double
r){
super
( x, y, r);
}
protected
void
drawCircle (){
DP2.drawCircle( _x, _y, _r);
}
}
我们来仔细看看这个例子。看看
draw()
和
V1Rectangle
都做了些什么。
-
Rectangle
的
draw()
方法和先前的一样
(
都根据需要调用
drawLine()
四次
)
。
-
drawLine()
通过调用
DP1
的
draw_a_line()
来实现。
其执行的步骤如图
10-4
所示。
Figure
10-4 Sequence Diagram when have a V1Rectangle.
虽然,在类图上看起来,程序里有很多的对象,但是实际上,我只需要处理三个对象。如图
10-5
所示。
Figure
10-5 The Objects present.
-
Client
对象使用到矩形。
-
V1Rectangle
对象。
-
绘图程序
DP1
。
当
client
对象传递一个消息到
V1Rectangle
的对象
(
也就是
myRectangle)
,执行其中的
draw()
方法的时候,它通过图中的步骤
2
到步骤
9
来完成调用
draw()
方法的全过程。
不幸的是,这种方法又引起了新的问题。回过头来看看图
10-3
,仔细看看第三行的类,想想下面的问题:
-
这一行的类代表了四种不同类型的
Shape
。
-
如果我现在又有了一个新的绘图程序,也就是说,在实现上又有了一个新的变化。这样,我是不是该有
6
个类了?
(2
个
Shape
再乘上
3
个绘图程序。
)
-
再来,如果在前面基础上,我现在又要实现一个新的形状,比方说,再来个椭圆,完了,现在,我该有
3
*
3
=
9
个类了。
类的数量就和滚雪球一样,越来越多。这种情况的出现是因为抽象
(
各种各样的
Shape)
和实现
(
多个绘图程序
)
他们很紧密地耦合在一起!每种确切的形状都要知道它所使用的绘图程序的具体类型。看样子,要避免这种类在数量上的爆炸式的增长,我得需要一个能够把抽象和实现分离看来的方法。让他们可以独立的发生变化。这样,类的数量就可以呈现出线性的增长,而不是近指数形式的增长了。如图
10-6
所示。
Figure
10-6 The Bridge pattern separate variations in abstraction and
implementation.
在具体讲解处理方法和
Bridge
模式之前,我还想讲点其他的问题。
让我们再次回过头去看看图
10-3
,自问一下:这个设计的缺点在哪里?
-
这个设计看起来是不是有点累赘?
-
设计里的对象间是结合是否紧密呢?
-
对象是不是很紧密的耦合在一起呢?
这样的代码,你愿意去维护吗?
-
滥用继承
|
作为一个新手,我曾经趋向于用“特殊化”的方式来解决问题,也就是利用继承。我曾经很喜欢使用继承这种方式,因为它是个新颖有功能强大的方法。我总是尽可能地使用继承,这样的情况在新手里经常出现,但是,这种做法是很天真的:有了“继承”这把“榔头”,见到啥玩意都象颗钉子,一榔头就下去了。然而,令人感到遗憾的是,在学习面向对象设计的过程中,人们总是受着“特殊化”的熏陶,总是被教导着通过特殊化来处理一切变化,无休止的从基类派生出新类。由于过度地注重“是什么”,而使得很多设计都是建立在一些很固化的继承链上的,这些设计在一开始的时候可以很好的运行,但是,随着时间的推移,这些设计将会变得非常难以维护
(
正如在第九章,”策略模式”里面所讲到的那样
)
。
即使是在我经验已经比较丰富的时候,我还是会把这种基于继承的方法过度的应用到设计当中,只管类”是什么”,而并没有意识到我这么做,将会使整个设计在结构上变得多么的复杂!
而,是设计模式的思维方法使我摆脱了这些。我学会用我的对象具有什么要嗯的职能的方式去思考问题,而不再是单从对象的结构上去思考问题。
真正有经验的面向对象的设计师,他们都会很有选择性地使用继承。而学习设计模式,将会使你更快的学到这些。它将带给你一个从把每个变体特殊化到学会使用聚合的转变。
|
最初,我看到这些问题的出现,我曾以为这一切都是因为我的继承方式使用得不对而引起的。于是,我试着改进了先前的继承链,把它变得像图
10-7
那样。
Figure
10-7 An alternative implementation.
我还是有四个类,但是,通过首先就按绘图程序的不同而区分类的方式,减少了很多
DP1
和
DP2
之间的冗余内容。
然而,我还是不能排除两种
Rectangle
和两种
Circle
之间的冗余,每种
Rectangle
和每种的
Circle
都有相同的
draw()
方法。
不管怎么说,先前的那种类数量上的爆炸式的增长,在这里又出现了。
新的设计方案的执行过程如图
10-8
所示。
Figure
10-8 Sequence diagram for new approach.
虽然这个方案比最初的那个要好那么一点点,但是还是存在类数量将会过大的问题,而且对象间的耦合也存在问题。
也就是说,我还是不想维护这样的代码,一定还会有更好的方案的!
-
替代原有设计方案
|
虽然我使用的替代方案并比我的原始方案优秀多少,但是,我还是要指出来的就是,在原有方案上寻找替代方案是很重要的。太多的程序员都是抱着最开始的东西不放。当然了,我不是说要去深入地考虑所有可替代的方案
(
真是这样的话,就永远停留在分析上了
)
。然而,在遇到困难的时候,回过头去看看是否可以在原始方案里就能够解决这些困难。这,是很重要的。事实上,我们只是退后一步而已,明知道原来的设计方案存在这样那样的问题,退回来看看有没有什么办法可以补救。正是这样,使我领悟到设计模式的魅力。
|
关于使用设计模式的一些观察
当人们开始关注设计模式的时候,通常他们只关心的是设计模式所提供的解决方法。这看起来不无道理,因为设计模式不是一直都被说成是能够提供一些好的办法来解决我们手头上的难题么?
然而,这样的看法是站在我们遇到一个难题的基础上提出来的。实际上,在运用设计模式到设计中之间,你应该首先明白你的设计将会做什么。正确的做法应该是考虑一下你可以在设计中的哪些地方用到设计模式,这将告诉你要做什么,而不是什么时候我要用设计模式了,也不是为什么我要去用设计模式。
我发现,关心一下一个设计模式将会处理的问题的方式很有用。这样,我就会知道什么时候,为什么要使用到这个模式。
当你的一个抽象有很多种实现方法的时候,你就会发现
Bridge
模式的妙用。它可以让你的抽象和实现各自独立的发生变化。
我们一直在讨论的这个绘图的问题就很符合
Bridge
模式的应用场合。因此,我知道,我应该在这个设计里用到
Bridge
模式了,虽然,到目前位置,我还不知道我该怎么做。让抽象和实现可以实现独立的变化意味着我可以任意的添加新的抽象而不用去修改原有的具体实现。
而当前的方案还不允许这样的独立变化。
有一点很重要,就是即使在不知道具体怎么实现
Bridge
模式的情况下,你还是可以肯定
Bridge
模式在这个设计中是很有用的。以后你就会发现,这种情况在设计模式领域里很常见,那就是,你可以确定你应该把某个模式应用到你的设计当中,尽管,你还并不知道该怎么去实现这个模式。
Bridge
模式的引出
相信你已经很清楚我们所要处理的问题了,那么,现在我们就用
Bridge
模式来解决我们的问题。这个过程将帮助你更加深入地理解这个复杂而强大的
Bridge
模式。
我们还是要遵循那两个“原则”:
-
找到什么是变化的并封装它。
-
尽量使用聚合而不是继承。
以前,开发人员总是依靠大量的继承来处理程序中的变化,而第二个“原则”告诉我要尽可能的使用聚合。使用聚合的目的就是让程序可以在单独的类里面包含它自己的变化,这样,今后有新变化出现的时候就可以通过添加新的类来包含这个变化,而不会对原先的类造成影响。要达到这个目的,我们就需要把所有的变化都包含到它们共同的抽象类里面,然后,我们再去考虑抽象类之间的联系,而不再考虑抽象类的具体实现。
-
再说封装
|
很多面向对象程序设计的开发人员都知道“封装”就是数据隐藏。其实,这是一个很狭隘的定义!实际上,“封装”除了数据隐藏以外,还有很多其他的用法。如果你再回过头去看看图
7-2
,你会在里面发现“封装”可以操纵很多个层次。当然,那里,封装的目的就是为每个
Shape
做数据隐藏。然而,要注意的是,
Client
对象并不知道它所处理的
Shape
的具体类型
(
到底是
Rectangle
还是
Circle)
。因此,那个具体被
Client
所处理的类就通过
Shape
而实现了隐藏
(
也就是封装
)
,这也就是
GoF
在说到“找到什么是变化的并封装它”的时候所指的“封装”,让抽象类
(Shape)
出面去和
Client
对象交互,具体的
Rectangle
、
Circle
这些类都“躲”在了
Shape
类的后面,
Client
只知道自己在和
Shape
交互,其他一无所知。
|
现在我具体讨论一下本章里一直在讲的这个绘图的例子。
首先,找出什么是变化的。在这个例子里,也就是不同的形状和不同的绘图程序。而这些不同的形状、不同的绘图程序在概念上,分属于“形状”和“绘图程序”,这样,我就可以用
10-9
所示的类来代表这些不同的形状和程序
(
注意图里面的类名是斜体的,这表示他们都是抽象类
)
。
Figure
10-9 What is varying?
这样,我打算用
Shape
来封装这个例子里的所有的不同的形状,而代表这些不同的形状的类将必须自己能够完成他本身的绘制工作。
Drawing
类的对象将仅仅负责完成“线”和“圆”等具体线条的绘制工作。这些具体的绘制,都将通过在类里定义的一些方法来完成。
现在我们已经很抽象地表示出“形状”和“绘图程序”了,下一步,就是怎么表示具体的形状和具体的绘图程序的问题了。我有的“形状”是矩形和圆,那么,我就从
Shape
里派生出
Rectangle
和
Circle
来分别代表矩形和圆。“绘图程序”我必须使用已有的
DP1
和
DP2
,但是我又不能直接使用它们,所以,我可以从
Drawing
派生出
V1Drawing
和
V2Drawing
,让他们分别去使用
DP1
和
DP2
。这样,就得到如图
10-10
所示的类图。
Figure
10-10 Represent the variations.
到目前为止,这一切看起来还是有点抽象。我只说了
V1Drawing
和
V2Drawing
要分别对应地使用到
DP1
和
DP2
,但是我还没有说怎么用。我只是把整个例子中存在的一些变化找了出来而已。
有了这两组类,我现在要关心的就是它们之间怎么彼此关联起来的问题了。我不想再出现一组用同样继承的方式出现的类,这样的结果我已经很清楚了
(
再回过头去看看图
10-3
和
10-7
,加深点印象
)
。相反地,我在想,我是不是可以通过用一组类使用另一组的方式来完成
(
这不就是用聚合了吗?
)
。现在的问题是,两组类,到底谁用谁?
很明显的两种可能性:要么在
Shape
里用到
Drawing
,要么反过来,
Drawing
里用到
Shape
。
我们先来看看后面这种
Drawing
里使用
Shape
的情况,如果绘图程序直接使用到具体的形状,那么这些程序就必须知道该怎么去完成这些具体形状的绘制。但是,这些形状仅仅只知道如何完成“线”和“圆”这些简单线条的绘制,这,超出了它们的能力范围。
那么,再看看前面的那种情况。如果在
Shape
里用
Drawing
会怎么样?
Shape
将没有必要知道它所使用的
Drawing
的确切类型,因为,我们已经用
Drawing
封装了
V1Drawing
和
V2Drawing
。
这样看起来不错,就像图
10-11
所示的那样。
Figure
10-11 Tie the classes together.
在这个设计里,
Shape
通过
Drawing
来完成自己的绘制。这里,我略去了
V1Drawing
使用
DP1
和
V2Drawing
使用
DP2
的细节,在图
10-12
里,我把这些都表示了出来还加入了一些
protected
的方法。
Figure
10-12 Expanding the design.
图
10-13
所示的是把
Shape(
抽象
)
和
Drawing(
实现
)
分离开来。
Figure
10-13 Class diagram illustrating separation of abstraction and
implementation.
-
一个规则,一个地方
|
我们必须遵循的一个非常重要的实现策略就是,一个规则仅仅应用到一个地方。也就是说,如果你有一个处理事务的规则,这个规则就仅用一次就好。很显然这会导致设计里出现很多的小方法。但是,这么做你可以避免一些预想不到的问题的出现,可以避免因为规则相似,而复制原有代码再修改满足新规则的情况,而这么做的代价是很小的。
虽然
Rectangle
或者
draw()
方法都可以直接操纵
Shape
的任意的
Drawing
对象,我可以通过使用下面的“一个规则,一个地方”的策略来改进这点。我让
Shape
的
drawLine()
方法来调用
Drawing
对象的
drawLine()
。
当然,凡事也不是那么的绝对。如果,我认为某个地方应该一直遵守某种规则的话,就像这个例子一样。我的
Shape
类有个
drawLine()
,因为它代表着我要用
Drawing
来绘制一条直线;我同样可以用这样的方式创建一个
drawCircle()
,因为它代表着用
Drawing
绘制一个圆。
|
站在各个类里面的具体方法
(method)
的角度来看,这和基于继承的设计方案
(
如图
10-3)
没什么不同。如果非要说有什么不同,那就是现在这些方法存在的位置不同,他们不再是在同一个类里面,而是被分散到多个类里面。
在这一章一开始我就说了,我对
Bridge
模式的困惑是源于我对“实现”的误解。我以为“实现”指的是怎么实现一个具体的抽象。
是
bridge
模式让我意识到,应该把“实现”看作是一个对象以外的东西,是一个为对象所用的东西。用这样的方式着手设计,在不同的继承链里包含各自的变化。图
10-13
左侧的继承链包含了抽象的变化,右侧的继承链包含的则是我如何实现这些抽象的变化。这和我们说要“尽量使用聚合而不是继承”的原则相一致。
虽然在整个设计里有不少的类,但是,你只要牢记一点,你一次要处理的就只有三个对象,这样你的思路就会很清晰。
Figure
10-14 There are only three objects at a time.
Example
10-3 Java Code Fragments
public
class
Client {
public
static
void
main(String args[]){
Shape myShapes[];
Factory myFactory
=
new
Factory();
//
get rectangles form 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
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);
}
}
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, y1, x2, y2);
}
public
void
drawCircle (
double
x,
double
y,
double
r){
DP2.drawCircle (x, y, r);
}
}
回顾
Bridge
模式
Bridge
模式主要功能
|
目标
|
把具体实现从使用该实现的对象中分离出来。
|
环境
|
(
概念
)
抽象类的派生类需要使用多个实现方法而又要避免类的数量的过多。
|
解决方式
|
给所有实现定义一个公共接口并让
(
概念
)
抽象的派生类使用它。
|
参与方式
|
Abstraction
给要实现的对象定义一个接口。
Implementor
给具体的实现定义接口,
Abstraction
的派生类使用
Implementor
的派生类而不管到底是用的哪一个
ConcreteImplementor
。
|
结果
|
|
执行方式
|
-
把所有实现封装在一个抽象类里。
-
在要实现的
(
概念
)
抽象里包含上面的抽象类。
|
Figure
10-15 Generic structure of the Bridge pattern.
|
posted on 2006-11-18 14:35
xiaosilent 阅读(1882)
评论(2) 编辑 收藏 所属分类:
设计模式