桥接模式
桥接模式要把握的很重要的一点就是:类的继承关系和类的组合/聚合关系,何时应该考虑使用何种关系。是不是在编程过程中一味地使用类的继承关系就代表这就是面向对象编程了?有时候并不是这样,Java的类继承设计成单继承模式我想应该就是不想把类的继承关系搞得过于复杂,实际上我们应该优先使用对象组合/聚合,而不是类继承。类继承不必我们多说,我们来看看何为组合/聚合关系。
当我们看到上图过后应该能明白什么是组合和聚合了。聚合体现的是“弱”的拥有关系,比如雁群可以包含大雁,但雁群不是大雁的一部分。组合体现的是“强”的拥有关系,或者体现的是部分与整体的关系,通过一对翅膀组合成大雁,翅膀是部分,大雁是整体。
在了解了什么是组合/聚合过后,我们来看看什么是桥接模式。同样我们通过《大话设计模式》书中的例子来说明。
在N多年前手机还未像现在的手机市场一样,由Android和iOS一统天下。N年前各个手机厂商的软件几乎是互不兼容,更严重的可能是同一个手机厂商不同型号的手机也互不兼容。如果我们考虑这种场景应该如何来设计我们的代码呢?我们通过手机品牌来分类。
这就是我们通过类继承造成的结果。如果现在需要新增一个手机品牌S的话,就得再增加手机品牌S的下属功能子类,手机品牌千千万万那要增加的类就不计其数。我们再换一种通过软件分类来呢?
其实这不也一样吗?其实质还是通过类的继承来实现。新增一个功能过后同样要新增N个手机品牌对应的软件。从上面两种设计中,其实我们可以发现可以将“手机”这个抽象的概念将其剥离为“手机软件”和“手机品牌”这两个实现,具体是怎么做到的呢?我们来看桥接模式下的UML类结构图。
通过UML类结构图我们可以看到手机品牌和手机软件成功解耦,新增功能并不影响其手机品牌,新增手机品牌也不会影响到手机软件,其中的奥秘就在于利用了聚合而不是继承。(但其实,我觉得在这个地方所举的这个例子有失偏颇。从开始我们假定的场景是各个手机品牌互不兼容各自的手机软件,最开始两种“坏”的设计是满足这个场景的,但是所使用的桥接模式实际上更加符合现在Android手机的应用场景。手机硬件厂商只负责生产手机硬件,而软件厂商只负责发布功能软件,通过将软件组合成一个智能手机)
接下来我们还是通过代码来感受一下桥接模式。
根据我们所画的UML类图先写手机软件。
1 package day_7_bridge; 2 3 /** 4 * 手机软件接口 5 * @author 余林丰 6 * 7 * 2016年10月7日 8 */ 9 public interface Software { 10 void run(); 11 }
1 package day_7_bridge; 2 3 /** 4 * 通讯录软件,实现手机软件接口 5 * @author 余林丰 6 * 7 * 2016年10月7日 8 */ 9 public class Contacts implements Software { 10 11 /* (non-Javadoc) 12 * @see day_7_bridge.Software#run() 13 */ 14 @Override 15 public void run() { 16 System.out.println("手机通讯录"); 17 } 18 19 }
1 package day_7_bridge; 2 3 /** 4 * 游戏软件,实现手机软件接口 5 * @author 余林丰 6 * 7 * 2016年10月7日 8 */ 9 public class Game implements Software { 10 11 /* (non-Javadoc) 12 * @see day_7_bridge.Software#run() 13 */ 14 @Override 15 public void run() { 16 System.out.println("游戏软件"); 17 } 18 19 }
我们再来实现手机品牌的实现。
1 package day_7_bridge; 2 3 /** 4 * 手机品牌抽象类 5 * @author 余林丰 6 * 7 * 2016年10月7日 8 */ 9 public abstract class AbstractPhone { 10 protected Software software; 11 12 public void setSoftware(Software software){ 13 this.software = software; 14 } 15 16 public abstract void run(); 17 }
1 package day_7_bridge; 2 3 /** 4 * 手机品牌M 5 * @author 余林丰 6 * 7 * 2016年10月7日 8 */ 9 public class PhoneM extends AbstractPhone { 10 11 /* (non-Javadoc) 12 * @see day_7_bridge.AbstractPhone#run() 13 */ 14 @Override 15 public void run() { 16 this.software.run(); 17 } 18 }
1 package day_7_bridge; 2 3 /** 4 * @author 余林丰 5 * 6 * 2016年10月7日 7 */ 8 public class PhoneN extends AbstractPhone { 9 10 /* (non-Javadoc) 11 * @see day_7_bridge.AbstractPhone#run() 12 */ 13 @Override 14 public void run() { 15 this.software.run(); 16 } 17 }
客户端测试代码。
1 package day_7_bridge; 2 3 /** 4 * @author 余林丰 5 * 6 * 2016年10月7日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 PhoneM phoneM = new PhoneM(); 15 phoneM.setSoftware(new Contacts()); 16 phoneM.run(); 17 phoneM.setSoftware(new Game()); 18 phoneM.run(); 19 } 20 21 }
就像上面所提到的,其实这个例子不怎么合适,用现在Android手机一统天下,将软硬件分离才契合这个桥接模式。说了那么多,还是给一个桥接模式的定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。注意,这里的抽象与实现分离,并不是指的抽象类和派生类的分离,在这个例子中指的就是“手机”这个抽象,应把“手机品牌”和“手机软件”做分离,而不是一味地使用继承关系。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?