“设计模式” 是程序员 “飞升上仙” 的必修课, 本文用生动有趣的故事阐述了关于 “设计模式” 的一些基础知识.

引子

我跟媳妇曾经就面向对象设计这个话题做过有趣的探讨。当我把它们发表在社区之后,得到了一些很不错的反馈,也大大鼓舞了我。所以,我很高兴能把我们后面的一次谈话继续分享出来,那是关于面向对象的设计模式的,大家往下看吧。

什么是设计模式

丈夫: 我想你现在对面向对象的设计原则有了一些基本概念了吧。我们那次关于OOD (面向对象设计) 原则(SOLID原则)的有趣谈话被我发表在社区上了,你不会介意吧?网址在这里: 我怎么向妻子解释OOD

设计模式则是这些原则在某些特定和常用条件下的应用,并且做了一些标准化。我们还是来一些例子吧。

媳妇: 好极了,我喜欢例子。

丈夫: 以我们的车为例吧。它是一个对象,不过有点复杂,是由几千个其它对象组成的,包括引擎、车轮、转向装置、座位、车身,等等。

一辆车的各种零件。这辆车在制造的时候,制造商收集所有的零件,把它们组装起来。这些零件本身也是复杂的对象,是由其它的制造商组装的。但汽车公司并不关心这些零件是怎么造出来的(当然,他们需要确信这些零件的质量是过硬的)。他们只会关心如何通过不同的方式将不同的零件组装起来,以便生产出不同型号的汽车。

由不同零件根据不同设计组装成的不同型号的车。

媳妇: 每种型号的汽车应该都有各自的设计和蓝图什么的,是吧?

丈夫: 非常正确。而且,这些设计是经过深思熟虑的,花了很长的时间和很大的努力才得以诞生。完成设计之后,汽车的生产就只剩下遵循设计这么简单的事了。

媳妇: 嗯……很不错的办法,先想出一些优秀的设计,然后遵照这些设计,就可以在很短的时间里造出不同的东西。如果制造商想要开发某种型号的产品,不需要从头进行设计,或者说重新造轮子,只要遵循那些设计就可以了。用于不同型号产品(车)的不同设计。

丈夫: 你说到点子上了。现在,回到现实里,我们是软件厂商,我们需要根据需求,用不同的组件来创造出不同的软件。在这个过程中,一定会碰到一些情形,是在许多不同的软件里都有的,对不对?

媳妇: 对啊。而且,我们还常常在不同的软件里碰到相同的设计难题呢。

丈夫: 我们尝试着用一种面向对象的方式来开发我们的软件,利用OOD原则来让我们的代码更容易管理、重用和扩展。就像你上面提到的那些相同的问题,如果我们预先就有一些良好的设计,那是不是很棒呢?媳妇: 是啊,那可以省下大把的时间,而且这样打造的软件质量更好,更容易管理。

丈夫: 没错。还有个好消息,我们并不需要自己造轮子。这么多年以来,遭遇同样问题的人们早已发现了许多很棒的解决方案,而且把它们标准化过了。我们管这些方案叫设计模式。我们要感谢四人帮(GoF),他们在设计模式: 可重用面向对象软件的基本元素这本书里归纳了23个最基本的设计模式。想知道这四个牛人是谁吗?Erich Gamma、Richard Helm、Ralph Johnson和 John Vlissides。面向对象的设计模式很多,但大家认为这23个模式是其它模式的基础。

媳妇:我能创建一个新模式吗?有可能吗?

丈夫:当然可以,亲爱的,为什么不行呢?设计模式并不是被科学家发明和创造的东西。他们只是被“发现”而已。也就是说,对任何一个普通的问题场景,肯定会有一些好的设计方案。如果我们能识别出一个能解决某个新问题的面向对象设计,那我们就定义了一个新的设计模式。谁知道呢?如果我们发现一些设计模式,没准儿大家会叫我们“二人帮”呢…哈哈。

媳妇::)

我们怎么来学习设计模式呢?

丈夫: 我始终坚信,通过例子学习是最好的。在我们的学习过程中,我们不会“先理论后实践”,因为我认为这是一种“坏”方法。设计模式不是基于理论发明的。相反,总是先有问题场景,再基于需求和情景不断演化设计方案,最后把一些方案标准化成“模式”。所以,我们讨论每一个设计模式时,要尽量用生活中的真实问题来理解和分析。然后尝试一步步地阐述设计,并以一个能匹配某些模式的设计收尾。设计模式就是这样被发现的,你觉得呢?

媳妇: 我觉得对我来说,这种方式可能更好使。如果我能通过先分析问题,然后阐述解决方案, 最后得到一个设计模式,我就不用死记那些图形和定义了。就这么办吧。

基础的设计问题和解决方案

丈夫: 让我们考虑一下下面的情况:我们的家里都有家用电器(比如电灯和风扇),他们都是由开关控制。 任何时候,你都可以在不改变其他东西的情况下做一些事。你可以在不更换开关的情况下换掉灯泡,也可以在不接触灯泡或者风扇的情况下更换开关,甚至可以在不接触开关的情况下,把灯泡和风扇的开关互换。

家用电器:风扇和灯泡

两种开关(第二个显然比第一个要好看)

媳妇:对啊,这看起来很自然,不是吗?

丈夫:是的,非常自然,同时也应该这样安排。当不同的事物联系到一起时,他们应该在一个可以变更或者可以替换的系统中以便不相互影响,或者影响尽可能的小。这样让你更为方便、成本最小地去管理你的系统。可以想象,如果你要换一个你房间里的灯泡得要求你把开关也换了,你会考虑在你房子里使用这样的一个系统吗?

媳妇:当然不会。

丈夫:现在,让我们想一下电灯或者电风扇是怎样和开关联系起来以便更换其中一个而不会影响到其他的。你想到什么了?

媳妇:当然是电线啦。丈夫:正确,是电线以及其他的电工手段把电灯/电风扇与开关连接起来。我们可以把这概括为沟通不同系统的桥梁。基本思想是,一个事物不能直接连接另一个事物。当然,他们能够通过一些桥梁或接口连接起来。在软件世界里,我们称之为“松耦合”。

媳妇:嗯,我明白这点。丈夫:现在,我们来尝试理解一些类似电灯/电风扇与开关类似的关键问题,同时尝试理解是怎样设计和关联它们的。媳妇:好的,我们开始吧。在我们的列子里,有一些开关,这些类似普通的开关、有不同的花式开关可能有不同的种类,但是,一般情况下,他们就是开关。同时,每个开关都能开和关。这样的话,我们就会得到如下的Switch基类:

1
2
3
4
5
6
7
8
public class Switch {  
public void On() {
//Switch has an on button
}
public void Off() {
//Switch has an off button
}
}

同时,我们可能也 需要一些特定类型的开关,譬如正常的开关、不同花式的开关等等。同样的我们扩展Switch类来实现FancySwitchNormalSwitch

1
2
3
4
5
6
public class NormalSwitch extends Switch{

}
public class FancySwitch extends Switch{

}

这两个特定的开关类可能用于它们自己特有的行为和特征,但是到目前为止,我们还是保持它们现在的简单形式。

丈夫:棒极了。现在,如何处理风扇和灯呢?

媳妇:让我试试。按照面向对象设计原则中的封闭原则,我认为我们需要试着在任何可能的地方做抽象处理,对吗?

丈夫:对。

媳妇: 电扇和电灯情况有点不一样,它们两个不是同一种东西。对于不同的开关,我们可以用同一个基本的Switch类,但对于电扇和电灯就不大合适了,感觉用接口会更合适一点。因为,从大体上讲,它们都算是电器,那么我们可以就定义一个接口: IElectricalEquipment,用它来抽象电扇和电灯,对不对?

丈夫: 很对。

媳妇: 那么,所有电器都有一些共性,可以被打开和关闭。那么这个接口就可以是:

1
2
3
4
public interface IElectricalEquipment{    
void PowerOn(); // Each electrical equipment can be turned on
void PowerOff(); // Each electrical equipment can be turned off
}

**媳妇: **没错,开关并不知道电扇和电灯的存在。它只知道它可以打开或关闭某个电器IElectricalEquipment。那么,也就是说每个Switch应该拥有一个IElectricalEquipment实例,是吧?

丈夫: 没错。你的抽象能力很强呀,媳妇。好了,现在我们还缺一座桥。在现实世界里,桥是电线。在对象的世界里,开关知道怎么开关电器,电器需要用某种方式跟开关连起来。可这里没有电线,我们唯一有的,是封装。

丈夫: 很对。这里,被封装的实例,也就是IElectricalEquipment,就是这座桥。好,我们来修改一下Switch类,让它把电器封装进去:

1
2
3
4
5
6
7
8
9
10
public class Switch  {  
public IElectricalEquipment equipment;

public void On() {
// Switch has an on button
}
public void Off() {
// Switch has an off button
}
}

媳妇: 我懂了。接下来我再定义真正的电器吧。电扇和电灯,总体上都是电器,所以它们应该实现IElectricalEquipment接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 电扇类:
public class Fan implements IElectricalEquipment{
public void PowerOn() {
System.out.println("Fan is on");
}
public void PowerOff() {
System.out.println("Fan is off");
}
}

// 电灯类:
public class Light implements IElectricalEquipment{
public void PowerOn() {
System.out.println("Light is on");
}
public void PowerOff() {
System.out.println("Light is off");
}
}

丈夫: 很好。现在该是接上开关的时候了。开关在打开和关闭的时候,必须能打开和关闭它所连接的电器。也就是说:当按下开关的打开按钮时,必须打开连接的电器。当按下开关的关闭按钮时,必须关闭连接的电器。我们想要的功能基本上是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args){  
// We have some electrical equipments, say Fan, Light etc.
// So, lets create them first.
IElectricalEquipment fan = new Fan();
IElectricalEquipment light = new Light();
// We also have some switches. Lets create them too.
Switch fancySwitch = new FancySwitch();
Switch normalSwitch = new NormalSwitch();
// Lets connect the Fan to the fancy switch
fancySwitch.equipment = fan;
// As the switch now has an equipment (Fan),
// so switching on or off should turn on or off the electrical equipment
fancySwitch.On(); // It should turn on the Fan.
// so, inside the On() method of Switch, we must turn on the electrical equipment.
// It should turn off the Fan. So, inside the On() method of
fancySwitch.Off();
// Switch, we must turn off the electrical equipment
// Now, lets plug the light to the fancy switch
fancySwitch.equipment = light;
fancySwitch.On(); //It should turn on the Light now
fancySwitch.Off(); //It should be turn off the Light now
}

媳妇: 明白了。那么,开关的On()方法应该调用电器的TurnOn()方法,而它的Off()方法应该调用电器的TurnOff()方法,Switch类应该是这个样子:

1
2
3
4
5
6
7
8
9
10
public class Switch {  
public void On() {
System.out.println("Switch on the equipment");
equipment.PowerOn();
}
public void Off() {
System.out.println("Switch off the equipment");
equipment.PowerOff();
}
}

丈夫: 干得好。这个电扇显示是可以换开关的。而且,反过来也是可以换的,可以不修改电扇和电灯,直接更换开关,例如,我们可以把电灯的开关从FancySwitch换成NormalSwitch:

1
2
normalSwitch .equipment = light;normalSwitch.On();   // It should turn on the Light
nownormalSwitch.Off(); // It should be turn off the Light now

看到没,我们可以在不影响任何一方的情况下,改变另一方。这个设计看起来很不错,而且相当的优雅。其实四人帮管这个设计叫桥梁模式。

媳妇:酷!我懂了。一般来说,两个系统不应该直接地互相联接和依赖。相反,他们应该通过抽象来联接或依赖(正如依赖倒置和开闭原则所言),这样它们就是松耦合的,我们就可以在必要时轻松地修改实现,而不对系统的其它部分造成太大影响。

丈夫:亲爱的,你得我真传了。我们来看一下桥接模式的定义吧:

把抽象和实现解耦,使得它们可以独立地变化

我们的设计完全符合定义。如果你有类设计器(Visual Studio和其它流行的IDE都有这功能),你能看到一个和下图相似的类图:

在我们的例子里,Abstraction是基础的Switch类,RefinedAbstraction是某个具体的开关类(FancySwitchNormalSwitch),ImplementorIElectricalEquipment接口,ConcreteImplementorAConcreteImplementorB分别是FanLight类。

媳妇: 我有点好奇。你不是说有很多模式么,干嘛先说桥梁模式呢?有什么特别重要的原因吗?

丈夫: 问得好。我从桥梁模式开始,而不是其它模式,只有一个原因。我觉得它是所有面向对象设计模式的基础。因为:

  • 它能教你如何抽象地思维,这可是OO设计模式的关键。
  • 它实现了基本的OOD原则。
  • 它很好理解。
  • 如果你能正确地理解它,学习其它模式就易如反掌了。

媳妇**:** 你觉得我理解的对么?

丈夫: 哦~亲爱的,我觉得你理解的非常好。

媳妇: 那么,下一步呢?

丈夫: 通过了解桥接模式,我们才稍微的了解了设计模式的概念。在我们接下来的谈话中,我们将会学习其他的涉及模式,希望你不会觉得它们无聊。

媳妇: 不会的,相信我。


本文转自开源中国社区 , 英文原文

原文代码是使用 C++ 写的, 考虑到我亲爱的同学们可能更熟悉 Java, 所以我把代码部分改成了 Java 语法.