学习设计模式第十 - 桥接模式
本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种"多维度的变化"?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?这就要使用Bridge模式。
意图
将抽象部分与实现部分分离,使它们都可以独立的变化。
UML
图1. 桥接模式UML图
参与者
这个模式涉及的类或对象:
-
Abstracion
-
定义"抽象"的接口。
-
维护一个Implementor对象的引用。
-
RefinedAbstraction
-
扩展"抽象"定义的接口。
-
Implementor
-
定义"实现"类型的接口。这个接口不必与"抽象"的接口完全一致,事实上两个接口可以完全不同。一般来说,"实现"接口仅提供一些基元操作,"抽象"基于这些基元定义更高层的操作。
-
ConcreteImplementor
-
给出"实现"接口定义操作的具体实现
适用性
将抽象部分与实现部分分离,使两者可以独立的变化。桥接模式是一个高层架构模式,其主要目标是通过抽象帮助.NET开发者编写更好的代码。桥接模式通过将一些类抽象操作移入接口中来使客户端和服务可以独立变化。通过接口这个抽象使得客户端与实现解耦合。
一个解释桥接模式的经典例子是编写设备驱动。驱动是一个独立操作计算机系统或外部硬件设备的对象。需要认识到的是客户端程序是抽象。每一个驱动实例是适配器模式的一个实现。整个系统,应用程序与驱动一起,表示一个桥接的实例。
在以下的情况下应当使用桥梁模式:
-
如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的联系。从而可以达到如使实现部分应可以被选择或者切换这样的效果。
-
设计要求实现化角色的任何改变不应当影响客户端,或者说实现化角色的改变对客户端是完全透明的。这样实现修改后客户的代码不必重新编译。
-
一个组件有多于一个的抽象化角色和实现化角色,系统需要它们之间进行动态耦合。
-
虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
-
类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时 Bridge 模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
-
你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。
DoFactory GoF代码
这段代码演示了将接口与实现相分离,符合抽象的实现可以在不改动客户端的情况下进行改进。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | // Bridge pattern // Structural example using System; namespace DoFactory.GangOfFour.Bridge.Structural { // MainApp test application class MainApp { static void Main() { Abstraction ab = new RefinedAbstraction(); // Set implementation and call ab.Implementor = new ConcreteImplementorA(); ab.Operation(); // Change implemention and call ab.Implementor = new ConcreteImplementorB(); ab.Operation(); // Wait for user Console.ReadKey(); } } // "Abstraction" class Abstraction { protected Implementor implementor; // Property public Implementor Implementor { set { implementor = value; } } public virtual void Operation() { implementor.Operation(); } } // "Implementor" abstract class Implementor { public abstract void Operation(); } // "RefinedAbstraction" class RefinedAbstraction : Abstraction { public override void Operation() { implementor.Operation(); } } // "ConcreteImplementorA" class ConcreteImplementorA : Implementor { public override void Operation() { Console.WriteLine( "ConcreteImplementorA Operation" ); } } // "ConcreteImplementorB" class ConcreteImplementorB : Implementor { public override void Operation() { Console.WriteLine( "ConcreteImplementorB Operation" ); } } } |
这个例子演示了使用桥接模式将业务对象的抽象与数据对象的实现相分离。数据对象可以在不改变客户端调用代码的情况下进行改进。
例子中涉及到的类与桥接模式中标准的类对应关系如下:
-
Abstracion - BusinessObject
-
RefinedAbstraction - CustomersBusinessObject
-
Implementor - DataObject
-
ConcreteImplementor - CustomersDataObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | // Bridge pattern //Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Bridge.RealWorld { // MainApp test application class MainApp { static void Main() { // Create RefinedAbstraction var customers = new Customers(); // Set ConcreteImplementor customers.Data = new CustomersData( "Chicago" ); // Exercise the bridge customers.Show(); customers.Next(); customers.Show(); customers.Next(); customers.Show(); customers.Add( "Henry Velasquez" ); customers.ShowAll(); // Wait for user Console.ReadKey(); } } // "Abstraction" class CustomersBase { private DataObject _dataObject; public DataObject Data { set { _dataObject = value; } get { return _dataObject; } } public virtual void Next() { _dataObject.NextRecord(); } public virtual void Prior() { _dataObject.PriorRecord(); } public virtual void Add( string customer) { _dataObject.AddRecord(customer); } public virtual void Delete( string customer) { _dataObject.DeleteRecord(customer); } public virtual void Show() { _dataObject.ShowRecord(); } public virtual void ShowAll() { _dataObject.ShowAllRecords(); } } // "RefinedAbstraction" class Customers : CustomersBase { public override void ShowAll() { // Add separator lines Console.WriteLine(); Console.WriteLine( "------------------------" ); base .ShowAll(); Console.WriteLine( "------------------------" ); } } // "Implementor" abstract class DataObject { public abstract void NextRecord(); public abstract void PriorRecord(); public abstract void AddRecord( string name); public abstract void DeleteRecord( string name); public abstract string GetCurrentRecord(); public abstract void ShowRecord(); public abstract void ShowAllRecords(); } // "ConcreteImplementor" class CustomersData : DataObject { private List< string > _customers = new List< string >(); private int _current = 0; private string _city; public CustomersData( string city) { _city = city; // Loaded from a database _customers.Add( "Jim Jones" ); _customers.Add( "Samual Jackson" ); _customers.Add( "Allen Good" ); _customers.Add( "Ann Stills" ); _customers.Add( "Lisa Giolani" ); } public override void NextRecord() { if (_current <= _customers.Count - 1) { _current++; } } public override void PriorRecord() { if (_current > 0) { _current--; } } public override void AddRecord( string customer) { _customers.Add(customer); } public override void DeleteRecord( string customer) { _customers.Remove(customer); } public override string GetCurrentRecord() { return _customers[_current]; } public override void ShowRecord() { Console.WriteLine(_customers[_current]); } public override void ShowAllRecords() { Console.WriteLine( "Customer City: " + _city); foreach ( string customer in _customers) { Console.WriteLine( " " + customer); } } } } |
在.NET优化版本的示例中,数据访问对象的抽象类被使用接口替换,因为其不包含实现代码,另外也使用了对象初始化器和集合初始化器等C#3.0的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | // Bridge pattern // .NET Optimized example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Bridge.NETOptimized { class MainApp { static void Main() { // Create RefinedAbstraction var customers = new Customers(); // Set ConcreteImplementor customers.DataObject = new CustomersData { City = "Chicago" } ; // Exercise the bridge customers.Show(); customers.Next(); customers.Show(); customers.Next(); customers.Show(); customers.Add( "Henry Velasquez" ); customers.ShowAll(); // Wait for user Console.ReadKey(); } } // "Abstraction" class CustomersBase { // Gets or sets data object public IDataObject< string > DataObject { get ; set ; } public virtual void Next() { DataObject.NextRecord(); } public virtual void Prior() { DataObject.PriorRecord(); } public virtual void Add( string name) { DataObject.AddRecord(name); } public virtual void Delete( string name) { DataObject.DeleteRecord(name); } public virtual void Show() { DataObject.ShowRecord(); } public virtual void ShowAll() { DataObject.ShowAllRecords(); } } // "RefinedAbstraction" class Customers : CustomersBase { public override void ShowAll() { // Add separator lines Console.WriteLine(); Console.WriteLine( "------------------------" ); base .ShowAll(); Console.WriteLine( "------------------------" ); } } // "Implementor" interface IDataObject<T> { void NextRecord(); void PriorRecord(); void AddRecord(T t); void DeleteRecord(T t); T GetCurrentRecord(); void ShowRecord(); void ShowAllRecords(); } // "ConcreteImplementor" class CustomersData : IDataObject< string > { // Gets or sets city public string City { get ; set ; } private List< string > _customers; private int _current = 0; // Constructor public CustomersData() { // Simulate loading from database _customers = new List< string > { "Jim Jones" , "Samual Jackson" , "Allan Good" , "Ann Stills" , "Lisa Giolani" }; } public void NextRecord() { if (_current <= _customers.Count - 1) { _current++; } } public void PriorRecord() { if (_current > 0) { _current--; } } public void AddRecord( string customer) { _customers.Add(customer); } public void DeleteRecord( string customer) { _customers.Remove(customer); } public string GetCurrentRecord() { return _customers[_current]; } public void ShowRecord() { Console.WriteLine(_customers[_current]); } public void ShowAllRecords() { Console.WriteLine( "Customer Group: " + City); _customers.ForEach(customer => Console.WriteLine( " " + customer)); } } } |
桥接模式解说
在创建型模式里面,我曾经提到过抽象与实现,抽象不应该依赖于具体实现细节,实现细节应该依赖于抽象。看下面这幅图:
图2. 抽象不应该依赖于实现细节
在这种情况下,如果抽象B稳定,而实现细节b变化,这时用创建型模式来解决没有问题。但是如果抽象B也不稳定,也是变化的,该如何解决?这就要用到Bridge模式了。
我们仍然用日志记录工具这个例子来说明Bridge模式。现在我们要开发一个通用的日志记录工具,它支持数据库记录DatabaseLog和文本文件记录FileLog两种方式,同时它既可以运行在.NET平台,也可以运行在Java平台上。
根据我们的设计经验,应该把不同的日志记录方式分别作为单独的对象来对待,并为日志记录类抽象出一个基类Log出来,各种不同的日志记录方式都继承于该基类:
图3. Log类结构图
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public abstract class Log { public abstract void Write( string log); } public class DatabaseLog : Log { public override void Write( string log) { //......Log Database } } public class TextFileLog : Log { public override void Write( string log) { //......Log Text File } } |
另外考虑到不同平台的日志记录,对于操作数据库、写入文本文件所调用的方式可能是不一样的,为此对于不同的日志记录方式,我们需要提供各种不同平台上的实现,对上面的类做进一步的设计得到了下面的结构图:
图4. 功能复杂的Log类结构图
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class NDatabaseLog : DatabaseLog { public override void Write( string log) { //......(.NET平台)Log Database } } public class JDatabaseLog : DatabaseLog { public override void Write( string log) { //......(Java平台)Log Database } } public class NTextFileLog : TextFileLog { public override void Write( string log) { //......(.NET平台)Log Text File } } public class JTextFileLog : TextFileLog { public override void Write( string log) { //......(Java平台)Log TextFile } } |
现在的这种设计方案本身是没有任何错误的,假如现在我们要引入一种新的xml文件的记录方式,则上面的类结构图会变成:
图5.功能进一步复杂的Log类结构图
如图中蓝色的部分所示,我们新增加了一个继承于Log基类的子类,而没有修改其它的子类,这样也符合了开放-封闭原则。如果我们引入一种新的平台,比如说我们现在开发的日志记录工具还需要支持Borland平台,此时该类结构又变成了:
图6.功能进一步复杂的Log类结构图2
同样我们没有修改任何的东西,只是增加了两个继承于DatabaseLog和TextFileLog的子类,这也符合了开放-封闭原则。
但是我们说这样的设计是脆弱的,仔细分析就可以发现,它还是存在很多问题,首先它在遵循开放-封闭原则的同时,违背了类的单一职责原则,即一个类只有一个引起它变化的原因,而这里引起Log类变化的原因却有两个,即日志记录方式的变化和日志记录平台的变化;其次是重复代码会很多,不同的日志记录方式在不同的平台上也会有一部分的代码是相同的;再次是类的结构过于复杂,继承关系太多,难于维护,最后最致命的一点是扩展性太差。上面我们分析的变化只是沿着某一个方向,如果变化沿着日志记录方式和不同的运行平台两个方向变化,我们会看到这个类的结构会迅速的变庞大。
现在该是Bridge模式粉墨登场的时候了,我们需要解耦这两个方向的变化,把它们之间的强耦合关系改成弱联系。我们把日志记录方式和不同平台上的实现分别当作两个独立的部分来对待,对于日志记录方式,类结构图仍然是:
图7.引入桥接模式的Log类
现在我们引入另外一个抽象类ImpLog,它是日志记录在不同平台的实现的基类,结构图如下:
图8.引入桥接模式的ImpLog类
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public abstract class ImpLog { public abstract void Execute( string msg); } public class NImpLog : ImpLog { public override void Execute( string msg) { //...... .NET平台 } } public class JImpLog : ImpLog { public override void Execute( string msg) { //...... Java平台 } } |
这时对于日志记录方式和不同的运行平台这两个类都可以独立的变化了,我们要做的工作就是把这两部分之间连接起来。那如何连接呢?在这里,Bridge使用了对象组合的方式,类结构图如下:
图9.桥接模式中将Log类与ImpLog类结合起来
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public abstract class Log { protected ImpLog implementor; public ImpLog Implementor { set { implementor = value; } } public virtual void Write( string log) { implementor.Execute(log); } } public class DatabaseLog : Log { public override void Write( string log) { implementor.Execute(log); } } public class TextFileLog : Log { public override void Write( string log) { implementor.Execute(log); } } |
可以看到,通过对象组合的方式,Bridge模式把两个角色之间的继承关系改为了耦合的关系,从而使这两者可以从容自若的各自独立的变化,这也是Bridge模式的本意。再来看一下客户端如何去使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class App { public static void Main( string [] args) { //.NET平台下的Database Log Log dblog = new DatabaseLog(); dblog.Implementor = new NImpLog(); dblog.Write(); //Java平台下的Text File Log Log txtlog = new TextFileLog(); txtlog.Implementor = new JImpLog(); txtlog.Write(); } } |
可能有人会担心说,这样不就又增加了客户程序与具体日志记录方式之间的耦合性了吗?其实这样的担心是没有必要的,因为这种耦合性是由于对象的创建所带来的,完全可以用创建型模式去解决,就不是这里我们所讨论的内容了。
最后我们再来考虑一个问题,为什么Bridge模式要使用对象组合的方式而不是用继承呢?如果采用继承的方式,则Log类,ImpLog类都为接口,类结构图如下:
图10.使用继承方式的桥接模式
实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class NDatabaseLog : DatabaseLog, IImpLog { //...... } public class JDatabaseLog : DatabaseLog, IImpLog { //...... } public class NTextFileLog : TextFileLog, IImpLog { //...... } public class JTextFileLog : TextFileLog, IImpLog { //...... } |
如上图中蓝色的部分所示,它们既具有日志记录方式的特性,也具有接口IimpLog的特性,它已经违背了面向对象设计原则中类的单一职责原则,一个类应当仅有一个引起它变化的原因。所以采用Bridge模式往往是比采用多继承更好的方案。说到这里,大家应该对Bridge模式有一些认识了吧?如果在开发中遇到有两个方向上纵横交错的变化时,应该能够想到使用Bridge模式,当然了,有时候虽然有两个方向上的变化,但是在某一个方向上的变化并不是很剧烈的时候,并不一定要使用Bridge模式。
.NET中的桥接模式
桥接模式是一个高层架构模式,所以没有在.NET类库本省有体现。虽然很多开发者没有意识到,但他们一直在使用这个模式。如果你构建一个通过驱动与数据库通信的应用,例如,通过ODBC,那么你正在使用桥接模式。ODBC是一个用于执行SQL语句的标准API,在桥接模式中它表示抽象接口。实现这些API的类正是ODBC驱动。构建在这些驱动上的应用程序也是通过抽象与任意提供ODBC驱动的数据库(SQL Server,Oracle,DB2等)协同工作。ODBC架构将抽象与实现解耦合使两者可以独立变化 – 桥接模式正在起作用。
效果及实现要点
-
Bridge模式使用"对象间的组合关系"解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。
-
所谓抽象和实现沿着各自维度的变化,即"子类化"它们,得到各个子类之后,便可以任意它们,从而获得不同平台上的不同型号。
-
Bridge模式有时候类似于多继承方案,但是多继承方案往往违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
-
Bridge模式的应用一般在"两个非常强的变化维度",有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换言之两个变化不会导致纵横交错的结果,并不一定要使用Bridge模式。
总结
Bridge模式是一个非常有用的模式,也非常复杂,它很好的符合了开放-封闭原则和优先使用对象,而不是继承这两个面向对象原则。对于其定义"将抽象部分与它的实现部分分离",一个很好的解释:实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异