动机
- 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个变化的维度。
如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度
举个栗子
我们有一个发送消息的抽象基类
class Messager{
public:
virtual void Login(string name, string password) = 0;
virtual void SendMessage(string message) = 0;
virtual void SendPicture(Image image) = 0;
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
virtual ~Messager() {}
};
在这种业务场景下,我们需要发送消息实现登录,发送文字,发送图片的功能。同时也可能有播放声音等等其他的功能需求。
针对不同的平台,我们需要不同的实现逻辑来完成基础需求。
class PCMessagerBase : public Messager{
public:
virtual void PlaySound(){
//PC平台的实现逻辑
}
virtual void DrawShape(){
//PC平台的实现
}
virtual void WriteText(){
//PC平台的实现
}
virtual void Connect(){
//PC平台的实现
}
};
class MobileMessagerBase : public Messager{
public:
virtual void PlaySound(){
//Mobile平台的实现逻辑
}
virtual void DrawShape(){
//Mobile平台的实现
}
virtual void WriteText(){
//Mobile平台的实现
}
virtual void Connect(){
//Mobile平台的实现
}
};
针对具体的不同业务场景,我们需要有不同的逻辑。比如说,我们需要一种精简版的逻辑,同时需要一种完美版的逻辑,类似针对非会员和会员的不同处理😀
//业务逻辑
class PCMessagerLite : public PCMessagerBase{
public:
virtual void Login(string name, string password){
PCMessagerBase::Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message){
PCMessagerBase::WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image){
PCMessagerBase::DrawShape(); //发送图片前对图片的处理等等
//...
}
};
class PCMessagerPerfect : public PCMessagerBase{
public:
virtual void Login(string name, string password){
PCMessagerBase::PlaySound();
PCMessagerBase::Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message){
PCMessagerBase::PlaySound();
PCMessagerBase::WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image){
PCMessagerBase::PlaySound();
PCMessagerBase::DrawShape(); //发送图片前对图片的处理等等
//...
}
};
perfect版本在lite版本的基础上,可能有一些其他的行为,比如说播放一段音乐等等。
//业务逻辑
class MobileMessagerLite : public MobileMessagerBase{
public:
virtual void Login(string name, string password){
MobileMessagerBase::Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message){
MobileMessagerBase::WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image){
MobileMessagerBase::DrawShape(); //发送图片前对图片的处理等等
//...
}
};
class MobileMessagerPerfect : public MobileMessagerBase{
public:
virtual void Login(string name, string password){
MobileMessagerBase::PlaySound();
MobileMessagerBase::Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message){
MobileMessagerBase::PlaySound();
MobileMessagerBase::WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image){
MobileMessagerBase::PlaySound();
MobileMessagerBase::DrawShape(); //发送图片前对图片的处理等等
//...
}
};
在移动端平台上也是一样的。业务流程是一样的,可能有一些实现调用了MobileMessagerBase基类的方法。
存在的问题
现在存在的类的关系是这样的。这样的类中间存在的大量的重复代码,比如PCMessageLite和MobileMessageLite的Login函数中,逻辑明显是一样的,唯一的区别在于调用的基类的Connect函数不同。
这样明显是一种不好的设计。这与我们之前写过的装饰模式中的问题非常类似。C++设计模式 #5 装饰模式(Decorator)-CSDN博客
重构
如果参考装饰模式(Decorator)的方式,我们可以将代码重构成以下这种形式。
class MessagerLite{
Messager* messager; //在运行时 = new PCMessagerBase() 或者 MobileMessagerBase()
public:
virtual void Login(string name, string password) {
messager->Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message) {
messager->WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image) {
messager->DrawShape(); //发送图片前对图片的处理等等
//...
}
};
class MessagerPerfect {
Messager* messager; //在运行时 = new PCMessagerBase() 或者 MobileMessagerBase()
public:
virtual void Login(string name, string password) {
messager->PlaySound();
messager->Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message) {
messager->PlaySound();
messager->WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image) {
messager->PlaySound();
messager->DrawShape(); //发送图片前对图片的处理等等
//...
}
};
看起来这种方法是可行的,但是注意这种方法存在着致命的缺陷。
PCMessagerBase和MobileMessagerBase这两个类,只重载了Messager类中的两个三个方法,另外三个依然是纯虚函数。PCMessagerBase和MobileMessagerBase这两个类依然是纯虚基类,它们是不可以被实例化的,也就是说,我们在运行时是无法初始化MessagerLite的messager指针为PCMessagerBase的。
造成这种问题的原因是,Messager的这些函数放在一个类中,并不合适。我们将代码彻底重构成如下形式
class Messager {
protected:
MessagerImp* messager; //在运行时 = new PCMessagerBase() 或者 MobileMessagerBase()
public:
virtual void Login(string name, string password) = 0;
virtual void SendMessage(string message) = 0;
virtual void SendPicture(Image image) = 0;
virtual ~Messager() {}
};
class MessagerImp {
public:
virtual void PlaySound() = 0;
virtual void DrawShape() = 0;
virtual void WriteText() = 0;
virtual void Connect() = 0;
virtual ~MessagerImp() {}
};
class PCMessagerBase : public MessagerImp {
public:
virtual void PlaySound() {
//PC平台的实现逻辑
}
virtual void DrawShape() {
//PC平台的实现
}
virtual void WriteText() {
//PC平台的实现
}
virtual void Connect() {
//PC平台的实现
}
};
class MobileMessagerBase : public MessagerImp {
public:
virtual void PlaySound() {
//Mobile平台的实现逻辑
}
virtual void DrawShape() {
//Mobile平台的实现
}
virtual void WriteText() {
//Mobile平台的实现
}
virtual void Connect() {
//Mobile平台的实现
}
};
//业务逻辑
class MessagerLite : public Messager{
public:
virtual void Login(string name, string password) {
messager->Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message) {
messager->WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image) {
messager->DrawShape(); //发送图片前对图片的处理等等
//...
}
};
class MessagerPerfect : public Messager{
public:
virtual void Login(string name, string password) {
messager->PlaySound();
messager->Connect(); //在登录前与服务器等等保持连接
//...
}
virtual void SendMessage(string message) {
messager->PlaySound();
messager->WriteText(); //在发送消息前进行消息的输入
//...
}
virtual void SendPicture(Image image) {
messager->PlaySound();
messager->DrawShape(); //发送图片前对图片的处理等等
//...
}
};
当前类的关系是这样的,我们成功的将业务上的扩展(MessagerLite/MessagerPerfect)与平台上的扩展(PCMessagerBase/MobileMessagerBase)两个方向上分开。用(n+m)数量的类,实现了(n*m)的功能。
模式定义
- 将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。——《设计模式》GoF
同样红色的部分是稳定的,蓝色的部分是变化的。
体现在我们上面的代码中,就是Messager类与MessagerImp类中间搭了一座桥。使得业务功能与平台扩展两个方向上可以分开变化。
总结
- 桥模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度变化。所谓抽象和实现沿着各自维度变化,即“子类化”
- 桥模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性较差。桥模式是比多继承方案更好的解决办法。
- 桥模式一般应用于“两个非常强的变化维度”,有时一个类有多于两个的维度变化,这时也可以使用桥模式的扩展模式。