
桥接模式的概念
- 桥接模式(Bridge Pattern)是一种结构型设计模式,它的主要目的是将抽象部分与它的实现部分分离,使它们都可以独立地变化。这种模式就像是在抽象和实现之间搭建了一座“桥”,让它们可以通过这座桥来进行协作,而不是将它们硬编码在一起。
- 例如,假设有一个图形绘制系统,需要绘制不同颜色和形状的图形。如果不使用桥接模式,可能会创建很多不同的类来表示各种颜色和形状的组合,如“RedCircle(红色圆形)”、“BlueRectangle(蓝色矩形)”等。这样会导致类的数量急剧增加,而且如果要添加一种新的颜色或者形状,就需要创建更多的类。而使用桥接模式,可以将颜色和形状这两个维度分开处理,通过“桥”来组合它们。
-
桥接模式的结构
- 抽象部分(Abstraction):定义了抽象类的接口,它包含一个对实现部分对象的引用。这个抽象类通常会提供一些高层次的操作,这些操作会调用实现部分的方法来完成具体的工作。
- 细化抽象部分(RefinedAbstraction):是抽象部分的子类,它实现了抽象类中的抽象方法,并且可以根据具体的需求扩展功能。这些子类可以通过调用实现部分的方法来完成自己的功能。
- 实现部分(Implementor):定义了实现部分的接口,这个接口不一定要和抽象部分的接口完全一致。它提供了基本的操作,这些操作可以被抽象部分及其子类调用。
- 具体实现部分(ConcreteImplementor):是实现部分接口的具体实现类,它们实现了实现部分接口中定义的方法,提供了具体的功能。
-
C++代码示例
- 假设我们要实现一个简单的图形绘制系统,有两种形状(圆形和矩形)和两种颜色(红色和蓝色)。
- 实现部分(Implementor)接口
- 首先定义一个颜色接口,用于表示颜色相关的操作。
-
class Color { public: virtual void applyColor() = 0; };
- 具体实现部分(ConcreteImplementor)类
- 然后实现红色和蓝色的具体颜色类。
-
class RedColor : public Color { public: void applyColor() override { std::cout << "Applying red color." << std::endl; } }; class BlueColor : public Color { public: void applyColor() override { std::cout << "Applying blue color." << std::endl; } };
- 抽象部分(Abstraction)类
- 定义一个形状抽象类,它包含一个颜色对象的引用,并且有一个绘制方法。
-
class Shape { protected: Color* color; public: Shape(Color* c) : color(c) {} virtual void draw() = 0; };
- 细化抽象部分(RefinedAbstraction)类
- 实现圆形和矩形的具体形状类,它们在绘制方法中会调用颜色对象的方法来设置颜色并进行绘制。
-
class Circle : public Shape { public: Circle(Color* c) : Shape(c) {} void draw() override { std::cout << "Drawing a circle. "; color->applyColor(); } }; class Rectangle : public Shape { public: Rectangle(Color* c) : Shape(c) {} void draw() override { std::cout << "Drawing a rectangle. "; color->applyColor(); } };
- 使用示例
- 在主函数中,可以这样使用桥接模式来绘制不同颜色的图形。
-
int main() { RedColor red; BlueColor blue; Shape* circle1 = new Circle(&red); Shape* circle2 = new Circle(&blue); Shape* rectangle1 = new Rectangle(&red); Shape* rectangle2 = new Rectangle(&blue); circle1->draw(); circle2->draw(); rectangle1->draw(); rectangle2->draw(); delete circle1; delete circle2; delete rectangle1; delete rectangle2; return 0; }
- 在这个代码中,形状类(Shape)和颜色类(Color)是通过桥接模式分离的。形状类的对象可以和不同的颜色类对象组合,从而实现了抽象和实现的分离,使得添加新的形状或者颜色都更加容易。例如,如果要添加一种新的颜色“GreenColor”,只需要创建一个新的类继承自Color接口并实现applyColor方法即可,而不需要修改形状相关的类。同样,如果要添加一种新的形状“Triangle”,只需要创建一个新的类继承自Shape抽象类并实现draw方法,也不需要修改颜色相关的类。
总结
一般问题:一个类需要在两个以上维度扩展,采用继承方式会导致子类数量过多
核心方案:将抽象部分与实现部分分离,使其都可以独立变化,采用组合方式
设计意图:桥接模式不是将两个不相干的类链接,而是将一个需要多维度变化的类拆分成抽象部分和实现部分,并且在抽象层对两者做组合关联,是用组合的方式来解决继承的问题。举个例子,如果一个类在两个维度分别有m和n种变化,采用继承的方式就需要扩展出m*n个子类,且一个维度每增加一种变化就多出另一个维度变化总数的子类;如果将两个维度拆分再组合,加起来也只有m+n个子类,且每个维度独立扩展,一个维度增加一种变化只需要增加1个子类。
合成/聚合复用原则
-
定义
- 合成聚合复用原则(Composite/Aggregate Reuse Principle,CARP)也叫组合/聚合复用原则。它是面向对象设计原则之一,指的是尽量使用对象组合(has - a关系)或聚合(contains - a关系)而不是继承来达到软件复用的目的。
- 组合是一种强“拥有”关系,体现部分和整体的生命周期是一致的,例如鸟和翅膀,翅膀作为鸟的一部分,当鸟不存在时,翅膀也不存在了。聚合是一种弱“拥有”关系,部分可以独立于整体存在,例如雁群和大雁,大雁可以离开雁群独立生存。
-
与继承复用对比
- 继承复用的缺点
- 强耦合性:在继承关系中,子类与父类紧密耦合。例如,有一个基类“Vehicle(交通工具)”,它有两个子类“Car(汽车)”和“Motorcycle(摩托车)”。如果在Vehicle类中添加一个新的方法“repair(维修)”,那么Car和Motorcycle类都会继承这个方法。但如果Car类需要对“repair”方法有不同的实现,比如汽车维修可能涉及更多复杂的检查项目,就需要重写这个方法。这可能会导致代码在继承体系中频繁修改,而且一旦父类发生变化,子类可能会受到意外的影响。
- 缺乏灵活性:继承关系在编译时就已经确定,不能在运行时改变。比如一个基于继承的图形绘制系统,有基类“Shape(形状)”和子类“Circle(圆)”“Rectangle(矩形)”等。如果想要在运行时动态地改变一个形状对象从圆形变成矩形,使用继承就很难实现这种灵活的转换。
- 组合/聚合复用的优点
- 松耦合:组合和聚合关系使得各个类之间相对独立。以汽车为例,可以有一个“Engine(引擎)”类和一个“Car”类,它们通过组合关系关联。“Car”类拥有一个“Engine”类的对象作为其成员。如果“Engine”类的内部实现发生变化,比如改进了引擎的燃油喷射系统,只要“Engine”类对外提供的接口不变,“Car”类的代码基本不需要修改。
- 灵活性高:在运行时可以方便地改变组合或聚合的对象。例如,在一个游戏开发中,角色的武器是通过组合的方式添加到角色身上的。可以在游戏运行过程中,根据游戏情节的发展,为角色更换不同的武器,只需要重新组合武器对象和角色对象即可。
- 继承复用的缺点
-
应用场景示例
- 游戏开发中的道具系统:假设正在开发一款角色扮演游戏。有一个“Player(玩家)”类,玩家可以拥有不同的“Item(道具)”。“Player”类和“Item”类之间是聚合关系,因为玩家可以捡起或丢弃道具。可以通过在“Player”类中定义一个集合(如列表)来存储玩家拥有的道具。
-
遵循原则的好处
- 提高代码的可维护性:由于类之间的耦合度降低,当对某个类进行修改时,对其他类的影响范围较小。这样在维护代码时,更容易定位和解决问题。
- 增强代码的可扩展性:方便添加新的功能。例如在上述游戏道具系统中,如果要添加一种新的道具类型,只需要创建新的道具类并实现其功能,然后在玩家类中稍作修改(如果需要),就可以将新道具集成到游戏中,而不会对原有的游戏系统造成大规模的破坏。
- 提升代码的复用性:可以更好地复用已有的类。比如在不同的游戏场景或者不同的游戏角色中,可以复用“Item”类的各种道具,通过组合或聚合的方式将它们添加到不同的角色或场景中。