《设计模式》是GoF(Gang of Four,四人帮)的经典著作,也是每个程序员的必读书。

这本书出版于1994年,到现在已经快三十年了。书中介绍的23种设计模式,至今仍是面向对象设计的基础。很多人说,设计模式已经过时了,现在是函数式编程、微服务、云原生的时代。但我觉得,设计模式的思想,永远不会过时。

我最近重读了这本书,有了很多新的体会。第一次读的时候,我只记住了模式的名字和UML图;这次重读,我开始理解模式背后的设计原则和思想。

本文分享我的读后感,包括设计模式的本质、常用模式的理解、对软件设计的思考、设计模式的局限,以及为什么三十年后这本书依然值得读。

一、这本书是什么

先简单介绍一下这本书。

1. 作者和背景

《设计模式》的作者是四位程序员:Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides,人称GoF(四人帮)。

这本书的全名是《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)。

90年代初,面向对象编程开始流行,但很多人写的面向对象代码,其实是"用面向对象语言写的面向过程代码"。GoF总结了前人的经验,把常见的面向对象设计技巧整理成了23种模式,帮助大家写出更好的面向对象代码。

2. 23种设计模式

书中介绍了23种设计模式,分为三类:

创建型模式(5种):

  • 工厂方法(Factory Method)
  • 抽象工厂(Abstract Factory)
  • 建造者(Builder)
  • 原型(Prototype)
  • 单例(Singleton)

结构型模式(7种):

  • 适配器(Adapter)
  • 桥接(Bridge)
  • 组合(Composite)
  • 装饰(Decorator)
  • 外观(Facade)
  • 享元(Flyweight)
  • 代理(Proxy)

行为型模式(11种):

  • 责任链(Chain of Responsibility)
  • 命令(Command)
  • 解释器(Interpreter)
  • 迭代器(Iterator)
  • 中介者(Mediator)
  • 备忘录(Memento)
  • 观察者(Observer)
  • 状态(State)
  • 策略(Strategy)
  • 模板方法(Template Method)
  • 访问者(Visitor)

3. 模式的要素

每个模式都包含四个要素:

  • 模式名称:一个简短的名字,方便交流
  • 问题:描述了什么时候使用模式
  • 解决方案:描述了设计的组成部分、它们之间的关系
  • 效果:描述了模式的优缺点和适用场景

这四个要素,构成了一个完整的模式描述。

二、设计模式的本质

重读之后,我对设计模式的本质有了新的理解。

1. 设计模式不是代码模板

很多人以为,设计模式就是代码模板,遇到问题就套一个模式,把代码写出来。

这是对设计模式的误解。设计模式不是代码模板,而是"解决问题的思路"。

同一个模式,在不同的语言、不同的场景下,代码实现可能完全不同。但核心的思路是一样的:如何组织代码,如何分配职责,如何应对变化。

2. 设计模式是"经验的总结"

设计模式,本质上是前人经验的总结。

GoF把那些反复出现的问题和解决方案,整理成了模式。这些模式,不是他们发明的,而是他们从大量的代码实践中总结出来的。

所以,学习设计模式,不是学习"新东西",而是学习"前人已经验证过的好方法"。

3. 设计模式的核心是"应对变化"

设计模式的核心目标,是应对变化。

软件需求总是在变。今天要加一个功能,明天要改一个逻辑。如果代码写得不好,每次变化都要改很多地方,很容易出bug。

设计模式,就是教我们如何组织代码,让代码更容易应对变化。通过封装、抽象、解耦,让变化的影响范围最小化。

4. 设计模式遵循的原则

设计模式背后,是几个面向对象的设计原则:

  • 单一职责原则:一个类只做一件事
  • 开闭原则:对扩展开放,对修改关闭
  • 里氏替换原则:子类可以替换父类
  • 接口隔离原则:接口要小而专
  • 依赖倒置原则:依赖抽象,不依赖具体

这五个原则,是SOLID原则。设计模式,就是这些原则的具体应用。

三、我最有体会的几个模式

说说我最有体会的几个模式。

1. 策略模式(Strategy)

策略模式是我用得最多的模式之一。

策略模式的核心是:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。

比如,一个电商系统,支付方式有微信支付、支付宝、银行卡。如果用if-else写,每加一种支付方式都要改代码。用策略模式,每种支付方式是一个策略类,新增支付方式只需要加一个类,不需要改原有代码。

# 策略模式示例
class PaymentStrategy:
    def pay(self, amount):
        pass

class WeChatPay(PaymentStrategy):
    def pay(self, amount):
        print(f"微信支付{amount}元")

class AliPay(PaymentStrategy):
    def pay(self, amount):
        print(f"支付宝支付{amount}元")

class Order:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def checkout(self, amount):
        self.strategy.pay(amount)

策略模式让我明白了:面对变化,不要用if-else,要用多态。

2. 观察者模式(Observer)

观察者模式也是非常常用的模式。

观察者模式的核心是:定义对象间的一对多依赖,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。

比如,用户注册成功后,要发欢迎邮件、发短信、送优惠券。如果把这些逻辑都写在注册方法里,注册方法会越来越臃肿。用观察者模式,注册成功后发一个事件,邮件、短信、优惠券的监听器各自处理。

现在的消息队列(Kafka、RabbitMQ)、事件总线,本质上都是观察者模式的延伸。

3. 装饰器模式(Decorator)

装饰器模式的核心是:动态地给一个对象添加额外的职责。

比如,一杯咖啡,可以加奶、加糖、加摩卡。如果用继承,会有无数个子类(咖啡+奶、咖啡+糖、咖啡+奶+糖……)。用装饰器模式,每个配料是一个装饰器,动态组合。

Python的装饰器语法,就是装饰器模式的语言级支持。Java的IO流(InputStream、BufferedInputStream、DataInputStream),也是装饰器模式的经典应用。

装饰器模式让我明白了:扩展功能,不一定要用继承,组合更灵活。

4. 单例模式(Singleton)

单例模式是最简单、也是最有争议的模式。

单例模式保证一个类只有一个实例,并提供全局访问点。

很多人滥用单例,到处都是单例,导致代码耦合严重、难以测试。但单例本身没有错,用在合适的场景(比如配置管理、连接池、日志)是很好的。

单例的实现方式也有很多:饿汉式、懒汉式、双重检查锁、静态内部类、枚举。每种方式都有优缺点。

单例模式让我明白了:没有不好的模式,只有用错地方的模式。

5. 外观模式(Facade)

外观模式的核心是:为子系统中的一组接口提供一个一致的界面。

比如,一个复杂的子系统,有很多个类和接口。客户端直接调用的话,需要了解很多细节。用外观模式,提供一个简单的接口,封装子系统的复杂性。

我们平时写的Service层、Controller层,某种程度上就是外观模式。外观模式让客户端和子系统解耦,也让子系统更容易使用。

四、对软件设计的思考

读《设计模式》,让我对软件设计有了更深的思考。

1. 设计是为了应对变化

软件设计的核心,是应对变化。

如果需求永远不变,那代码怎么写都行。但现实是,需求一直在变。好的设计,就是让代码更容易适应变化。

设计模式,就是教我们如何设计出容易变化的代码。通过抽象和封装,把变化的部分隔离出来,让变化不影响其他部分。

2. 好的设计是"简单"的

很多人以为,好的设计就是用了很多模式、很复杂的架构。但实际上,好的设计是简单的。

设计模式的目的,是简化代码,而不是复杂化。如果用了模式之后,代码更复杂了,那一定是用错了。

好的设计,是用最简单的方式,解决问题。模式只是工具,不是目的。

3. 设计是权衡的艺术

软件设计,没有绝对的对错,只有权衡。

  • 用策略模式,增加了类的数量,但提高了可扩展性
  • 用单例,方便了全局访问,但增加了耦合
  • 用观察者模式,实现了解耦,但增加了调试的难度

每个模式都有优缺点,选择哪个模式,取决于具体的场景和需求。没有最好的设计,只有最合适的设计。

4. 过度设计是大忌

设计模式用多了,容易过度设计。

为了用模式而用模式,为了"可扩展"而加了很多抽象层,结果代码变得复杂难懂,而那些"可能的扩展"永远不会到来。

好的设计,是恰到好处。够用就好,不要为了不确定的未来,过度设计现在的代码。

5. 设计是持续演进的

设计不是一次性的,而是持续演进的。

最开始,代码很简单。随着需求变化,代码变得复杂。这时候,再重构,引入设计模式。然后,需求继续变化,代码继续演进。

不要一开始就追求完美的设计。先写简单的代码,等需求变化了,再重构。这就是"敏捷设计"的思想。

五、设计模式的局限

客观地说,设计模式也有一些局限。

1. 面向对象的局限

设计模式是面向对象时代的产物,主要解决面向对象编程中的问题。

现在,函数式编程越来越流行。在函数式编程中,很多设计模式不需要了,或者有了不同的实现方式。比如,策略模式在函数式编程中就是传一个函数;观察者模式可以用响应式流。

但设计模式的思想(解耦、封装、应对变化),在函数式编程中依然适用。

2. 语言特性的发展

很多编程语言,已经把设计模式做成了语言特性。

  • Python的装饰器语法 = 装饰器模式
  • C#的事件 = 观察者模式
  • Java的枚举 = 单例模式
  • Rust的trait = 策略模式

当模式变成语言特性,我们就不需要手动实现了。但理解模式的原理,能帮助我们更好地使用语言特性。

3. 有些模式已经不常用了

23种模式中,有些现在已经不常用了。

比如,解释器模式、中介者模式、访问者模式,在日常开发中用得比较少。有些模式,因为语言和框架的发展,已经被更好的方式替代了。

但了解这些模式,依然有价值。至少在遇到特定场景的时候,你知道有这样一种解决方案。

4. 容易被滥用

设计模式最大的问题,是容易被滥用。

很多人学了设计模式之后,到处用模式,不管适不适合。结果代码变得复杂难懂,维护成本很高。

记住:模式是工具,不是目的。用最简单的方式解决问题,才是最好的设计。

六、为什么今天依然值得读

虽然这本书出版快三十年了,但今天依然值得读。

1. 思想不过时

技术会变,语言会变,框架会变,但设计的思想不会变。

如何应对变化?如何解耦?如何封装?如何写出可维护的代码?这些问题,是永恒的。设计模式给出的答案,今天依然有效。

2. 是程序员的共同语言

设计模式的名字,已经成为程序员的共同语言。

当你说"这里用策略模式",大家都明白你的意思。当你说"这是一个单例",大家都知道怎么回事。

这种共同语言,提高了沟通效率。读这本书,能让你和其他程序员在同一个频道上交流。

3. 帮助理解框架源码

很多主流框架,都大量使用了设计模式。

  • Spring:工厂模式、代理模式、观察者模式、模板方法模式
  • MyBatis:装饰器模式、代理模式、组合模式
  • JDK:迭代器模式、装饰器模式、观察者模式

读了设计模式,再看框架源码,就能理解作者为什么这么设计,而不是只看到一堆类和接口。

4. 提升设计能力

读设计模式,最终是为了提升自己的设计能力。

当你理解了23种模式,面对复杂的需求,你就有了更多的设计选择。你知道什么时候用什么模式,知道每种模式的优缺点,能做出更好的设计决策。

这种能力,是高级程序员和初级程序员的重要区别。

七、阅读建议

如果你打算读这本书,给你几个建议。

1. 不要死记硬背

不要试图记住23种模式的UML图和代码。记不住的,也没必要记。

重点是理解每个模式解决什么问题、核心思想是什么、什么时候用。理解了思想,代码可以自己写出来。

2. 结合实际项目

读的时候,结合自己的项目想想:这个模式在我的项目里能用吗?我以前写的代码,是不是可以用这个模式优化?

结合实际,理解会更深。

3. 先读常用的模式

23种模式,不需要一次读完。先读最常用的:

  • 创建型:工厂方法、单例、建造者
  • 结构型:适配器、装饰器、外观、代理
  • 行为型:策略、观察者、模板方法、迭代器

这些模式掌握了,其他模式用到的时候再查。

4. 读代码,重构代码

读完之后,找一段自己的旧代码,看看能不能用设计模式重构。

实践是最好的学习。通过重构,你会真正理解模式的价值。

5. 多读几遍

这本书,读一遍是不够的。

第一次读,记住模式的名字。第二次读,理解模式的思想。第三次读,能在实际中灵活运用。

不同的阶段读,会有不同的体会。

八、写在最后

《设计模式》是一本经典的书,也是一本容易被误解的书。

很多人觉得它过时了,觉得它太理论了,觉得它是面向对象时代的遗物。但当你真正读懂了,你会发现,它讲的不是模式,而是设计的思想。这些思想,在任何时代、任何语言下,都适用。

重读这本书,我最大的收获是:不再为了用模式而用模式,而是理解了模式背后的原则和思想。面对复杂的需求,我能更从容地选择合适的设计,而不是生搬硬套。

2022年了,软件行业变化很快。新语言、新框架、新架构层出不穷。但有些东西是不变的:如何写出好代码,如何设计出易维护的系统,如何应对变化。这些,正是《设计模式》教给我们的。

最后,用一句话总结我的感受:"设计模式不是银弹,而是工具箱。理解每个工具的用途,在合适的时候用合适的工具,才是真正的设计能力。"

愿我们都能写出优雅、可维护、易扩展的代码。