桥接模式

思考并回答以下问题:

  • 对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。怎么理解?
  • 聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。怎么理解?
  • 子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。怎么理解?
  • 大雁有两个翅膀,翅膀与大雁是部分和整体的关系,并且它们的生命周期是相同的,于是大雁和翅膀就是合成关系。而大雁是群居动物,所以每只大雁都是属于一个雁群,一个雁群可以有多只大雁,所以大雁和雁群是聚合关系。怎么理解?
  • 盲目使用继承当然就会造成麻烦,而其本质原因主要是继承是一种强耦合的结构。父类变,子类就必须要变。怎么理解?
  • 所以在用继承时,一定要在是‘is-a’的关系时再考虑使用,而不是任何时候都去使用。怎么理解?

紧耦合的程序演化

Nokia和Motorola火的时候这两个手机品牌的软件是不能兼容的,即使是Nokia同品牌的不同手机,有些软件也不能兼容。

现在有一个N品牌的手机,它有一个小游戏,要玩游戏,写一个此品牌的游戏类,再用客户端调用。

游戏类

1
2
3
4
5
6
7
8
// N品牌的手机中的游戏
class HandsetNGame
{
public void Run()
{
Console.WriteLine("运行N品牌手机游戏");
}
}

客户端代码

1
2
HandsetNGame game = new HandsetNGame();
game.Run();

现在又有一个M品牌的手机,也有小游戏,客户端也可以调用。

两个品牌,都有游戏,从面向对象的思想来说,应该有一个父类‘手机品牌游戏’,然后让N和M品牌的手机游戏都继承于它,这样可以实现同样的运行方法。

手机游戏类

1
2
3
4
5
6
class HandsetGame
{
public virtual void Run()
{
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HandsetMGame : HandsetGame
{
public override void Run()
{
Console.WriteLine("运行M品牌手机游戏");
}
}

class HandsetNGame : HandsetGame
{
public override void Run()
{
Console.WriteLine("运行N品牌手机游戏");
}
}

由于手机都需要通讯录功能,N品牌和M品牌都增加了通讯录的增删改查功能。

父类应该是‘手机品牌’,下有‘手机品牌M’和‘手机品牌N’,每个子类下各有‘通讯录’和‘游戏’子类。

代码结构图

手机类

1
2
3
4
5
6
7
// 手机品牌
class HandsetBrand
{
public virtual void Run()
{
}
}

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

1
2
3
4
5
6
7
8
9
// 手机品牌M
class HandsetBrandM : HandsetBrand
{
}

// 手机品牌N
class HandsetBrandN : 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
28
29
30
31
32
33
34
35
// 手机品牌M的游戏
class HandsetBrandMGame : HandsetBrandM
{
public override void Run()
{
Console.WriteLine("运行M品牌手机游戏");
}
}

// 手机品牌N的游戏
class HandsetBrandNGame : HandsetBrandN
{
public override void Run()
{
Console.WriteLine("运行N品牌手机游戏");
}
}

// 手机品牌M的通讯录
class HandsetBrandMAddressList : HandsetBrandM
{
public override void Run()
{
Console.WriteLine("运行M品牌手机通讯录");
}
}

// 手机品牌N的通讯录
class HandsetBrandNAddressList : HandsetBrandN
{
public override void Run()
{
Console.WriteLine("运行N品牌手机通讯录");
}
}

客户端调用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();

Console.Read();
}

如果现在需要每个品牌都增加一个MP3音乐播放功能,那就要在每个品牌的下面都增加一个子类。

这两个子类差别不大,不过没办法,因为品牌不同,增加功能必须要这样。

现在又来了一家新的手机品牌‘S’,它也有游戏、通讯录、MP3音乐播放功能,那就得再增加‘手机品牌S’类和三个下属功能子类。这有点麻烦了。

如果需要增加‘输入法’功能、‘拍照’功能,再增加‘L品牌’、‘X品牌’,类如何写?

另一种结构图。

要是增加手机功能或是增加品牌都会产生很大的影响。

一直在用面向对象的理论设计,先有一个品牌,然后多个品牌就抽象出一个品牌抽象类,对于每个功能,就都继承各自的品牌。或者,不从品牌,从手机软件的角度去分类,这有什么问题?

刚开始学会用面向对象的继承时,感觉它既新颖又功能强大,所以只要可以用,就都用上继承。这就好比是‘有了新锤子,所有的东西看上去都成了钉子。’但事实上,很多情况用继承会带来麻烦。比如,对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。

现在这样的继承结构,如果不断地增加新品牌或新功能,类会越来越多。

在面向对象设计中,还有一个很重要的设计原则,那就是合成/聚合复用原则。即优先使用对象合成/聚合,而不是类继承。

合成/聚合复用原则

1
尽量使用合成/聚合,尽量不要使用类继承。

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

合成/聚合复用原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。就刚才的例子,需要学会用对象的职责,而不是结构来考虑问题。其实答案就在之前说的手机与PC电脑的差别上。

手机是不同的品牌公司,各自做自己的软件,就像现在的设计一样,而PC却是硬件厂商做硬件,软件厂商做软件,组合起来才是可以用的机器。

‘组合’这个词,实际上,像‘游戏’、‘通讯录’、‘MP3音乐播放’这些功能都是软件,如果可以让其分离与手机的耦合,那么就可以大大减少面对新需求时改动过大的不合理情况。

应该有个‘手机品牌’抽象类和‘手机软件’抽象类,让不同的品牌和功能都分别继承于它们,这样要增加新的品牌或新的功能都不用影响其他类了。

结构图

手机品牌和手机软件之间的关系应该是手机品牌包含有手机软件,但软件并不是品牌的一部分,所以它们之间是聚合关系。

结构图

松耦合的程序

改动代码如下。

手机软件抽象类

1
2
3
4
5
// 手机软件
abstract class HandsetSoft
{
public abstract void Run();
}

游戏、通讯录等具体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 手机游戏
class HandsetGame : HandsetSoft
{
public override void Run()
{
Console.WriteLine("运行手机游戏");
}
}

// 手机通讯录
class HandsetAddressList : HandsetSoft
{
public override void Run()
{
Console.WriteLine("运行手机通讯录");
}
}

手机品牌类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class HandsetBrand
{
protected HandsetSoft soft;

// 设置手机软件
// 品牌需要关注软件,所以可在机器中安装软件(设置手机软件),以备运行
public void SetHandsetSoft(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
// 手机品牌N
class HandsetBrandN : HandsetBrand
{
public override void Run()
{
soft.Run();
}
}

// 手机品牌M
class HandsetBrandM : HandsetBrand
{
public override void Run()
{
soft.Run();
}
}

客户端调用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void Main(string[] args)
{
HandsetBrand ab;
ab = new HandsetBrandN();

ab.SetHandsetSoft(new HandsetGame());
ab.Run();

ab.SetHandsetSoft(new HandsetAddressList());
ab.Run();

ab = new HandsetBrandM();

ab.SetHandsetSoft(new HandsetGame());
ab.Run();

ab.SetHandsetSoft(new HandsetAddressList());
ab.Run();

Console.Read();
}

现在如果要增加一个功能,比如MP3音乐播放功能,那么只要增加这个类就行了。不会影响其他任何类。类的个数增加也只是一个。

1
2
3
4
5
6
7
8
// 手机MP3播放
class HandsetMP3 : HandsetSoft
{
public override void Run()
{
Console.WriteLine("运行手机MP3播放");
}
}

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

1
2
3
4
5
6
7
8
// 手机品牌S
class HandsetBrandS : HandsetBrand
{
public override void Run()
{
soft.Run();
}
}

符合了开放-封闭原则。这样的设计显然不会修改原来的代码,而只是扩展类就行了。合成/聚合复用原则,也就是优先使用对象的合成或聚合,而不是类继承。聚合的魅力无限。相比,继承的确很容易造成不必要的麻烦。

盲目使用继承当然就会造成麻烦,而其本质原因主要是继承是一种强耦合的结构。父类变,子类就必须要变。

所以在用继承时,一定要在是‘is-a’的关系时再考虑使用,而不是任何时候都去使用。

两个抽象类之间有一个聚合线,像一座桥,这个设计模式就叫做‘桥接模式’。

桥接模式

1
将抽象部分与它的实现部分分离,使他们都可以独立地变化。

什么叫抽象与它的实现分离,这并不是说,让抽象类与其派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象。就刚才的例子而言,就是让‘手机’既可以按照品牌来分类,也可以按照功能来分类。

按品牌分类实现结构图

按软件分类实现结构图

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

桥接模式基本代码

桥接模式(Bridge)结构图

Implementor类

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

ConcreteImplementorA和ConcreteImplementorB等派生类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConcreteImplementorA : Implementor
{
public override void Operation()
{
Console.WriteLine("具体实现A的方法执行");
}
}

class ConcreteImplementorB : Implementor
{
public override void Operation()
{
Console.WriteLine("具体实现B的方法执行");
}
}

Abstraction类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Abstraction
{
protected Implementor implementor;

public void SetImplementor(Implementor implementor)
{
this.implementor = implementor;
}

public virtual void Operation()
{
implementor.Operation();
}
}

RefinedAbstraction类

1
2
3
4
5
6
7
class RefinedAbstraction : Abstraction
{
public override void Operation()
{
implementor.Operation();
}
}

客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
static void Main(string[] args)
{
Abstraction ab = new RefinedAbstraction();

ab.SetImplementor(new ConcreteImplementorA());
ab.Operation();

ab.SetImplementor(new ConcreteImplementorB());
ab.Operation();

Console.Read();
}

桥接模式所说的‘将抽象部分与它的实现部分分离’,就是实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。

这种定义更通俗,而人家却更简练而已。也就是说,在发现需要多角度去分类实现对象,而只用继承会造成大量的类增加,不能满足开放-封闭原则时,就应该要考虑用桥接模式了。

只要真正深入地理解了设计原则,很多设计模式其实就是原则的应用而已,或许在不知不觉中就在使用设计模式了。

0%