<audio id="audio" title="25 | 设计模式：每一种都是一个特定问题的解决方案" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9c/31/9c0df118ae01cc263b0a33b15bf75a31.mp3"></audio>

你好，我是郑晔！

今天，我们来聊聊设计模式。作为一个讲软件设计的专栏，不讲设计模式有些说不过去。现在的程序员，基本上在工作了一段时间之后，都会意识到学习设计模式的重要性。

因为随着工作经验的增多，大家会逐渐认识到，代码写不好会造成各种问题，而设计模式则是所有软件设计的知识中，市面上参考资料最多，最容易学习的知识。

但是，你也知道，设计模式的内容很多，多到可以单独地作为一本书或一个专栏的内容。如果我们要在这个专栏的篇幅里，细致地学习设计模式的内容就会显得有些局促。

所以，这一讲，我打算和你谈谈**如何理解和学习设计模式**，帮助你建立起对设计模式的一个整体认知。

## 设计模式：一种特定的解决方案

所谓模式，其实就是针对的就是一些普遍存在的问题给出的解决方案。模式这个说法起源于建筑领域，建筑师克里斯托佛·亚历山大曾把建筑中的一些模式汇集成册。结果却是墙里开花墙外香，模式这个说法却在软件行业流行了起来。

最早是Kent Beck和Ward Cunningham探索将模式这个想法应用于软件开发领域，之后，Erich Gamma把这一思想写入了其博士论文。而真正让建筑上的模式思想成了设计模式，在软件行业得到了广泛地接受，则是在《设计模式》这本书出版之后了。

这本书扩展了Erich Gamma的论文。四位作者Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides也因此名声大噪，得到了GoF的称呼。我们今天大部分人知道的23种设计模式就是从这本书来的，而困惑也是从这里开始的。

因为，这23种设计模式只是在这本书里写的，并不是天底下只有23种设计模式。随着人们越发认识到设计模式这件事的重要性，越来越多的模式被发掘了出来，各种模式相关的书先后问世，比如，Martin Fowler 写过[《企业应用架构模式》](http://book.douban.com/subject/4826290/)，甚至还有人写了一套 5 卷本的[《面向模式的软件架构》](/https://book.douban.com/series/14666)。

但是，很多人从开始学习设计模式，就对设计模式的认知产生了偏差，所谓的23个模式其实就是23个例子。

还记得我们前面几讲学习的设计原则吗？如果用数学来比喻的话，**设计原则就像公理**，它们是我们讨论各种问题的基础，而**设计模式则是定理**，它们是在特定场景下，对于经常发生的问题给出的一个可复用的解决方案。

所以，你要想把所有已知的模式统统学一遍，即便不是不可能，也是会花费很多时间的，更何况还会有新的模式不断地出现。而且，虽然《设计模式》那本书上提到的大部分设计模式都很流行，但**有一些模式，如果你不是编写特定的代码，你很可能根本就用不上**。

比如Flyweight模式，如果你的系统中没有那么多小对象，可能就根本用不到它；而 Visitor 模式，在你设计自己系统的时候也很少会用到，因为你自己写的类常常都是可以拿到信息的，犯不上舍近求远。

所以，**学习设计模式不要贪多求全，那注定会是一件费力不讨好的事**。

想要有效地学习设计模式，首先我们要知道**每一个模式都是一个特定的解决方案**。关键点在于，我们要知道这个模式在解决什么问题。很多人强行应用设计模式会让代码不必要地复杂起来，原因就在于他在解决的问题，和设计模式本身要解决的问题并不一定匹配。**学习设计模式不仅仅要学习代码怎么写，更重要的是要了解模式的应用场景**。

## 从原则到模式

设计模式之所以能成为一个特定的解决方案，很大程度上是因为它是一种好的做法，符合软件设计原则，所以，**设计原则其实是这些模式背后的东西**。

我们前面花了大量的篇幅在讲各种编程范式、设计原则，因为它们是比设计模式更基础的东西。掌握这些内容，按照它们去写代码，可能你并没有在刻意使用一个设计模式，往往也能写出符合某个设计模式的代码。

我给你举个例子。比如，在用户注册完成之后，相关信息会发给后台的数据汇总模块，以便后面我们进行相关的数据分析。所以，我们会写出这样的代码：

```
interface UserSender {
  void send(User user);
}

// 把用户信息发送给后台数据汇总模块
class UserCollectorSender implements UserSender {
  private UserCollectorChannel channel;
  
  public void send(final User user) {
    channel.send(user);
  }
}

```

同时，我们还要把用户注册成功的消息通过短信通知给用户，这里会用到第三方的服务，所以，我们这里要有一个APP的key和secret：

```
// 通过短信发消息
class UserSMSSender implements UserSender {
  private String appKey;
  private String appSecret;
  private UserSMSChannel channel;
  
  public void send(final User user) {
    channel.send(appKey, appSecret, user);
  }
}

```

现在，我们要对用户的一些信息做处理，保证敏感信息不会泄漏，比如，用户密码。同时，我们还希望信息在发送成功之后，有一个统计，以便我们知道发出了多少的信息。

如果不假思索地加上这段逻辑，那两个类里必然都会有相同的处理，本着单一职责原则，我们把这个处理放到一个父类里面，于是，代码就变成这样：

```
class BaseUserSender implements UserSender {
  // 敏感信息过滤
  protected User sanitize(final User user) {
    ...
  }
  
  // 收集消息发送信息
  protected void collectMessageSent(final User user) {
    ...
  }
}

class UserCollectorSender extends BaseUserSender {
  ...
  
  public void send(final User user) {
    User sanitizedUser = sanitize(user);
    channel.send(sanitizedUser);
    collectMessageSent(user);
  }
}

class UserSMSSender extends BaseUserSender {
  ...
  
  public void send(final User user) {
    User sanitizedUser = sanitize(user);
    channel.send(appKey, appSecret, user);
    collectMessageSent(user);
  }
}

```

然而，这两段发送的代码除了发送的部分不一样，其他部分是完全一样的。所以，我们可以考虑把共性的东西提取出来，而差异的部分让子类各自实现：

```
class BaseUserSender implements UserSender {
  // 发送用户信息
  public void send(final User user) {
    User sanitizedUser = sanitize(user);
    doSend(user);
    collectMessageSent(user);
  }
  
  // 敏感信息过滤
  private User sanitize(final User user) {
    ...
  }
  
  // 收集消息发送信息
  private void collectMessageSent(final User user) {
    ...
  }
}


class UserCollectorSender extends BaseUserSender {
  ...
  
  public void doSend(final User user) {
    channel.send(sanitizedUser);
  }
}


class UserSMSSender extends BaseUserSender {
  ...
  
  public void doSend(final User user) {
    channel.send(appKey, appSecret, user);
  }
}

```

你是不是觉得这段代码有点眼熟了呢？没错，这就是Template Method的设计模式。我们只是遵循着单一职责原则，把重复的代码一点点地消除，结果，我们就得到了一个设计模式。在真实的项目中，你可能很难一眼就看出当前场景是否适合使用某个模式，更实际的做法就是这样遵循着设计原则一点点去调整代码。

其实，只要我们遵循着同样的原则，大多数设计模式都是可以这样一点点推演出来的。所以说，**设计模式只是设计原则在特定场景下的应用**。

## 开眼看模式

学习设计模式，我们还应该有一个更开阔的视角。首先是要看到**语言的局限**，虽然设计模式本身并不局限于语言，但很多模式之所以出现，就是受到了语言本身的限制。

比如，Visitor模式主要是因为C++、Java之类的语言只支持单分发，也就是只能根据一个对象来决定调用哪个方法。而对于支持多分发的语言，Visitor模式存在的意义就不大了。

[Peter Norvig](http://norvig.com/)，Google 公司的研究总监，早在 1996 年就曾做过一个分享[《动态语言的设计模式》](http://www.norvig.com/design-patterns/)，他在其中也敏锐地指出，设计模式在某种意义上就是为了解决语言自身缺陷的一种权宜之计，其中列举了某些设计模式采用动态语言后的替代方案。

我们还应该知道，随着时代的发展，有一些设计模式**本身也在经历变化**。比如，Singleton 模式是很多面试官喜爱的一个模式，因为它能考察很多编程的技巧。比如，通过将构造函数私有化，保证不创建出更多的对象、在多线程模式下要进行双重检查锁定（double-check locking）等等。

然而，我在讲可测试性的时候说过，Singleton并不是一个好的设计模式，它会影响系统的可测试性。从概念上说，系统里只有一个实例和限制系统里只能构建出一个实例，这其实是两件事。

尤其是在DI容器普遍使用的今天，DI容器缺省情况下生成的对象就是只有一个实例。所以，在大部分情况下，我们完全没有必要使用Singleton模式。当然，如果你的场景非常特殊，那就另当别论了。

在讲语法和程序库时，我们曾经说过，一些好的做法会逐渐被吸收到程序库，甚至成为语法。设计模式常常就是好做法的来源，所以，一些程序库就把设计模式的工作做了。比如，Observer 模式早在1.0版本的时候就进入到 JDK，被监听的对象要继承自 [Observable](http://docs.oracle.com/javase/8/docs/api/java/util/Observable.html) 类就好，用来监听的对象实现一个 [Observer](http://docs.oracle.com/javase/8/docs/api/java/util/Observer.html) 接口就行。

当然，我们讲继承时说过，继承不是一个特别好的选择，Observable是一个要去继承的类，所以，它做得也并不好。从Java 9开始，这个实现就过时（deprecated）了，当然官方的理由会更充分一些，你要是有兴趣可以去了解一下。JDK中提供的替代方案是[PropertyChangeSupport](http://docs.oracle.com/javase/8/docs/api/java/beans/PropertyChangeSupport.html)，简言之，用组合替代了继承。

我个人更欣赏的替代方案是Guava的[EventBus](http://github.com/google/guava/wiki/EventBusExplained)，你甚至都不用实现一个接口，只要用一个Annotation标记一下就可以监听了。

Annotation可以说是消灭设计模式的一个利器。我们刚说过，语言本身的局限造成了一些设计模式的出现，这一点在Java上表现得尤其明显。随着Java自身的发展，随着Java世界的发展，有一些设计模式就越来越少的用到了。比如，Builder模式通过Lombok这个库的一个Annotation就可以做到：

```
@Builder
class Student {
  private String name;
  private int age;
  ...
}

```

而Decorator模式也可以通过Annotation实现，比如，一种使用 Decorator 模式的典型场景，是实现事务，很多Java程序员熟悉的一种做法就是使用Spring的Transactional，就像下面这样：

```
class Handler {
  @Transactional
  public void execute() {
    ...
  }
}

```

随着Java 8引入Lambda，Command模式的写法也会得到简化，比如写一个文件操作的宏记录器，之前的版本需要声明很多类，类似下面这种：

```
Macro macro = new Macro();
macro.record(new OpenFile(fileReceiver));
macro.record(new WriteFile(fileReceiver));
macro.record(new CloseFile(fileReceiver));
macro.run();

```

而有了Lambda，就可以简化一些，不用为每个命令声明一个类：

```
Macro macro = new Macro();
macro.record(() -&gt; fileReceiver.openFile());
macro.record(() -&gt; fileReceiver.writeFile());
macro.record(() -&gt; fileReceiver.closeFile());
macro.run();

```

甚至还可以用Method Reference再简化：

```
Macro macro = new Macro();
macro.record(fileReceiver::openFile);
macro.record(fileReceiver::writeFile);
macro.record(fileReceiver::closeFile);
macro.run();

```

所以，我们学习设计模式除了学习标准写法的样子，还要知道，随着语言的不断发展，新的写法变成了什么样子。

## 总结时刻

今天，我们谈到了如何学习设计模式。学习设计模式，很多人的注意力都在模式的代码应该如何编写，却忽略了模式的使用场景。强行应用模式，就会有一种削足适履的感觉。

设计模式背后其实是各种设计原则，我们在实际的工作中，更应该按照设计原则去写代码，不一定要强求设计模式，而按照设计原则去写代码的结果，往往是变成了某个模式。

学习设计模式，我们也要抬头看路，比如，很多设计模式的出现是因为程序设计语言自身能力的不足，我们还要知道，随着时代的发展，一些模式已经不再适用了。

比如 Singleton 模式，还有些模式有了新的写法，比如，Observer、Decorator、Command 等等。我们对于设计模式的理解，也要随着程序设计语言的发展不断更新。

好，关于设计模式，我们就先谈到这里。下一讲，我会和你讨论一些很多人经常挂在嘴边的编程原则，虽然它们不像设计原则那么成体系，但依然会给你一些启发性的思考。

如果今天的内容你只能记住一件事，那请记住：**学习设计模式，从设计原则开始，不局限于模式。**

<img src="https://static001.geekbang.org/resource/image/4f/36/4f01c17b5509c29085b166a7ccec6c36.jpg" alt="">

## 思考题

最后，我想请你谈谈你是怎么学习设计模式的，你现在对于设计模式的理解又是怎样的。欢迎在留言区分享你的想法。

感谢阅读，如果你觉得这一讲的内容对你有帮助的话，也欢迎把它分享给你的朋友。
