一、基础概念
桥接模式的本质是【分离抽象和实现】。
桥接模式的定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
序号 | 认识桥接模式 | 说明 |
1 | 什么是桥接 | 通俗点说就是在不同的东西之间搭一个桥,让它们能够连接起来,可以相互通讯和使用。在桥接模式中是给什么东西搭桥呢?【是为被分离了的抽象部分和实现部分来搭桥】 注意:在桥接模式中桥接是单向的,也就是只能是抽象部分的对象去使用具体实现部分的对象,而不能反过来,这就是单向桥。 |
2 | 为何需要桥接 | 为了达到让抽象部分和实现部分都可以独立变化的目的,在桥接模式中,是把抽象部分和实现部分分离开来。 虽然从程序结构上是分开了,但是抽象部分实现的时候,还是需要使用具体的实现,这可怎么办?【抽象部分如何才能调用到具体实现部分的功能呢?】搭个桥就可以了,让抽象部分通过这个桥就可以调用到实现部分的功能了,因此需要桥接。 |
3 | 如何桥接 | 只要让抽象部分拥有实现部分的接口对象,就桥接上了,在抽象部分即可通过这个接口来调用具体实现部分的功能(即:桥接在程序上体现了在抽象部分拥有实现部分的接口对象,维护桥接就是维护这个关系)。 |
4 | 独立变化 | 桥接模式的意图是使得抽象和实现可以独立变化,都可以分别扩充。也就是说抽象部分和实现部分是一种非常松散的关系。从某个角度来讲,抽象部分和实现部分是可以完全分开的,独立的,抽象部分不过是一个使用实现部分对外接口的程序罢了。 如果这么看桥接模式的话,就类似于策略模式了。抽象部分需要根据某个策略,来选择真实的实现,也就是说桥接模式的抽象部分相当于策略模式的上下文,更原始的就直接类似于面向接口编程,通过接口分离的两个部分而已。但是别忘了,桥接模式的抽象部分,是可以继续扩展和变化的,而策略模式只有上下文,是不存在所谓抽象部分的。 抽象和实现为什么还要组合在一起呢?原因是在抽象部分和实现部分还是存在内部联系的,抽象部分的实现通常是需要调用实现部分的功能来实现的。 |
5 | 动态变换功能 | 由于桥接模式中的抽象部分和实现部分是完全分离的,因此可以在运行时动态组合具体的真实实现,从而达到动态变换功能的目的。 从另外一个角度看,抽象部分和实现部分没有固定的绑定关系,因此同一个真实实现可以被不同的抽象对象使用;反过来,同一个抽象也可以有多个不同的实现。 |
6 | 退化的桥接模式 | 如果接口仅有一个实现,那么就没有必要创建接口了,这是一种桥接模式退化的情况(即:抽象类和接口是一对一的关系,虽然如此,但还是要保持它们的分离状态,这样,它们才不会相互影响,才可以分别扩展) |
7 | 桥接模式和继承 | 继承是扩展对象功能的一种常见手段,通常情况下,继承扩展的功能变化纬度都是一纬的,也就是变化的因素只有一类。 对于出现变化因素有两类:也就是有两个变化纬度的情况,继承实现就会比较痛苦。从理论上来说,如果用继承的方式来实现这种有两个变化纬度的情况,最后实际的实现类应该是两个维度上可变数量的乘积那么多个。如果要在任何一个纬度上进行扩展,都需要实现另外一个纬度上的可变数量那么多个实现类,这也是为何会感觉扩展起来很困难;且随着程序规模的加大,会越来越难以扩展和维护。 【桥接模式】就是用来解决这种有两个变化纬度的情况下,如果灵活地扩展功能的一个很好的方案,其实,桥接模式主要是把继承改成了使用对象组合,从而把两个维度分开,让每一个纬度单独去变化,最后通过对象组合的方式,把两个维度组合起来,每一种组合的方式就相当于原来继承中的一种实现,这样就有效地减少了实际实现的类的个数【理论上,如果使用桥接模式的方式来实现这种有两种变化纬度的情况,最后实际的实现类应该是两个纬度上可变数量的和】。 |
序号 | 谁来桥接说明 |
1 | 由客户端来负责创建接口对象,并在创建抽象类对象的时候,把它设置到抽象部分的对象中去。 |
2 | 可以在抽象部分对象构建的时候,由抽象部分的对象自己来创建相应的接口对象,也可以给它传递一些参数,根据参数来选择并创建具体的接口对象。 |
3 | 可以在抽象类中选择并创建一个默认的接口对象,然后子类可以根据需要改变这个实现。 |
4 | 也可以使用抽象工厂或者简单工厂来选择并创建具体的接口对象,抽象部分的类可以通过调用工厂的方法来获取接口对象。 |
5 | 如果使用IOC/DI容器的话,还可以通过IOC/DI容器来创建具体的接口对象,并注入会到抽象类中。 |
序号 | 桥接模式的优点 |
1 | 分离抽象和实现部分:桥接模式分离了抽象部分和实现部分,从而及大地提高了系统的灵活性。让抽象部分和实现部分独立开来,分别定义接口,这有助于对系统进行分层,从而产生更好的结构化的系统。对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了。 |
2 | 更好的扩展性:由于桥接模式把抽象部分和实现部分分离开了,而且分别定义接口,这就使得抽象部分和实现部分可以分别独立地扩展,而不会相互影响,从而大大提高了系统的可扩展性。 |
3 | 可动态地切换实现:由于桥接模式把抽象部分和实现部分分离开了,所以在实现桥接的时候,就可以实现动态的选择和使用具体的实现。也就是说一个实现不再是固定的绑定在一个抽象接口上了,可以实现在运行期间动态地切换。 |
4 | 可以减少子类的个数:对于两个变化纬度的情况,如果采用继承的实现方式,大约需要在两个纬度上的可变化数量的乘积个子类;而采用桥接模式来实现,大约需要两个纬度上的可变化数量的和个子类。可以明显的减少子类的个数。 |
序号 | 说明 |
1 | 桥接模式的本质是:分离抽象和实现;桥接模式最重要的工作就是分离抽象部分和实现部分,这是解决问题的关键。只有把抽象部分和实现部分分离开了,才能够让它们独立地变化;只有抽象部分和实现部分可以独立地变化,系统才会有更好的可扩展性和可维护性(还有其他好处如:可以动态地切换实现、可以减少子类个数等)。 |
2 | 对设计原则的体现: 《1》桥接模式很好地实现了开闭原则(通常应用桥接模式的地方,抽象部分和实现部分都是可变化的,也就是应用会有两个变化纬度,桥接模式就是找到这两个变化,并分别封装起来,从而合理地实现OCP)。在使用桥接模式的时候,通常情况下,顶层的抽象类和接口是不变的,而继承抽象类的具体类是可变的。由于抽象类是通过接口来操作具体的实现类,因此具体的实现类是可以扩展的,并根据需要可以有多个具体的实现。 |
何时选用桥接模式?
1、如果不希望在抽象部分和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象部分和实现部分分开,然后在程序运行期间来动态地设置抽象部分需要用到的具体的实现,还可以动态地切换具体的实现。
2、如果出现抽象部分和实现部分都能够扩展的情况,可以采用桥接模式,让抽象部分和实现部分独立地变化,从而灵活地进行单独扩展,而不是搅在一起,扩展一边会影响到另一边。
3、如果希望实现部分的修改不会对客户产生影响,可以采用桥接模式。由于客户是面向抽象的接口在运行,实现部分的修改可以独立于抽象部分,并不会对客户产生影响,也可以说对客户是透明的。
4、如果采用继承的方案,会导致产生很多子类,对于这种情况,可以考虑采用桥接模式,分析功能变化的原因,看看能否分离不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目。
二、桥接模式示例
业务需求:发送提示消息(如某人有新的工作了,需要发送一条消息提示他)。从业务上看,消息由分为普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的。如:加急消息是在消息上添加加急,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成就会继续催促;从发送消息的手段上看:有系统内消息、手机短信消息、邮件消息等。
2.1、不使用模式的示例
2.1.1、实现简化版本
我们先实现一个简单版本(如:消息只是实现发送普通消息,发送方式只是实现系统内消息和邮件)由于发送普通消息会有两种不同的实现方式,为了让外部能够统一操作,因此,把消息设计为接口,然后由两个不同的实现类分别实现系统内消息方式和邮件发送消息方式。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 消息统一接口/// </summary>internal interface IMessage{/// <summary>/// 发送消息/// </summary>/// <param name="message">需要发生的消息</param>/// <param name="toUser">消息要发送给的人员</param>void Send(string message,string toUser);}//Interface_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 站内消息的方式发送普通消息/// </summary>internal class CommonMsgSMS : IMessage{public void Send(string message, string toUser){Console.WriteLine($"现在使用【站内消息方式】发送消息【{message}】给【{toUser}】");}}//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 使用Email的方式发送普通消息/// </summary>internal class CommonMsgEmail : IMessage{public void Send(string message, string toUser){Console.WriteLine($"现在使用【Email方式】发送消息【{message}】给【{toUser}】");}}//Class_end
}
2.1.2、实现加急发送消息
加急发送消息的实现不同于普通消息,需要在消息前加上加息,然后在发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息的处理进度(如:相应的人员是否接收到这个消息,相应的工作是否已经开展等)因此加急消息需要扩展新的接口,除了实现基本的发送消息功能外,还需要添加监控功能:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 加急消息接口/// </summary>internal interface IUrgencyMessage:IMessage{/// <summary>/// 监控某消息的处理/// </summary>/// <param name="messageId">被监控消息的编号</param>/// <returns>返回监控到的数据对象</returns>object Watch(string messageId);}//Interface_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 站内消息的方式发送加急消息/// </summary>internal class UrgencyMsgSMS : IUrgencyMessage{public void Send(string message, string toUser){message = $"[加急] {message}";Console.WriteLine($"现在使用【站内消息方式】发送消息【{message}】给【{toUser}】");}public object Watch(string messageId){//获取相应的数据,组织成为监控的数据对象,然后返回return null;}}//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.NoPattern
{/// <summary>/// 使用Email的方式发送加急消息/// </summary>internal class UrgencyMsgEmail : IUrgencyMessage{public void Send(string message, string toUser){message = $"[加急] {message}";Console.WriteLine($"现在使用【Email方式】发送消息【{message}】给【{toUser}】");}public object Watch(string messageId){//获取相应的数据,组织成为监控的数据对象,然后返回return null;}}//Class_end
}
2.1.3、客户端测试
namespace BridgePattern
{internal class Program{static void Main(string[] args){NoPatternTest();Console.ReadLine();}/// <summary>/// 不使用模式的示例/// </summary>private static void NoPatternTest(){Console.WriteLine("------不使用模式的示例------");//使用站内消息方式发送【普通】消息NoPattern.IMessage message = new NoPattern.CommonMsgSMS();message.Send("请你吃饭", "张三");//使用站内消息方式发送【加急】消息message = new NoPattern.UrgencyMsgSMS();message.Send("请你吃饭", "张三");Console.WriteLine();//使用邮件的方式发送【普通】消息message = new NoPattern.CommonMsgEmail();message.Send("请你吃饭", "张三");//使用邮件的方式发送【加急】消息message = new NoPattern.UrgencyMsgEmail();message.Send("请你吃饭", "张三");}}//Class_end
}
2.1.4、运行结果
这个示例是满足了基本的功能要求,可是这么实现好不好呢?有没有什么问题呢?
通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易;不同类型的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类都要加入这种新的发送方式的实现。要是考虑业务功能上再扩展一下呢?(如:群发消息,也就是一次可以发送多条消息)就意味着很多地方都要修改,这样的实现很明显是不灵活的。
2.2、桥接模式示例
桥接模式就是用来解决上述问题的,将抽象部分与它的实现部分分离,使得它们都可以独立的变化。仔细分析上面的示例要求,示例的变化具有两个纬度,一个纬度是抽象的消息(包含普通消息、加急消息、特急消息);另一个纬度是具体的消息发送方式(包含:站内消息、Email消息、手机短信消息)这几个方式是平等的,可被切换方式。这两个纬度一共组合出9种可能性,如下图所示:
出现问题的根本原因是:在与消息的抽象和实现是混合在一起的,这就导致了一个纬度的变化会引起另一个纬度进行相应的变化,从而使得程序扩展起来非常困难。要想解决这个问题,就必须把这两个纬度分开(即:将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单)。
桥接模式通过引入实现的接口,把实现部分从系统中分离出去。那么,抽象这边如何使用具体的实现呢?肯定是用面向实现的接口来编程,为了让抽象这边能够很方便地与实现结合起来,把顶层的抽象接口改成抽象类,在其中持有一个具体的实现部分的实例。这样一来,对于需要发送消息的客户端来说,只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正消息发送法师来把消息发送出去(也就是说:客户端只是想要发送消息而已,并不想关心具体如何发送)。
2.2.1、实现简单功能
我们先从简单的功能开始,实现普通消息和加急消息功能。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 发送消息的统一接口/// </summary>internal interface IMessage{/// <summary>/// 发送消息/// </summary>/// <param name="message">要发送的消息内容</param>/// <param name="toUser">消息发送给的人员</param>void Send(string message,string toUser);}//Interface_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 抽象的消息对象/// </summary>internal class AbstractMessage{//持有一个实现消息的对象protected IMessage message;/// <summary>/// 构造函数/// </summary>/// <param name="message">实现消息的对象</param>public AbstractMessage(IMessage message){this.message = message;}/// <summary>/// 发送消息/// </summary>/// <param name="message">需要发送的消息内容</param>/// <param name="toUser">消息发送的给的人员</param>public virtual void SendMsg(string message,string toUser){this.message.Send(message, toUser);}}//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 站内消息/// </summary>internal class MsgSMS : IMessage{public void Send(string message, string toUser){Console.WriteLine($"使用站内消息的方式,发送消息【{message}】给【{toUser}】");}}//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 使用Email方式发送消息/// </summary>internal class MsgEmail : IMessage{public void Send(string message, string toUser){Console.WriteLine($"使用Email消息的方式,发送消息【{message}】给【{toUser}】");}}//Class_end
}
接下来就是扩展抽象消息接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 普通消息/// </summary>internal class CommonMsg : AbstractMessage{public CommonMsg(IMessage message) : base(message){}public override void SendMsg(string message, string toUser){//【普通消息】直接调用父类方法,把消息发送出去就可以了base.SendMsg(message, toUser);}}//Class_end
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 加急消息/// </summary>internal class UrgencyMsg : AbstractMessage{public UrgencyMsg(IMessage message) : base(message){}public override void SendMsg(string message, string toUser){message = $"[加急] {message}";base.SendMsg(message, toUser);}/// <summary>/// 扩展新功能(监控消息的处理过程)/// </summary>/// <param name="messageId">被监控的消息的编号</param>/// <returns>返回被监控到的数据对象</returns>public object Watch(string messageId){//获取相应的数据,组织成监控的数据对象,然后返回return null;}}//Class_end
}
2.2.2、添加新功能
上面已经使用桥接模式实现了2种消息发送方式和2种消息类型消息;现在来看一下能够解决前面提出的问题,我们通过新添加还未实现的功能来看看(即:新添加特急消息处理;新增加使用手机发送消息的方式)该如何实现?
我们只需要在抽象部分新添加一个特急消息类,扩展抽象消息就可以把特急消息的处理功能加入系统中了;对于新增手机发送消息的方式也简单,只需要在新增一个类实现手机发送消息的方式即可。
《1》新增特急消息处理类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 特急消息/// </summary>internal class SpecialUrgencyMsg : AbstractMessage{public SpecialUrgencyMsg(IMessage message) : base(message){}public override void SendMsg(string message, string toUser){message = $"[特急] {message}";base.SendMsg(message, toUser);}public void Hurry(string messageId){//执行催促的业务,发出催促消息}}//Class_end
}
《2》新增的手机发送消息方式功能
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 手机短信的方式发送消息/// </summary>internal class MsgMobile : IMessage{public void Send(string message, string toUser){Console.WriteLine($"使用手机短信的方式,发送消息【{message}】给【{toUser}】");}}//Class_end
}
2.2.3、客户端测试
namespace BridgePattern
{internal class Program{static void Main(string[] args){BridgeDemoOneTest();Console.ReadLine();}/// <summary>/// 桥接模式示例1/// </summary>private static void BridgeDemoOneTest(){Console.WriteLine("------桥接模式示例1------");/*把发送消息实现方式切换为站内信*///创建具体的实现对象BridgeDemoOne.IMessage messageStyle = new BridgeDemoOne.MsgSMS();//创建一个普通的消息对象BridgeDemoOne.AbstractMessage abstarctMessage=new BridgeDemoOne.CommonMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭","张三");//创建一个紧急消息对象abstarctMessage=new BridgeDemoOne.UrgencyMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭","张三");//创建一个特急消息对象abstarctMessage = new BridgeDemoOne.SpecialUrgencyMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭", "张三");Console.WriteLine();/*把发送消息实现方式切换为邮件*///创建具体的实现对象messageStyle=new BridgeDemoOne.MsgEmail();//创建一个普通的消息对象abstarctMessage = new BridgeDemoOne.CommonMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭", "张三");//创建一个紧急消息对象abstarctMessage = new BridgeDemoOne.UrgencyMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭", "张三");//创建一个特急消息对象abstarctMessage = new BridgeDemoOne.SpecialUrgencyMsg(messageStyle);abstarctMessage.SendMsg("请你吃饭", "张三");}}//Class_end
}
2.2.4、运行结果
2.2.5、谁来桥接
《1》由抽象部分的对象自己来创建相应的对象
这种情况又分为两种实现:一种是需要外部传入参数,另一种是不需要外部传入参数:
①外部传入参数:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 抽象的消息对象/// </summary>internal class AbstractMessage2{//持有一个实现部分的对象protected IMessage message;public AbstractMessage2(int type){switch (type){case 1:message = new MsgSMS();break;case 2:message = new MsgEmail();break;case 3:message = new MsgMobile();break;default:break;}}/// <summary>/// 发送消息/// </summary>/// <param name="message">需要发送的消息内容</param>/// <param name="toUser">消息发送的给的人员</param>public virtual void SendMsg(string message, string toUser){this.message.Send(message, toUser);}}//Class_end
}
②不需要外部传入参数
这种不需要外部传入参数的情况,那就说明在抽象类中,有可能在抽象类的构造函数中选择;也有可能在具体的方法中选择。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{/// <summary>/// 抽象的消息对象/// </summary>internal class AbstractMessage3{//持有一个实现部分的对象protected IMessage message;public AbstractMessage3(){}/// <summary>/// 发送消息/// </summary>/// <param name="message">需要发送的消息内容</param>/// <param name="toUser">消息发送的给的人员</param>public virtual void SendMsg(string message, string toUser){this.message.Send(message, toUser);}//根据消息的长度来选择合适的实现protected IMessage GetImpl(string message){IMessage msg = null;if (string.IsNullOrEmpty(message)){//若没有任何消息则默认使用站内消息msg = new MsgSMS();}else if (message.Length < 100){//如消息长度在100以内,则使用手机短信msg = new MsgMobile();}else if (message.Length < 1000){//如消息长度在100-1000以内,则使用站内消息msg = new MsgSMS();}else{//如消息长度在1000以上,则使用Emailmsg = new MsgEmail();}return msg;}}//Class_end
}
《2》在抽象类的构造函数中创建默认实现对象
直接在抽象类的构造方法中,创建一个默认的实现对象,然后子类根据需要,可以选择直接使用还是覆盖掉。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace BridgePattern.BridgeDemoOne
{internal class AbstractMessage4{//持有一个实现消息的对象protected IMessage message;/// <summary>/// 构造函数/// </summary>public AbstractMessage4(){//创建一个默认的实现this.message = new MsgSMS();}/// <summary>/// 发送消息/// </summary>/// <param name="message">需要发送的消息内容</param>/// <param name="toUser">消息发送的给的人员</param>public virtual void SendMsg(string message, string toUser){this.message.Send(message, toUser);}}//Class_end
}
三、项目源码工程
kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern