面向对象设计模式之桥接模式
设计模式要在有了一定的编程经验之后才好理解与领悟,其实最主要的还是领悟!!!
一.前言
对于学习过C#,Java,Python等高级语言的同学来说,面向对象编程肯定不陌生,我们在使用面向对象设计程序的过程中要尽可能的降低代码的耦合度,还要使代码具有可拓展性,毕竟谁也不敢肯定后期需求不会改变。
设计模式总共有23种,在使用面向对象来设计程序时,我们尽量要使用设计模式来设计程序,并要遵从面向对象程序设计的5大基本原则,即开放封闭原则,单一职责原则,里氏代换原则,依赖倒置原则,接口隔离原则,在这几个基本原则中,开放封闭原则和单一职责原则非常重要,对于有一定编程经验的同学来说肯定深有体会,在此我就不再累述。
二.分析
那么对于本次文章,我们就来讨论23种设计模式之一桥接模式,先来看看对这种模式的专业解释:将抽象部分与它的实现部分分离,使他们可以独立的变化。看着一头雾水,以下就来举例子学习这个模式。
差不多在10年以前,在手机领域九键手机可谓是当时的潮牌,各式各样型号,厂家众多,那时的手机硬件软件都不统一。假设现在某个软件厂商开发了“音乐播放器”,“魂斗罗游戏”等软件,这些软件需要分别安装在品牌A手机,品牌B,品牌C手机上。按照面向对象的常规思想抽象,得到以下代码结构图:
这个结构乍一看还不错,但代码重复很多。现在软件商家又开发了“地图”软件,要为每个品牌手机添加这个功能,那么现在又要为每一个品牌类增加一个“地图”子类了,这样确实可以解决问题,但是要添加3次,这样又增加了很多重复的代码,而且软件和手机品牌耦合在一起,便不合理,所以需要重新设计代码结构图了。
在设计代码结构图之前,需要先知道合成/聚合复用原则。它们的定义是:合成是一种强的拥有‘拥有’关系,体现了严格的整体和部分的关系,部分和整体的生命周期是一样的。则聚合是一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。听着有点绕,举个例子,大雁的翅膀和大雁是部分与整体的关系,两者的生命周期一样,所以大雁的翅膀和大雁是合成关系。大雁是一种群居动物,单只大雁属于一个雁群中的一只,一个雁群由很多大雁组成,所以体现的是聚合关系。所以在面对这个上面的需求时,我们可以抽象手机的品牌和软件厂商开发的软件,以下是代码结构图:
中间的那条横线是不是很像一座条桥,连接了连个模块,可能这就是桥接模式名称的由来吧。对于这个结构图,分别抽象了手机品牌和软件,使他们相对独立,仅仅只用聚合关系将两者联系起来,对于软件抽象模块下的改变并不会影响手机品牌模块,这就符合了开放封闭原则。以下我们使用C#来实现。
三.案例实现
手机品牌模块的抽象类:
// 手机品牌抽象类
abstract class MobileBrand
{
// 引用 软件 的抽象, 实现 手机品牌 和 软件 的 聚合
protected Software software;
// 手机上安装软件,参数software表示安装软件的类型
public void InstallSoftware(Software software) {
this.software = software;
}
// 运行软件
public abstract void Run();
}
手机品牌A类:
// 手机品牌A
class MobileBrand_A : MobileBrand
{
// 手机品牌A 安装软件
public void InstallSoftware(Software software)
{
this.software = software;
}
// 运行软件
public override void Run()
{
// 传递的参数是类名,代表手机品牌的名字
software.Run(this.ToString());
}
}
手机品牌B,C的代码都和手机品牌A一样,再此不再写出。
软件模块的抽象类:
// 软件的抽象类
abstract class Software
{
// 运行软件,mobileBrandName参数代表运行的手机平台的名称
public abstract void Run(string mobileBrandName);
}
魂斗罗类:
// 魂斗罗类,继承软件抽象类
class Software_Contra : Software
{
// 运行 魂斗罗
public override void Run(string mobileBrandName)
{
Console.WriteLine("--- " + mobileBrandName + " 牌手机 运行 魂斗罗");
}
}
地图类:
// 地图类,继承软件抽象类
class Software_Map : Software
{
// 运行地图
public override void Run(string mobileBrandName)
{
Console.WriteLine("--- " + mobileBrandName + " 牌手机 运行 地图");
}
}
音乐播放器类:
// 音乐播放器类,继承软件抽象类
class Software_MusicPlay : Software
{
// 运行 音乐播放器
public override void Run(string mobileBrandName)
{
Console.WriteLine("--- " + mobileBrandName + " 牌手机 运行 音乐播放器");
}
}
客户端代码:
class Client
{
static void Main(string[] args)
{
// 手机品牌A
MobileBrand mobileBrandA = new MobileBrand_A();
// 在手机品牌A上安装 音乐播放器
mobileBrandA.InstallSoftware(new Software_MusicPlay());
// 手机品牌A运行音乐播放器
mobileBrandA.Run();
// 手机品牌A在手机品牌A上安装 魂斗罗
mobileBrandA.InstallSoftware(new Software_Contra());
// 手机品牌A运行魂斗罗
mobileBrandA.Run();
Console.WriteLine();
// 手机品牌B
MobileBrand mobileBrandB = new MobileBrand_B();
// 在手机品牌B上安装 地图
mobileBrandB.InstallSoftware(new Software_Map());
// 手机品牌B运行音乐播放器
mobileBrandB.Run();
// 手机品牌B在手机品牌A上安装 音乐播放器
mobileBrandB.InstallSoftware(new Software_Contra());
// 手机品牌B运行魂斗罗
mobileBrandB.Run();
Console.WriteLine();
// 手机品牌C
MobileBrand mobileBrandC = new MobileBrand_C();
// 在手机品牌C上安装 音乐播放器
mobileBrandC.InstallSoftware(new Software_MusicPlay());
// 手机品牌C运行音乐播放器
mobileBrandC.Run();
// 在手机品牌C上安装 地图
mobileBrandC.InstallSoftware(new Software_Map());
// 手机品牌C运行地图
mobileBrandC.Run();
// 手机品牌C在手机品牌A上安装 魂斗罗
mobileBrandC.InstallSoftware(new Software_Contra());
// 手机品牌C运行魂斗罗
mobileBrandC.Run();
Console.ReadLine();
}
}
运行结果:
、
四.总结
文章开头说过,面向对象程序要尽量的控制代码的耦合性,还要具有可拓展性,在上上面的这个案例中,两个模块相互独立,互不影响,模块内部的改变不影响另一个模块,这就具有较低的耦合性。比如现在软件商家还要开发‘电子邮件’软件,只需在软件模块添加子类即可,没有影响到手机品牌模块,或是现在有了新的手机品牌D,在手机品牌模块中添加子类即可,不影响软件模块,这就表明这个程序具有可拓展性。
对于桥接模式,它的运用场景受到了一定的限制,在上面的那个例子中有两个维度的变化,手机品牌的变化和软件种类的变化,如果一个系统中有两个维度的变化,则可以考虑使用桥接模式。接下来分析这种模式的优缺点:
优点:
- 桥接模式提高了程序的可拓展性。
- 降低了程序的耦合度。
- 分离了抽象接口和实现部分。
缺点:
- 需要有两个维度的变化,这样限制了使用场景。
- 使用桥接模式会增加系统的理解难度和设计难度。