桥接模式
本片博客将介绍桥接模式,桥接模式可以说就是一个组合复用原则的例子,其主要思想就是组合复用。桥接模式是一个比较常用的设计模式,一般情况下,当我们想要构建一个复杂对象时,往往都会使用到桥接模式来进行设计。桥接模式可以将一个复杂对象的多个独立维度分离。
通常情况下,使用桥接模式的复杂对象往往会使用建造者模式来构建。
模式分类
结构型设计模式。
模式产生的原因
如果不使用桥接模式,我们一般就会使用多层继承的方式来构建一个复杂的对象,但是使用多层继承构建对象会存在两点缺点:1. 当有需求变化的时候,系统内使用多层继承的类的数量会急剧上升。2. 使用多层继承构建出来的对象,其不同维度之间的耦合度较高,不能独立拓展。
但是如果我们使用桥接模式来构建这个对象,就不会存在上述的两个问题。
模式灵感的来源
在现实生活中,毛笔和蜡笔是比较常见的两种文具,他们都归属于画笔。假如现在需要大,中,小3种型号的画笔,并且可以分别绘制12种颜色。如果我们使用蜡笔我们就需要准备3 * 12 = 36只蜡笔,但是如果我们使用毛笔我们只需要准备3个型号的毛笔,外加一个有12种颜色的调色板即可,这里我们只需要准备3 + 12 = 15,远小于36,却可以实现与36只蜡笔相同的功能。这里毛笔就使用了桥接模式的思想,将笔的型号和笔的颜色两个属性分离,让其可以单独变化。
这时,如果我们新加一种型号的笔,蜡笔需要增加12只,而毛笔只需要增加1只。
模式类图
桥接模式由以下4个对象组成:
Abstraction(抽象类):
抽象类是我们想要创建的对象的其中一个维度,用于定义抽象类的接口,通常是抽象类而不是接口,抽象类会维护一个实现类的对象。
RefineAbstraction(扩充抽象类):
抽象类的子类,可以拓展父类的一些方法。
Implementor(实现类):
实现类是我们想要创建对象的其余维度,可以只有一个实现类也可以有多个实现类,具体的,有几个维度,我们就有几个实现类,关于如何分别抽象类和实现类我们很快就会说到。
ConcreteImplementor(具体实现类):
实现类的具体类。
关于抽象类和实现类如何判断?
这里我们要知道我们希望通过桥接模式来创建出一个具有多个维度的复杂对象,通常情况下,我们会将与业务方法关系最紧密的维度视为抽象类,也就是主体,剩下的维度会被视为实现类。
就比如上面我们举得例子:我们可以将毛笔的型号作为抽象类,颜色作为实现类。
当然你可以将所有的维度都视为实现类,这时我们就需要单独找一个类来作为我们的抽象类。这种判断方法更加简单快速。
就比如上面我们举得例子:我们可以将毛笔作为抽象类,型号和颜色作为实现类。
代码实现
代码实现上我们举一个飞机制造商的例子:
空客(Airbus)、波音(Boeing)、麦道(McDonnell-Douglas)都是飞机制造商,他们都可以生产载客飞机(PassengerPlane)、载货飞机(CargoPlane),请使用桥接模式来描述飞机制造商和他们生产的飞机。
飞机制造商基类(抽象类):
using System;
using System.Collections.Generic;
namespace Bridge.Bridge.Question5
{
public abstract class PlaneMaker
{
//飞机种类列表
public List<PlaneClass> PlaneProduct = new List<PlaneClass>();
//飞机制造商名字
public string MakerName;
public PlaneMaker(string name)
{
MakerName = name;
}
//设置飞机商可以生产的飞机种类
public void SetPlaneClass(PlaneClass planeClass)
{
if (PlaneProduct.Contains(planeClass))
{
return;
}
PlaneProduct.Add(planeClass);
}
//打印出这些飞机种类
public void SpeakPlaneName()
{
foreach (var item in PlaneProduct)
{
Console.WriteLine($"{MakerName}的飞机产品{item.SpeakName()}");
}
}
}
}
飞机种类类(实现类):
using System;
namespace Bridge.Bridge.Question5
{
public abstract class PlaneClass
{
private string PlaneName;
public PlaneClass(string name)
{
PlaneName = name;
}
public string SpeakName()
{
return PlaneName;
}
}
}
空客:
namespace Bridge.Bridge.Question5
{
public class Airbus : PlaneMaker
{
public Airbus(string name) : base(name)
{
}
}
}
波音:
namespace Bridge.Bridge.Question5
{
public class Boeing : PlaneMaker
{
public Boeing(string name) : base(name)
{
}
}
}
麦道:
namespace Bridge.Bridge.Question5
{
public class McDonnell_Douglas : PlaneMaker
{
public McDonnell_Douglas(string name) : base(name)
{
}
}
}
载客飞机:
namespace Bridge.Bridge.Question5
{
public class PassengerPlane : PlaneClass
{
public PassengerPlane(string name) : base(name)
{
}
}
}
载货飞机:
namespace Bridge.Bridge.Question5
{
public class CargoPlane : PlaneClass
{
public CargoPlane(string name) : base(name)
{
}
}
}
Program:
using Bridge.Bridge.Question5;
namespace Bridge
{
internal class Program
{
public static void Main(string[] args)
{
PlaneMaker planeMaker = new Airbus("Airbus");
planeMaker.SetPlaneClass(new CargoPlane("CargoPlane"));
planeMaker.SetPlaneClass(new PassengerPlane("PassengerPlane"));
planeMaker.SpeakPlaneName();
planeMaker = new Boeing("Boeing");
planeMaker.SetPlaneClass(new CargoPlane("CargoPlane"));
planeMaker.SetPlaneClass(new PassengerPlane("PassengerPlane"));
planeMaker.SpeakPlaneName();
planeMaker = new McDonnell_Douglas("McDonnell_Douglas");
planeMaker.SetPlaneClass(new CargoPlane("CargoPlane"));
planeMaker.SetPlaneClass(new PassengerPlane("PassengerPlane"));
planeMaker.SpeakPlaneName();
}
}
}
桥接模式和适配器模式的联用
这里我们介绍两种联用的情况,对于这种变化程度较大的具体需求,这里不再提供类图,对于具体需求的构建每个人都会有自己的想法,死记硬背别人的类图没有什么意义。
联用场景1:
当一个适配器的职责比较多时,我们可以利用桥接模式将适配器的职责分离,这样当一个职责有新需求的时候我们就不要重新更改适配器类了。
联用场景2:
当一个复杂对象想要使用一些无法直接调用的接口时,我们可以加入一个适配器维度。
桥接模式的总结
桥接模式的优点:
- 在很多情况下,桥接模式都可以取代多层继承方案,多层继承方案违背了单一职责原则,复用性较差,且类的个数非常多。桥接模式是比多层继承方案更好的解决方案,它极大地减少了子类的个数。
- 桥接模式提高了系统的可拓展性,在两个变化维度中任意拓展一个维度,不需要修改原有系统,符合开闭原则。
桥接模式的缺点:
- 桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计。
- 桥接模式需要正确识别出变化的维度。