10. 桥接模式
一、桥接模式
桥接模式 (Bridge Pattern)是一种对象结构型模式,又称为 柄体模式(Handle and Body)或 接口模式(Interface)。它将 抽象部分 与它的 实现部分 分离,使它们都可以独立地变化。桥接模式的核心逻辑是,通过抽象类和实现类之间的聚合关系,将两者解耦,从而允许抽象部分和实现部分独立地变化。
如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合单一职责原则。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
桥接模式用一种巧妙的方式处理多层继承存在的问题。桥接模式采用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。
在桥接模式中,通常涉及以下角色:
- 抽象化角色(Abstraction):定义抽象类的接口,并保存一个对实现化对象的引用。
- 扩展抽象化角色(Refined Abstraction):扩展抽象化角色,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化角色(Implementor):定义实现化角色的接口,供扩展抽象化角色调用,但不给出具体的实现。这个接口不一定和抽象化角色的接口定义相同。
- 具体实现化角色(Concrete Implementor):给出实现化角色接口的具体实现。
在使用桥接模式时,首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为抽象类层次结构(抽象部分),而将另一个维度设计为实现类层次结构(实现部分)。
总之,桥接模式通过将抽象部分与实现部分分离,使得两者可以独立变化,从而提高了系统的可扩展性和灵活性。在实际应用中,可以根据具体需求选择是否使用桥接模式来解决问题。
二、C++实现桥接模式
在具体编码实现时,由于在桥接模式中存在两个独立变化的维度,为了使两者之间耦合度降低,首先需要针对两个不同的维度提取抽象类和实现类接口,并建立一个抽象关联关系。对于 “实现部分” 维度,典型的实现化接口代码如下:
// 实现化角色
class VideoFile
{
public:
virtual void decode(std::string fileName) = 0;
};
具体实现化角色实现实现接口中声明的方法,用于定义与该维度相对应的一些具体方法。
// 具体实现化角色
class AviFile : public VideoFile
{
public:
void decode(std::string fileName) override;
};
void AviFile::decode(std::string fileName)
{
std::cout << "avi file decode: " + fileName << std::endl;
}
// 具体实现化角色
class Mp4File : public VideoFile
{
public:
void decode(std::string fileName) override;
};
void Mp4File::decode(std::string fileName)
{
std::cout << "mp4 file decode: " + fileName << std::endl;
}
在抽象化角色类中定义了一个实现类接口类型的成员对象 videoFile,再通过注入的方式给该对象赋值。
// 抽象化角色
class Player
{
private:
VideoFile * videoFile;
public:
Player(VideoFile* videoFile);
virtual void play(std::string filaName) = 0;
VideoFile* getVideoFile(void);
void setVideoFile(VideoFile* videoFile);
};
Player::Player(VideoFile* videoFile) : videoFile(videoFile) {}
VideoFile * Player::getVideoFile(void)
{
return videoFile;
}
void Player::setVideoFile(VideoFile* videoFile)
{
this->videoFile = videoFile;
}
扩展抽象化角色实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
// 扩展抽象化角色
class WindowsPlayer : public Player
{
public:
using Player::Player;
void play(std::string filaName) override;
};
void WindowsPlayer::play(std::string fileName)
{
getVideoFile()->decode(fileName);
}
// 扩展抽象化角色
class LinuxPlayer : public Player
{
public:
using Player::Player;
void play(std::string filaName) override;
};
void LinuxPlayer::play(std::string fileName)
{
getVideoFile()->decode(fileName);
}
main() 函数:
#include <iostream>
int main(void)
{
Mp4File * mp4 = new Mp4File();
AviFile * avi = new AviFile();
Player * windowPlayer = new WindowsPlayer(mp4);
windowPlayer->play("魔卡少女樱.mp4");
windowPlayer->setVideoFile(avi);
windowPlayer->play("狐妖小红娘.avi");
Player * linuxPlayer = new LinuxPlayer(mp4);
linuxPlayer->play("某科学的超电磁炮.mp4");
linuxPlayer->setVideoFile(avi);
linuxPlayer->play("关于我转生变成史莱姆这档事.avi");
delete mp4;
delete avi;
delete windowPlayer;
delete linuxPlayer;
return 0;
}
三、桥接模式的总结
3.1、桥接模式的优点
- 分离抽象接口及其实现部分。桥接模式使用 “对象间的关联关系” 解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化(即抽象和实现不再在同一个继承层次结构中,而是 “子类化” 它们,使它们各自都具有自己的子类,以便任意组合子类,从而获得多维度组合对象)。
- 在很多情况下,桥接模式可以取代多层继承方案。多层继承方案违背了单一职责原则,复用性较差,且类的个数非常多。桥接模式是比多层继承方案更好的解决方法,它极大地减少了子类的个数。
- 桥接模式提高了系统的可扩展性。在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合开闭原则。
3.2、桥接模式的缺点
- 桥接模式的使用会增加系统的理解与设计难度。由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。
3.3、桥接模式的适用场景
- 如果一个系统需要在抽象类和具体类之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- 抽象部分和实现部分可以以继承的方式独立扩展而互不影响,在程序运行时可以动态地将一个抽象类子类的对象和一个实现类子类的对象进行组合,即系统需要对抽象类角色和实现类角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。