25 | 设计模式:每一种都是一个特定问题的解决方案

你好,我是郑晔!

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

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

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

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

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

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

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

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

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

但是,很多人从开始学习设计模式,就对设计模式的认知产生了偏差,所谓的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,Google 公司的研究总监,早在 1996 年就曾做过一个分享《动态语言的设计模式》,他在其中也敏锐地指出,设计模式在某种意义上就是为了解决语言自身缺陷的一种权宜之计,其中列举了某些设计模式采用动态语言后的替代方案。

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

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

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

在讲语法和程序库时,我们曾经说过,一些好的做法会逐渐被吸收到程序库,甚至成为语法。设计模式常常就是好做法的来源,所以,一些程序库就把设计模式的工作做了。比如,Observer 模式早在1.0版本的时候就进入到 JDK,被监听的对象要继承自 Observable 类就好,用来监听的对象实现一个 Observer 接口就行。

当然,我们讲继承时说过,继承不是一个特别好的选择,Observable是一个要去继承的类,所以,它做得也并不好。从Java 9开始,这个实现就过时(deprecated)了,当然官方的理由会更充分一些,你要是有兴趣可以去了解一下。JDK中提供的替代方案是PropertyChangeSupport,简言之,用组合替代了继承。

我个人更欣赏的替代方案是Guava的EventBus,你甚至都不用实现一个接口,只要用一个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(() -> fileReceiver.openFile());
macro.record(() -> fileReceiver.writeFile());
macro.record(() -> 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 等等。我们对于设计模式的理解,也要随着程序设计语言的发展不断更新。

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

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

思考题

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

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

精选留言

  • 桃子-夏勇杰

    2020-07-31 07:37:42

    从为了设计而设计,到为了解决问题而设计,这几乎是成熟程序员的必由之路。学费公司出,这也是我们这个行业的必由之路😂
    作者回复

    我不得不说,你说得对😂

    2020-07-31 09:34:14

  • giteebravo

    2020-08-05 08:28:32


    曾经买过一本《大话设计模式》,每晚必挑灯夜读之。然而现在,心中已无模式,只有原则。
    作者回复

    恭喜上了一个台阶!

    2020-08-05 09:33:41

  • 阳仔

    2020-07-24 10:13:56

    设计模式是对一些常见问题抽象后给出的特定解决方案,很多人或多或少都听说过或使用过设计模式,比如观察者模式,工厂模式,builder模式,单例模式,策略模式等等
    这些模式都遵循软件开发设计的SOLID原则,设计模式就是从这个原则推导出来,所以掌握基本的设计原则就理解了设计模式的基础,在实际编码中,不刻意使用设计模式但也可以写出某个设计模式相似的代码,我觉得这个才是“无招胜有招”的境界
    当然学习设计模式也能够更好帮助反过来思考这些模式背后的基本原则
  • 奔奔奔跑

    2020-07-24 23:28:28

    看老师的专栏太难受了!思考的乐趣与设计的美感,每每反思自己的代码,总感觉心痒痒的,恨不得把所有相关代码都看一遍,了解各种框架,各种中间件是怎么去做的,去实践的。
    作者回复

    别控制,大胆去做!

    2020-07-25 09:39:06

  • 李文通

    2020-09-15 18:04:36

    第一阶段: 为了学设计模式而学设计模式。一段时间学代码觉得用不是很上。到后面觉得代码改动似乎很大,而且就和之前课程提到的一样,把面向对象写成了面向过程。 然后就是有意识的搬设计模式中的内容。有些场景下会使用设计模式,看着像是符合了某种设计原则。但其实自己还是处于无知的状态。

    第二阶段:通过课程对设计原则有了更深刻的理解之后。更能明白设计模式的出发点及用意。就如课程所说的,设计原则是背后的东西。设计模式仅是针对某种场景的具体例子。 希望能够深刻理解到设计原则,自己能够基于场景推导设计模式,或是基于模式及原则推导可能适用的场景。 做到“心法” 和 “招式”的对应,并且能够灵活运用。甚至是在找不到合适“招式”的时候,也能够自己推演出来。
  • 猪肉怂

    2021-06-03 22:42:31

    之前就曾经叹服于一些设计模式,比如工厂模式,到了动态语言中会有那么简洁的实现,简洁到甚至感觉不到模式的存在,原来设计模式只是为了通往更好设计的解决方案。当语言的本身掣肘随着发展而被祛除,设计模式也就完成了它们的使命。

    当一些设计模式最终更好的开发语汇所替换掉,模式背后的设计原则,将会永远存在下去。

    这就是郑老师所讲的根基与枝叶的关系吧。
    作者回复

    你的理解很到位

    2021-06-06 18:28:13

  • 佟宏元

    2020-11-21 21:55:30

    平时使用过单例,策略,观察者等模式,从一开始纠结于如何按照规定编写代码,到后面已经脱离了编码层面的使用,我总结出,学习设计模式,学习的不是如何编写代码,而是一种软件设计思想,比如什么场景使用策略模式,什么场景使用观察者模式,甚至是已经完全不体现在代码层面,而是对系统的功能的设计,对业务场景的梳理。所以我觉得设计模式最后的样子,是一种解决业务,设计软件的模型。
    作者回复

    于是,你就升华了。

    2021-01-31 21:19:13

  • Janenesome

    2020-10-24 11:38:13

    Template Method 这个模式用得比较多,因为只需要将不同的部分封装到子类,场景比较清晰。其他的设计模式就用得比较少。

    这个专栏给出的原则挺多,准备一个一个来践行,因为一下子体会到全部有点难,得一边写代码一边对照思考,慢慢改变自己的编程思想。
    作者回复

    可以用这些原则一个一个过一遍设计模式。

    2021-02-02 16:54:00

  • 木头发芽

    2020-10-10 11:45:20

    跟独孤九剑一个道理,剑式就那么九个,怎么解决敌人就看具体是什么问题,在这九式的指导下见招拆招就可以
    作者回复

    是的,理解得越深入,运用得越纯熟。

    2020-10-20 08:30:28

  • Keith

    2020-08-03 17:37:44

    设计原则对是抽象, 设计模式为具体的实现, 但又不是简单的继承, 而是通过组合等方式实现
  • 2020-07-24 15:11:54

    设计模式反复看过很多次,可能和我做Bs架构的管理系统有关,只用过单例,消费者,生产者,然后是简单工厂。其他没用过
    作者回复

    还是有必要全面了解一下的。

    2020-07-24 18:39:23

  • Jxin

    2020-07-24 13:11:34

    我觉得应该是从设计模式开始,到深入理解设计原则。反过来,很看天赋,难。

    1.把设计模式抄熟,多用。学会识别设计模式要解决的问题场景。领会该设计模式在该问题场景的应用是基于什么设计原则的考量。

    2. 设计模式和设计原则可类比成太极拳的拳法与神,以模仿拳法入门,以领悟神韵进阶。拳、劲、神->模式、场景、原则。那么怎么练好呢?熟而渐悟懂劲,由懂劲而阶及神明。然非用力之久不能豁然贯通焉。

    歧义点:
    ef java那本书也有提到,单例双重检验是个失败案例。毕竟指令重排,并发线程可能拿到有内存地址的空对象,进而会存在空指针异常。
  • 6点无痛早起学习的和尚

    2023-09-26 08:48:41

    之前看过好几种的设计模式课程,每次都是草草收场,看不懂或者一看就会一用就废,导致了就不再看设计模式了,但是又很纠结设计模式可以写出来优美的代码,在学/不学设计模式中纠结,看到了这节,我觉得应该好好学一学设计原则

    看到这个我想到了什么,想到了摄影,设计原则就像摄影的原则:光线、构图等等,设计模式就像:拍人要把脚放在九宫格的下面
  • ifelse

    2022-05-19 21:15:57

    学习设计模式,从设计原则开始,不局限于模式。--记下来
  • java小霸王

    2022-05-09 12:57:47

    23种设计模式,可以分为大概几类 创造型 结构型 行为型
  • Nio

    2022-04-17 10:08:01

    设计原则是公理,是基础,设计模式是定理,是应用
  • aoe

    2021-11-02 09:48:57

    怪不得老师花了很多篇幅讲设计原则,好有道理
  • escray

    2020-09-27 15:43:03

    那个《面向模式的软件架构》实在是太狠了,居然有 5 卷,并且还得了 Jolt 大奖。

    这篇专栏可以作为学习设计模式的一个开篇词。

    之前也学习过 23 中设计模式,但是在写程序的时候其实用的并不多,时间长了,连 23 个模式的名字都记不全。

    有时候按照 SOLID 原则去重构一些代码,总感觉好像是对应了某一个设计模式,但是又似是而非,需要到隔壁专栏复习一下设计模式。

    “看眼看模式”部分,讲到程序设计语言的发展使得一些模式有了新的写法,我有点好奇,在业界究竟有多少公司能够跟进语言的演变。
    作者回复

    写代码很多时候,是一个人,或一个团队的事,整个行业的演变速度是非常慢的。

    2021-01-27 14:19:11

  • 2020-09-02 01:39:20

    感觉悟了;如果没有理解分离关注点和dip;很可能就在业务处理流程中直接调用 UserCollectorChannel的send方法了;都不会想到抽象出usersend接口。
    作者回复

    有收获就没白学

    2021-01-27 14:20:02