对Bridge模式的理解
摘要:本文首先解释了Bridge模式的定义。然后通过一个例子,一步步将Bridge模式实现。
在一切开始之前,请允许我先给出三条经典名言:Design to interfaces. Find what varies and encapsulate it. Favor composition over inheritance.后面我们会反复,并且是反反复复的用到。我认为在做设计的时候这三句话要牢牢的印在脑子里。
一. 定义
根据GOF的定义,Bridge模式的目的是“解耦抽象与它的实现,以便二者可以独立的变化。”这个定义中最容易误解的“抽象”与“它的实现”。因为这两个词在面向对象的语言中都有对应的关键字。在Java中即是“abstract”和“implement”,所以很容易造成困惑的是认为要解耦一个抽象类和它的实现类。实际上,这里实现是指的抽象类和它的派生类用以实现自己的对象,进一步说就是这里的抽象指的是一个概念或者说是一个继承体系中的对象,而实现被抽象使用并完成自己的功能。举个例子,这是吕震宇兄想出的一个例子(http://www.cnblogs.com/zhenyulu/articles/67016.html),这是我见过的最经典的例子了,以至于我不得不在这里重复一遍:
小时候我们都用蜡笔画画,一盒蜡笔12种颜色。一开始我都是用最小号的蜡笔画个太阳公公、月亮婆婆足够了。后来开始画一些抽象派的作品,就得换中号的了,要不然画个背景都要描半天,好一盒中号的也是12种颜色。再后来我开始转向豪放派,中号就有些捉襟见肘了,只好换大号的了,好一盒大号的也只有12种颜色。你看,像我这样不太出名的画家就需要36种画笔,哇,太麻烦了。但是据我观察,另一些比我出名的画家倒是没有这么多笔,他们只有几把刷子和一些颜料,这样就解决了蜡笔的“种类爆炸”问题。如下图所示,注意图也是从吕兄的网站偷来的。
齐白石老先生只用3种毛笔和12种颜料
回到上面的定义,定义里面的抽象指的就是毛笔,实现就是颜料。颜料被毛笔使用,用以完成毛笔的功能。
(http://www.niufish.com/books/Pattern/com/niufish/pattern/bridge/package-use.html)
在下认为上图中有以下几点可以变通:
1. Abstraction与Implementor的聚合关系。该关系可以由RefinedAbstraction和Implementor的聚合关系代替。理由是Java编程的时候Abstraction可能会实现为interface,不能与Implementor构成聚合关系。
2. Operation方法不一定在Abstraction中做实现,理由同上,但是必须声明。
可见在Bridge模式中有两个继承体系,为了方便描述我们称左边的为Abstraction继承体系,右边为Implementor继承体系。Abstraction使用Implementor完成自己的功能。同时,该模式允许Abstraction和Implementor各自独立变化(所谓变化,我认为就是派生)。
二. 解决的问题
上面已经说了,提出毛笔的概念目的是解决蜡笔的“种类爆炸”问题。3×12变成了3+12,并且在将来毛笔型号和颜料种类可以独立的扩充。上面的例子在说明这种设计模式的特点和优势上很有好处,但是毕竟我们实际编码中很少这么幸运的碰上这么简单的问题。下面我用一个相对复杂的问题来重新描述这个模式,关于蜡笔和毛笔的故事的代码可以到上面给出的链接查找。
三. 一个更加复杂的例子
这里我们给出一个更为复杂的例子,并且用一种循序渐进的方式描述,逐渐加入新的功能和约束条件。设想我们要做一个编辑器(Editor),可以打开文本文件,但是不同的文件要求用不同的编辑器打开。比如“.txt”文件用文本编辑器打开,而“.xml”文件用xml编辑器打开。
Editor接口
package com.gemplus.editor;
public abstract interface Editor {
public void openFile(String path);
}
TextEditor实现
package com.gemplus.editor;
public class TextEditor implements Editor {
public void openFile(String path) {
System.out.println("Open file with Text Editor. FileName: " + path);
}
}
XMLEditor实现
package com.gemplus.editor;
public class XMLEditor implements Editor {
public void openFile(String path) {
System.out.println("Open file with XML Editor. FileName: " + path);
}
}
Client
package com.gemplus.editor;
public class Client {
public static void main(String[] args) {
Editor xmlEditor = new XMLEditor();
xmlEditor.openFile("test.xml");
Editor textEditor = new TextEditor();
textEditor.openFile("test.txt");
}
}
输出
Open file with XMLEditor. FileName: test.xml
Open file with Text Editor. FileName: test.txt
到目前为止,我们什么模式也没用到。不过我们还是用到了一项伟大的技术多态,还有就是对接口编程。代码写的还算优雅,只是没有写注释。
接下来我们要增加一个功能,与其说是功能,不如说是一项约束。就是,我不希望客户端了解那些文件要由xmlEditor打开,哪些由textEditor打开。但是总要有人知道,对吧?这项看似直观的约束往往被忽略,或者在潜意识中没有意识到。为了简单起见,我们引入一个简单工厂EditorImpl。
public class SmartEditor implements Editor {
private static Editor textEditor = new TextEditor();
private static Editor xmlEditor = new XMLEditor();
public void openFile(String path) {
if (path.endsWith(".xml")) {
xmlEditor.openFile(path);
} else {
textEditor.openFile(path);
}
}
}
Client也要做相应修改
public class Client {
public static void main(String[] args) {
// Editor xmlEditor = new XMLEditor();
// xmlEditor.openFile("FileName");
// Editor textEditor = new TextEditor();
// textEditor.openFile("FileName2");
Editor editor = new SmartEditor();
editor.openFile("test.xml");
editor.openFile("test.txt");
}
}
输出结果与修改之前完全一样,但是现在Client端已经不需要知道应该使用哪个Editor了,这其实也是对Find what vary and encapsulate it的应用。另外一点,也可以请大家注意在EditorImpl的实现中,即继承了Editor接口又使用了组合这其实是联合inheritance和composition的优点。Javaworld上有一篇文章(作者Bill Venners)就是讲这个技巧和使用方法的,我找了半天没找到,以后找到再把链接添上。
先别高兴太早,我们看看目前这种设计有什么问题。SmartEditor在充满了技巧和高级的、伟大的技术的同时,你有没有觉得它管的东西太多了呢?一个Editor要去关心文件名的问题,不错SmartEditor出现的目的就是要来关心文件名,然而从概念上讲,仍然不是好的设计。我们来看看什么在变化?答案是文件类型。OK,封装之!
我们把它称作什么呢?对应与Edtior,我们不妨称之为Editable。(说实话,我这样这个概念建立起来有一些生硬,但是你可以想像一下:如果被编辑的东西是更加复杂的呢?我所取的例子实际上是我工作中的一个例子,要编辑的东西要比这里描述的复杂的多!)
简单之极,以至于我觉得没有必要给出代码。但是第一次写文章,总要给大家留点好印象。
public class FileEditable implements Editable {
Editor editor;
String filePath;
public FileEditable(Editor editor, String filePath) {
this.editor = editor;
this.filePath = filePath;
}
public void open() {
editor.openFile(filePath);
}
}
Client端
public class Client {
public static void main(String[] args) {
Editable editableText = new FileEditable(new TextEditor(), "test.txt");
Editable editableXML = new FileEditable(new XMLEditor(), "test.xml");
editableText.open();
editableXML.open();
}
}
输出仍然没有变化。(前面做的其实也可以算作是重构吧,可观察行为没有变化)
不得不承认,现在客户端又需要了解什么样的文件用什么编辑器打开了。但是一个小的变化就可以避免这一点。为FileEditable增加一个不包含Editor参数的构造函数。
public FileEditable(String filePath) {
this.filePath = filePath;
if (filePath.endsWith(".xml")) {
editor = new XMLEditor();
} else {
editor = new TextEditor();
}
}
客户端,这里就不给出了。
现在该是增加功能的时候了。现在我们要求,用户不但可以打开文件也有可能用这个Editor来编辑一段文字,即字符串。我们假设,所有的Editor都可以编辑文件和字符串。我们为Editor增加一个方法:openString。前面我们说了,Bridge模式中有两个继承体系,两边可以独立变化。现在我们有了两个Editable,即两种可编辑体:文件和字符串。也可以看出我们当初把Editable封装起来多么的英明神武啊!!
在Editable的继承体系中又增加了一员:StringEditable。
虽然我前面已经说了,Abstraction和Implementor的聚合关系可以由派生类RefinedAbstraction和Implementor的聚合关系代替,为了与经典模式类图保持尽量的相似,以便大家容易理解,我们还是在StringEditable和FileEditable上面加了一层:EditableImpl。
好了,我们已经实现了Bridge模式。不过这里面还是有一点让人不舒服的地方,就是Editor中包含的两个方法,实际上对应了两种Editable。也许是这个例子举的不好。但是《设计模式 explained》中的例子和本例大同小异。
我也是刚刚开始学习设计模式,如果有什么不对的地方请大家指出。接下来,我会继续这个例子,增加一个更加复杂的功能:可以嵌套组合的Editor。比如对于同一个文件我们可能希望有两种打开方式,以网页为例,我们希望在一个Editor中包含两个子Editor,分别显式源代码和页面效果。这里面会用到Composite模式。
参考资料:
1. http://www.cnblogs.com/zhenyulu/articles/67016.html
《设计模式随笔-蜡笔与毛笔的故事》本文对于Bridge模式给了一个相当漂亮的比喻,并且给出了代码示例。
2. http://www.cnblogs.com/zhenyulu/articles/62720.html
《设计模式(16)-Bridge Pattern》这篇文章没有仔细看,前面的角色定义和解释很精辟。
3. http://www.cnblogs.com/idior/articles/97283.html
《Bridge Strategy 和State的区别》个人认为,作者在Bridge和Strategy模式的区别上面有误解。下面有我的回复:
我倒是不赞同楼主所说在“蜡笔与毛笔的故事”中“缺少被抽象的行为”的说法。Bridge模式中解耦的是“抽象”与“实现”。这里的实现不一定是对“行为”的抽象,按照《Design Pattern Explained》一书中所说,这里实现是指的抽象类和它的派生类用以实现自己的某个对象(本身是抽象与其派生类构成的继承体系,要不怎么变化呢)。进一步说就是这里的抽象指的是一个概念或者说是一个继承体系中的对象,而实现则是被抽象使用并完成自己的功能的另一个继承体系。桥梁模式的目的就是让这两个继承体系可以,独立的变化、派生。蜡笔与毛笔的故事中毛笔(第一个继承体系)和颜料(第二个继承体系)可以独立的变化,所以我认为这是个非常恰当的桥梁模式的例子。
继续:)
我觉得区分两个模式的方法不是从模式的实现上面看,因为一个模式的实现往往夹杂了其它的模式,比如idior给的第一个例子中就有Template Method模式。我赞成吕震宇的说法,这里面有Bridge模式的影子,甚至我觉得不止是影子,这本身是一个Bridge模式的例子。
区分两个模式的方法应该从解决的问题上看,也就是从context上分析。
我觉得简单的说Strategy模式是从N变化为1+N,原来有N个类但是这N个类里面只有某个算法的区别,我们把N个算法提取出来就变成了1个抽象类(不要理解成Java中的abstract class,而是这个抽象类表示一个概念)和N个实现类(同理,不要理解成对前面那个抽象类的实现,而是辅助实现抽象类的某个功能的一个继承体系)。注意这里只有一个继承体系。
而Bridge模式是从M×N变化为M+N,原来系统中有M×N个类,但是从中可以提取出N个算法(或者辅助类)和M个主体(我想不出一个好的名次)。这样构成了两个继承体系,N个算法(颜料)构成一个继承体系,M个主体类(毛笔的不同型号)构成一个继承体系。两个继承体系可以独立的变化。
从解决的问题上看,二者都要解决重复代码的问题,但是前者不强调锥把(见吕震宇的回复)的变化,而后者强调,并且强调锥头和所有锥把的兼容。我认为这才是二者的根本区别。
这是我个人的理解,与楼主商榷。
4. http://www.niufish.com/books/Pattern/com/niufish/pattern/bridge/package-use.html
设计模式速查,很不错