桥接模式(Bridge)
1.1.1 摘要
在软件系统中,某些类型由于自身的逻辑,它具有两个或两个以上的维度变化,那么如何应对这种“多维度的变化”呢?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度呢?这就是即将要介绍的桥接模式(Bridge)。
- 定义
桥接模式(Bridge),将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 意图
将抽象与实现解耦。
- 动机
当一种抽象类型可能有多种实现方式时,一般情况我们可以考虑使用继承来解决抽象类型的多种实现,在抽象类型中定义接口,而子类负责接口的具体实现。但这种做法缺乏灵活性,由于抽象类型和子类之间紧紧地绑定在一起,使得这种关系在运行时不能再修改,这使得它难以修改、扩展和重用不利于抽象和实现解耦,而且这也违背OOP原则:“优先使用对象聚集,而不是继承”。
- 结构图
图1 桥接模式(Bridge)
- 参与者
Abstraction
1. 定义抽象接口。
2. 拥有一个Implementor类型对象引用。
- RefinedAbstraction
1. 扩展Abstraction中的接口定义。
- Implementor
1. Implementor是具体实现的接口,Implementor和Abstraction接口并不一定完全一致(注:Proxy和ISubject接口一一对应),实际上这两个接口可以完全不一样,Implementor提供具体操作方法,而Abstraction提供更高层次的调用。
- ConcreteImplementor
1. 实现Implementor接口,给出具体实现。
1.1.2 正文
首先根据桥接模式(Bridge)的定义:将抽象部分与它的实现部分分离,但令我不解的是,怎样才能将抽象与其实现的具体方式分离呢?其实我的迷惑主要是因为误解了实现的含义。这里实现指的是抽象类及其派生类用来实现自定的对象(而不是抽象类的派生类,这些派生类被称为具体类)。不过这样还是难以理解,现在让我们通过具体的例子说明。
我们必需了解桥接模式(Bridge)存在的价值,然后推迟该模式,OK我们通过一个简单的例子说明为什么需要桥接模式(Bridge)吧!
从一个绘制形状的简单问题开始,假设我们接受一个任务:编写一个程序,使用两个绘图程序DrawProgram1和DrawProgram2之一绘制图形(矩形,圆形等),而且我们被告知,实例化图形的时候,它会知道应该使用绘图程序DP1还是DP2。
通过分析我们可以找出抽象类Shape,然后定义Retangle和Circle类继承抽象类Shape,还有就是绘图程序DP1和DP2。
图2绘图程序类图
我们现在初步定义了相关的方法和类,而把Shape,Retangle和Circle都定义为抽象类型方便以后扩展。但我们可以发现图形类型并没有与绘图程序关联起来,而且前面需求中提到图形实例化时候知道具体调用哪个绘图程序。OK那么现在让我们把图形和绘图程序关联起来。
图3绘图程序类图
我们定义了Retangle1和Retangle2继承于抽象类Retangle,添加DrawLine()方法分别调用DP1和DP2的DrawLine()方法,并且Circle1和Circle2中的实现基本相同。我们使用了一种直截了当的方法,实现了两种图形和两个绘图程序的关联。
现在Shape有四个具体类型(Retangle1,Retangle2,Circle1和Circle2),而且每个具体类型都和相应绘图程序对于,但我们要记住“没有不变的需求,世上的软件都改动过三次以上,唯一一个只改动过两次的软件的拥有者已经死了,死在去修改需求的路上”,所以需求总是在不断的变化之中,如果添加新的绘图程序DP3,那么我们就要增加两个具体类型,而且具体类型中调用DP3方法跟之前调用DP1、DP2并没有太大的区别(冗余问题),现在有两种图形(Retangle和Cirle)和三个绘图程序(DP1,DP2和DP3),那么就拥有六种不同Shape(2种图形 * 3个绘图程序),如果我们继续扩展成三种图形那么就具有九种不同Shape(3种图形 * 3个绘图程序),这就会导致“类爆炸”问题。
上面的解决方法,因为抽象类型(Shape)和绘图程序之间是紧耦合,于是存在严重的“类爆炸”问题(每种形状都必须知道自己用哪个绘图程序)。我们需要一种方式将抽象上的变化和实现变化进行解耦。
将抽象与实现解耦这不就是桥接模式(Bridge)的意图吗?在介绍桥接模式(Bridge)之前,我们总结一下前面方式中的问题。
- 存在冗余
- 低内聚
- 紧耦合
由于前面的例子按照不同图形来进行继承的分类,如果我们按照不同绘图程序分类结果又如何呢?OK,那么让我们画出按照不同绘图程序分类类图。
图4绘图程序类图
现在我们继续使用四个类表示现有的图形的组合,但这里我们按照不同绘图程序派生不同图形,所以我们消除了图形类(Retangle1,Retangle2, Circle1和Circle2)和绘图程序的之间的紧耦合,从而消除了它们之间的冗余。现在把耦合转移到更高的继承层次,但问题有出现了当有新的绘图程序加入时,我们的确可以轻松地进行扩展,只要增加ShapeDP3类继承抽象类Shape就OK了,但是要实现一套一模一样的Retangle3和Circle3了。
尽管这种方式对前面的方式有所改进,但冗余和耦合问题依然存在。
在我们每次使用设计模式时,我们应该根据设计原则去设计,而不是直接使用已有设计模式去套用,我们要明白的一点是设计模式是根据一定的设计原则而产生的。
这次我们要遵循两个基本原则:
- 找出变化封装之
- 优先使用对象聚集,而不是继承
首先我们可以很快的找出需求中变化:图形和绘图程序,然后我们使用抽象来封装变化就OK了。
图5封装绘图程序中变化
现在我们已经找出了变化图形和绘图程序,注意这里的OperationalDP1和OperationalDP2作为调用绘图程序(DP1和DP2)的接口,因为DP1和DP2是两个已经存在的程序所为我们无法直接抽象出DP1和DP2的高层接口,通过一种间接方式抽象出高层接口,使用抽象类把变化封装在它的“后面”,接着我们就是要在抽象类Shape和DrawingProgramming直接建立依赖关系了(优先使用对象聚集,而不是继承),所以可以通过在其中一个抽象类中保持对方的引用就OK了,但究竟是哪个类依赖于哪个类呢?
这里存在两种情形:
一、DrawProgramming类保存Shape对象引用
二、Shape类保存DrawProgramming对象引用
首先考虑第一种情形,如果在DrawProgramming保存Shape对象引用,我们通过调用Draw()方法绘制图形,但它们必需对Shape类中的图形有所了解(Draw()方法将具体图形封装了),当我们使用OperationalDP1中的DrawCircle()方法时,就要知道Shape中的Circle类型,这违反了对象应该只对自己负责。
图6 情形一
第二种情形,如果Shape对象使用DrawProgramming对象绘制图形时,图形无需知道具体绘图程序。当使用Circle中的Draw()方法我们只需要调用DrawProgramming中的方法DrawCircle(),因此可以让Shape保存DrawProgramming的对象引用。
图6 情形二
通过分析我们可以确定采用情形二实现起来相对简单,接着在抽象类Shape增加DrawProgramming对象引用,从而在Shape和DrawProgramming之间建立了一种聚集关系(has – a关系)。
现在让我们回忆一下桥接模式(Bridge)的定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。Shape类及其实现就是抽象部分,而DrawingProgramming及其实现是具体部分,通过聚集使得它们分离开来,从而应对变化和扩展更加灵活。
图7 桥接模式实现绘图程序
我们已经完成了绘图程序的类设计,而且通过该程序类设计我们了解到了桥接模式(Bridge)的作用,现在让我们通过具体代码完成绘图程序。
/// <summary> /// As Abstraction. /// </summary> public abstract class Shape { /// <summary> /// Has a reference from DrawingProgramming. /// </summary> private DrawingProgramming _dp; public Shape() { } public Shape(DrawingProgramming dp) { this.Dp = dp; } public abstract void Draw(); public DrawingProgramming Dp { get { return _dp; } set { _dp = value; } } } /// <summary> /// As Refined Abstraction. /// </summary> public class Retangle : Shape { private int _width = 0; private int _height = 0; public Retangle(DrawingProgramming dp, int width, int height) : base(dp) { this.Width = width; this.Height = height; } public override void Draw() { this.DrawRetangle(Width, Height); } public void DrawRetangle(int width, int height) { this.Dp.DrawRetangle(width, height); } public int Width { get { return _width; } set { _width = value; } } public int Height { get { return _height; } set { _height = value; } } } /// <summary> /// As Refined Abstraction. /// </summary> public class Triangle : Shape { private int _rows = 0; public Triangle(DrawingProgramming dp, int rows) : base(dp) { this.Rows = rows; } public override void Draw() { this.DrawTriangle(this.Rows); } public void DrawTriangle(int rows) { this.Dp.DrawTriangle(rows); } public int Rows { get { return _rows; } set { _rows = value; } } } /// <summary> /// As Implementor. /// </summary> public abstract class DrawingProgramming { /// <summary> /// Abstract draw method. /// </summary> /// <param name="width"></param> /// <param name="height"></param> public abstract void DrawRetangle(int width, int height); public abstract void DrawTriangle(int Rows); } /// <summary> /// As concrete Implementor /// </summary> public class OperationalDP1 : DrawingProgramming { public OperationalDP1() { } public DP1 DP1 { get { throw new System.NotImplementedException(); } set { } } public override void DrawRetangle(int width, int height) { DP1.DrawRetangle(width, height); } public override void DrawTriangle(int Rows) { DP1.DrawTriangle(Rows); } } /// <summary> /// As concrete Implementor /// </summary> public class OperationalDP2 : DrawingProgramming { public OperationalDP2() { } public DP2 DP2 { get { throw new System.NotImplementedException(); } set { } } public override void DrawRetangle(int width, int height) { DP2.DrawRetangle(width, height); } public override void DrawTriangle(int Rows) { DP2.DrawTriangle(Rows); } } /// <summary> /// existed drawing programming. /// </summary> public class DP1 { /// <summary> /// Concrete draw method. /// </summary> /// <param name="width"></param> /// <param name="height"></param> public static void DrawRetangle(int width, int height) { for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { Console.Write("■"); } Console.WriteLine(); } } public static void DrawTriangle(int Rows) { for (int i = 0; i < Rows; i++) { for (int j = 0; j < i + 1; j++) { Console.Write("■"); } Console.WriteLine(); } } } /// <summary> /// existed drawing programming. /// </summary> public class DP2 { /// <summary> /// Concrete draw method. /// </summary> /// <param name="width"></param> /// <param name="height"></param> public static void DrawRetangle(int width, int height) { for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { Console.Write("★"); } Console.WriteLine(); } } public static void DrawTriangle(int Rows) { for (int i = 0; i < Rows; i++) { for (int j = 0; j < i + 1; j++) { Console.Write("★"); } Console.WriteLine(); } } }
通过上面的例子我们对桥接模式(Bridge)有了初步的了解,假设我们的绘图程序DP1拥有特有擦除方法Wipe(),从而需要在OperationalDP1类中增加相应的方法,但没有必要修改DrawingProgramming类,现在问题出现了我们要在抽象类Shpe中增加Wipe()方法,但这种修改是我们愿意看到的。这时我们可以考虑以下两种方法解决问题。
方法一:在基类中添加虚的方法,然后需要的子类重写基类的虚方法。
方法二:使用.NET Framework中的扩展方法,对基类方法进行扩展。
1.1.3 总结
桥接模式(Bridge)优点:
将实现予以解耦,让它和界面之间不再永久绑定。
抽象和实现可以独立扩展,不会影响到对方。
对于具体实现的修改,不会影响到客户端。
桥接模式(Bridge)缺点:
增加了设计复杂度。
抽象类的修改影响到子类。
桥接模式(Bridge)用途:
适用在需要跨多平台的图形和窗口系统。
当需要用不同的方式改变接口和实现时。
通过上述的介绍,我们了解为什么需要桥接模式(Bridge)和如何使用桥接模式(Bridge),由于对象的多维度的变化,使得难以决定变化时,我们可以把对象和变化抽象出来。
如果我们的对象依赖于抽象,对于具体的实现并不关心,我们可以通过对象组合,组合出我们想要的对象。桥接模式符合OCP(对于扩展开发,对于修改关闭)设计模式的原则。
关于作者:[作者]:
JK_Rush从事.NET开发和热衷于开源高性能系统设计,通过博文交流和分享经验,欢迎转载,请保留原文地址,谢谢。 |