22.手机软件何时统一 - 桥接模式(Bridge Pattern)

22.1 凭什么你的手机我不能玩儿

  • 时间: 5月31日20点   地点: 大鸟房间   人物: 小菜, 大鸟

     “大鸟, 捧着个手机, 玩儿什么?” 小菜冲进了大鸟的房门.

     “哈, 玩小游戏呢, 新买的手机, 竟然可以玩儿小时候的游戏 ‘魂斗罗’. 很久没有碰到这东西了, 感觉很爽啊.”
     “哦, 是吗, 连这游戏都有啊, 给我看看.”
     “等你死了, 你什么时候会死啊.” 小菜笑道.
     “那还有段时间了, 至少半小时吧.”
     “半小时才死呀, 哦, 那我半小时之后来给你收尸.” 小菜故意提高了嗓门.
     “你小子, 找死呀. 给你给你!” 大鸟笑着把手机递给了小菜, “和红白机上的一模一样, 很让人怀旧啊. 嗨, 我跟你们这种80后的小子说红白机, 不就等于对牛弹琴吗!”
     “……”
     “哈, 手机厂商为什么不可以学计算机呢? 由专业公司开发操作系统和应用软件, 手机商只要好好把手机硬件做好就行了.”
     “统一谈何容易, 谁做的才是标准呢? 而谁又不希望自己的硬件和软件成为标准, 然后一统天下. 这里有很多商业竞争的问题, 不是我们想的那么简单. 不过目前很多智能手机都在朝这个方向发展. 或许过几年, 我们手里的手机就可以实现软件完全兼容了.”
     “我想那个时候应该不叫手机了, 而是掌上电脑比较合适.”(Android, IOS)

22.2 紧耦合的程序演化.

     “说的有道理, 另外你有没有想过, 这里其实蕴含着两种完全不同的思维方式?”
     “你是说手机软硬件和 PC 软硬件?”
     “对的, 如果我现在有一个 N 品牌的手机, 他有一个小游戏, 我要玩儿游戏, 程序应该如何写?”
     “这还不简单. 先写一个此品牌的游戏类, 在用客户端调用即可.”
     游戏类

1
2
3
4
5
6
// N 品牌手机中的游戏
public class HandsetNGame {
public void run() {
System.out.println("运行N品牌手机的游戏");
}
}

     客户端代码

1
2
3
4
public static void main(String[] args) {
HandsetNGame game = new HandsetNGame();
game.run();
}

     “很好, 现在又有一个 M 品牌的手机, 也有小游戏, 客户端也可以调用, 如何做?”
     “恩, 我想想, 两个品牌, 都有游戏, 我觉得从面向对象的思想来说, 应该有一个父类 ‘手机品牌游戏’, 然后让N 和 M品牌的手机游戏都继承他, 这样可以实现相同的运行方式.”
     “小菜不错, 抽象的感觉来了.”
     手机游戏类

1
2
3
public abstract class HandsetGame {
public abstract void run();
}

     M 品牌手机游戏和 N 品牌手机游戏

1
2
3
4
5
6
7
8
9
10
11
public class HandsetMGame extends HandsetGame{
public void run() {
System.out.println("运行M品牌手机的游戏");
}
}
public class HandsetNGame extends HandsetGame{
public void run() {
System.out.println("运行N品牌手机的游戏");
}
}

     “然后, 由于手机都需要通讯录功能, 于是N品牌手机和 M 品牌都增加了通讯录功能的增删改查功能, 你如何处理?”
     “啊, 这就有点麻烦了, 那就意味着, 父类应该是 ‘手机品牌’, 下有 ‘手机品牌M’ 和 ‘手机品牌N’, 每个子类下各有 ‘通讯录’ 和 ‘游戏’ 子类.”



     手机类

1
2
3
4
// 手机品牌
public abstract class HandsetBrand {
public abstract void run();
}

     手机品牌N 和手机品牌M 类

1
2
3
4
5
6
7
8
// 手机品牌M
public abstract class HandsetBrandM extends HandsetBrand {
}
// 手机品牌N
public abstract class HandsetBrandN extends HandsetBrand {
}

     下属的各自通讯录类和游戏类

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
// 手机品牌M 的游戏
public class HandsetBrandMGame extends HandsetBrandM{
public void run() {
System.out.println("运行M品牌手机的游戏");
}
}
// 手机品牌N 的游戏
public class HandsetBrandNGame extends HandsetBrandN{
public void run() {
System.out.println("运行N品牌手机的游戏");
}
}
// 手机品牌M 的通讯录
public class HandsetBrandMAddressList extends HandsetBrandM{
public void run() {
System.out.println("运行N品牌手机的通讯录");
}
}
// 手机品牌N 的通讯录
public class HandsetBrandNAddressList extends HandsetBrandN{
public void run() {
System.out.println("运行N品牌手机的通讯录");
}
}

客户端调用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
HandsetBrand ab;
ab = new HandsetBrandMAddressList();
ab.run();
ab = new HandsetBrandMGame();
ab.run();
ab = new HandsetBrandNAddressList();
ab.run();
ab = new HandsetBrandNGame();
ab.run();
}

     “哈, 这个结构应该还是可以的, 现在我问你, 如果我现在需要每个品牌都增加一个 MP3 音乐播放功能, 你如何做?”
     “这个? 那就在每个品牌的下面都增加一个子类.”
     “你觉得这两个子类差别大不大?” 大鸟追问道.
     “应该是不大的, 不过没办法呀, 因为品牌不同, 增加功能就必须要这样做.” 小菜无奈的说.
     “好, 那我现在又来了一家手机品牌 ‘S’, 它也有游戏, 通讯录, MP3 音乐播放功能, 你如何处理?”
     “啊, 那就得在增加 ‘手机品牌S’ 类和三个下属功能子类. 这好像有点麻烦了.”
     “你也感觉麻烦了? 如果我在增加 ‘输入法’ 功能, ‘拍照’ 功能, 在增加 ‘L品牌’, ‘X品牌’, 你的类如何写?”
     “啊哦,” 小才学了一声唐老鸭叫, 感慨道, “我要疯了. 要不这样, 我换一种方式.”
     过了几分钟, 小菜活出了另一种结构图.



     “你觉得这样问题就可以解决了吗?”
     “啊, “ 小菜摇了摇头, “不行, 要是增加手机功能或是增加品牌都会产生很大的影响.”
     “你知道问题出在哪里吗?”
     “我不知道呀,” 小菜很疑惑, “我感觉我一直在用面向对象的理论设计的, 现有一个品牌, 然后多个品牌就抽象出一个品牌抽象类, 对于每个功能, 就都集成各自的品牌. 或者, 不从品牌, 从手机软件的角度去分类, 这有什么问题吗?”
     “是呀, 就像我刚开始学会用面向对象的继承时, 感觉它集新颖又功能强大, 所以只要可用, 就都用上继承. 这就好比 ‘有了新锤子, 所有的东西看上去都是钉子’.[DPE] 但事实上, 很多情况用继承会带来麻烦. 比如, 对象的继承关系是在编译时就定义好了, 所以无法在运行时改变从父类的实现. 子类的实现与他的父类有非常紧密的依赖关系, 以至于父类实现中的任何变化必然会导致子类发生变化. 当你需要复用子类时, 如果继承下来的实现不适合解决新的问题, 则父类必须重写或被其他更适合的类替换. 这种依赖关系限制了灵活性并最终限制了复用性[DP].
     “是呀, 我这样的继承结构, 如果不断的增加新的品牌或新功能, 类会越来越多的.”
     “在面向对象的设计中, 我们还有一个很重要的设计原则, 那就是合成/聚合复用原则. 即优先使用对象的合成/聚合, 而不是类继承.”

22.3 合成/聚合复用原则

合成/聚合复用原则(CARP Composition Aggregation Reuse Principle), 尽量使用合成/聚合, 尽量不要使用类继承[J&DP].

     合成(Composition, 也有翻译成组合) 和聚合(Aggregation) 都死关联的特殊种类. 聚合便是一种弱的 ‘拥有’ 关系, 体现的是A对象可以包含B对象, 但B 对象不是 A对象的一部分; 合成则是一种强的 ‘拥有’ 关系, 体现了严格的部分和整体的关系, 部分和整体的生命周期一样[DPE]. 比方说, 打压有两个翅膀, 翅膀与大雁是部分和整体的关系, 并且它们的生命周期是相同的, 于是大雁和翅膀就是合成关系. 而大雁是群居动物, 所以每只大雁都是属于一个雁群, 一个雁群局可以有多只大雁, 所以大雁和雁群是聚合关系.



     “合成/聚合复用的原则好处是, 优先使用对象的合成/聚合将有助于你保持每个类被封装, 并被集中在单个任务上. 这样类和类的继承层级会保持在较小的规模, 并且不太可能增长成为不可控制的庞然大物[DP]. 就刚才的例子, 你需要学会用对象的职责, 而不是结构来考虑问题. 其实答案就在之前我们聊到的手机与PC 电脑的差别上.”
     “哦, 我想想看, 手机是不同的品牌公司, 各自做自己的软件, 就像我现在设计的一样, 而PC 确实硬件厂商做硬件, 软件厂商做软件, 组合起来才是可以用的机器. 你是这个意思吗?”
     “很好, 我很喜欢你提到 ‘组合’ 这个词, 实际上, 像 ‘游戏’, ‘通讯录’, ‘MP3 音乐播放器’ 这些功能都是软件, 如果我们可以让其分离与手机的耦合, 那么就可以大大减少面对新需求时改动过大的不合理情况.”
     “好的好的, 我想想怎么弄, 你的意思其实就是应该有个 ‘手机品牌’ 抽象类和 ‘手机软件’ 抽象类, 让不同的品牌和功能都分别继承与他们, 这样增加减新的品牌或功能都不用影响其他类了.”



     “还剩个问题, 手机品牌和手机软件之间的关系呢?” 大鸟问道.
     “我觉得应该是手机品牌包含有手机软件, 单软件并不是品牌的一部分, 所以他们之间应该是聚合关系.”



     “说得好. 来试着写写看吧.”

22.4 松耦合的程序

     小菜经过半小时, 改动代码如下.
     手机软件抽象类.

1
2
3
public abstract class HandsetSoft {
public abstract void run();
}

     游戏, 通讯录等具体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 手机游戏
public class HandsetGame extends HandsetSoft {
@Override
public void run() {
System.out.println("运行手机游戏");
}
}
// 手机通讯录
public class HandsetAddressList extends HandsetSoft {
@Override
public void run() {
System.out.println("运行手机通讯录");
}
}

     手机品牌类

1
2
3
4
5
6
7
8
9
10
11
// 手机品牌
public abstract class HandsetBrand {
protected HandsetSoft soft;
// 设置手机软件
public HandsetBrand(HandsetSoft soft) {
this.soft = soft;
}
public abstract void run();
}

     品牌N 品牌M具体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 手机品牌N
public class HandsetBrandN extends HandsetBrand {
public HandsetBrandN(HandsetSoft soft) {
super(soft);
}
@Override
public void run() {
soft.run();
}
}
// 手机品牌M
public class HandsetBrandM extends HandsetBrand {
public HandsetBrandM(HandsetSoft soft) {
super(soft);
}
@Override
public void run() {
soft.run();
}
}

     客户端调用代码

1
2
3
4
5
6
7
8
public static void main(String[] args) {
HandsetBrand ab;
ab = new HandsetBrandN(new HandsetGame());
ab.run();
ab = new HandsetBrandM(new HandsetAddressList());
ab.run();
}

     “感觉如何? 是不是好多了.”
     “是呀, 现在如果要增加一个功能, 比如MP3 音乐播放功能, 那么只需要增加着各类就行了. 不会影响其他任何类. 类的个数增加也只是一个.”

1
2
3
4
5
6
public class HandsetMP3 extends HandsetSoft {
@Override
public void run() {
System.out.println("运行手机MP3");
}
}

     “如果是要增加S 品牌, 只需要增加一个品牌的子类就可以了. 个数也是一个, 不会影响其他类的改动.”

1
2
3
4
5
6
7
8
9
10
public class HandsetBrandS extends HandsetBrand {
public HandsetBrandS(HandsetSoft soft) {
super(soft);
}
@Override
public void run() {
soft.run();
}
}

     “这显然也是符合了我们之前的一个什么设计原则?”
     “开放—封闭原则. 这样的设计显然不会修改原来的代码, 而只是扩展类就行了. 但今天我的感受最深的是合成/聚合复用原则, 也就是优先使用对象的合成或聚合, 而不是类继承. 聚合的魅力无限呀. 相比, 继承的确很容易造成不必要的麻烦.”
     “盲目的使用继承就会造成麻烦, 而其本质原因主要是什么?”
     “我想应该是, 继承是一种强耦合的结构. 父类变, 子类就必须要变.”
     “OK, 所以我们再用继承时, 一定是要在 ‘is-a’ 的关系是在考虑使用, 而不是任何时候都去使用.”
     “大鸟, 今天这个例子是不是一个设计模式?”
     “哈, 当然, 你看看刚才画得那幅图, 两个抽象类之间有什么, 像什么?”
     “有一个聚合线, 像一座桥.”
     “哈, 说得好, 这个设计模式就叫做 ‘桥接模式’”.

22.5 桥接模式

桥接模式(Bridge), 将抽象部分与它的实现部分分离, 是他们都可以独立的变化.[DP].

     “这里需要解释一下, 什么叫抽象与它的实现分离, 这并不是说, 让抽象类与其派生类分离, 因为这没有任何意义. 实现指的是抽象类和他的派生类用来实现自己的对象. 就刚才的例子而言, 就是让 ‘手机’ 既可以按照品牌来分类, 也可以按照功能来分类.”
     按品牌分类实现结构图



     按软件分类实现结构图



     “由于实现的方式有多种, 桥接模式的和兴意图就是把这些实现独立出来, 让他们各自的变化. 这就使得每种实现的变化不会影响其他的实现, 从而达到应对变化的目的.”



22.6 桥接模式的基本代码



     Implementor 类

1
2
3
public abstract class Implementor {
public abstract void operation();
}

     ConcreteImplementorA 和 ConcreteImplementorB 等派生类.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteImplementorA extends Implementor {
@Override
public void operation() {
System.out.println("具体实现A的方法执行");
}
}
public class ConcreteImplementorB extends Implementor {
@Override
public void operation() {
System.out.println("具体实现B的方法执行");
}
}

     Abstraction 类

1
2
3
4
5
6
7
8
9
10
11
public class Abstraction {
protected Implementor implementor;
public void setImplementor(Implementor implementor) {
this.implementor = implementor;
}
public void operation() {
implementor.operation();
}
}

     RefinedAbstraction 类

1
2
3
4
5
6
public class RefinedAbstraction extends Abstraction {
@Override
public void operation() {
implementor.operation();
}
}

     客户端实现

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Abstraction ab = new RefinedAbstraction();
ab.setImplementor(new ConcreteImplementorA());
ab.operation();
ab.setImplementor(new ConcreteImplementorB());
ab.operation();
}

     “我觉得桥接模式所说的 ‘将抽象部分与他的实现部分分离’, 还是不好理解, 我的理解就是实现系统可能有多角度分类, 每一种分类都有可能变化, 那么就把这种多角度分离出来让他们独立变化, 减少他们之间的耦合.
     “哈, 小蔡说的和GoF 说的不就是一回事吗! 只不过你说得更通俗, 而人家却更简练而已. 也就是说, 在发现我们需要多角度去分类实现对象, 而只用继承会造成大量的类增加, 不能满足开放封闭原则时, 就应该考虑用桥接模式了.”
     “哈, 我感觉只要深入理解了设计原则, 很多设计模式其实就是原则的应用而已, 或许在不知不觉中就在使用设计模式了.

22.7 我要开发’好’游戏

     “说得好, 好了, 你该干嘛去干嘛去, 我要继续玩儿游戏了.” 打鸟的注意回到了手机上.
     “……”

~感谢捧场,您的支持将鼓励我继续创作~