设计模式怎么学?一份来自实践者的系统思考

[an error occurred while processing the directive]

在早前重新梳内容稍长一些,但都是我结合实践总结的经验与思考,如果你也对设计模式感兴趣,欢迎耐心读完。

提到设计模式,其实有很多话题可以展开。比如,在过去的面试过程中,我经常会问候选人一些与设计模式相关的问题,结果发现不少人对设计模式的理解存在一个常见误区:

过于关注模式的“样子”,却忽略了它试图解决的“问题”。

也就是说,很多人努力去记住某个模式的结构或类图,却没有真正理解设计模式的核心价值——解决特定场景下的设计问题。

本文将从一个设计模式学习者的角度,简要分享我在学习过程中的一些体会与心得,主要包括以下几个方面:

一、学习设计模式的“门槛”在深入设计模式之前,我们最好先掌握哪些基础知识?比如面向对象编程的基本原则、常见的设计原则(如 SOLID)、重构手法等。

二、学习设计模式的不同阶段回顾自己的学习过程,大致可以分为以下四个阶段:

熟悉阶段:了解常见的设计模式名称和基本结构;进阶阶段:尝试理解每个模式背后的动机与应用场景;实践阶段:在实际项目中主动识别并应用合适的模式;沉淀阶段:总结经验,思考模式在项目中的利弊与边界。

三、学习设计模式的小结最后,回顾一下常见的学习误区,比如“死记硬背结构”、“脱离上下文生搬硬套”等。理解这些问题,有助于我们更有效地掌握并运用设计模式。

01

🎯 学习设计模式的“门槛”从个人经验来看,在学习设计模式之前,具备以下三方面的基础知识会更为高效:

1. 面向对象编程(OOP)的基本概念以 Java 为例,如果你还不太清楚什么是封装、继承、抽象、多态,那么建议先深入理解这些核心特性。因为在设计模式中,尤其是抽象和多态的使用非常普遍,是很多模式实现的基础。

2. 统一建模语言(UML)的基础知识设计模式通常通过 UML 的类图和时序图来描述结构与行为,掌握这些图示有助于更直观地理解模式中的对象关系和交互过程。

例如,在讲解观察者模式时,类图能够清晰展示观察者与被观察者的继承结构,而时序图则能揭示它们之间的消息传递过程。

以下是 UML 类图中几种常见关系的简要说明:

继承 / 实现(Generalization / Realization)

表示一个类继承另一个类,或实现某个接口(即 is-a 关系)。关联 / 聚合 / 组合(Association / Aggregation / Composition)

表示类与类之间的“拥有”关系(即 has-a 关系),其中组合关系的耦合程度更强。依赖(Dependency)

表示一个类在某种程度上依赖另一个类的行为(即 uses-a 关系)。而时序图主要用于展示对象在运行时的交互顺序,对于理解行为型模式(如责任链、命令、状态等)尤其重要。

如果你不熟悉这些内容,不妨先通过一些 UML 教程、相关书籍或文章打好基础。尽管 UML 并不是学习设计模式的前提条件,但它无疑会帮助你更高效地理解设计意图与实现细节。

3. 一定的项目实践经验有过实际项目经验,会让你更容易在具体情境中感知“为什么需要某个设计模式”,而不是仅仅从书本上记住“它长什么样”。只有在面对真实复杂度时,模式的价值才会真正显现。

补充说明:

即使缺乏实际项目经验,也完全可以通过阅读优秀的源码来理解设计模式在真实场景中的应用方式。

重点是观察模式在什么场景下被使用,以及它们是如何解决具体问题的。推荐的学习材料包括:JDK、Spring、MyBatis、Guava 等开源项目,这些源码中大量运用了经典的设计模式,极具参考价值。

接下来,我想结合自己的经历,聊一聊我是如何一步步学习和理解设计模式的,从入门到实践,大致经历了以下几个阶段:

02

🎯阶段一、熟悉阶段我第一次接触设计模式,是在大学的《设计模式》课程上——当时是必修还是选修已记不清,老师选用的教材正是:

另外,我还专门购买了下面这本“砖头书”作为辅导资料。个人觉得这本书内容扎实、讲解深入,是一本非常值得参考的好书。

这个阶段的学习相对中规中矩,主要是为了应对考试的需要。课程内容涵盖了设计模式的起源、基本概念、构成要素、设计原则、分类方式,以及课堂上重点讲解的十个左右的典型设计模式。

📌 设计模式的由来设计模式最初源自建筑学领域,后来由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(合称 GoF:Gang of Four)将这一思想引入软件工程。1994 年,他们总结并发表了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,系统归纳了 23 种在软件开发中高频使用的设计模式。

他们的目标是通过模式建立一套通用语言,弥合面向对象方法在分析、设计和实现阶段之间的鸿沟。

📌 设计模式的定义与组成设计模式本质上是对重复出现问题及其解决方案的总结与抽象。每一个模式都描述了一个常见问题的上下文及其核心解决方案,使我们可以反复使用这些方案,而无需从头思考。

通俗地说,设计模式就是“前人踩过的坑”和“走通的套路”,它们体现了面向对象设计的最佳实践。合理使用模式,能够显著提升代码的可复用性、可扩展性和可维护性。

通常,一个设计模式由以下四个要素构成:

模式名称(Pattern Name)

用一到两个词概括模式所解决的问题及其解决方案,有助于开发人员之间的沟通与交流。大多数模式名称都来源于其功能或结构特征。问题(Problem)

描述模式适用的场景,包括在什么情况下使用该模式,以及背后所要解决的设计问题和动因。解决方案(Solution)

描述模式的结构组成、各部分之间的关系、各自的职责以及协作方式。通常通过 UML 类图与关键代码片段进行展示。效果(Consequences)

分析模式的优缺点,说明使用该模式可能带来的收益、影响,以及需要权衡的设计因素。📌 设计模式的分类虽然 GoF 提出的设计模式只有 23 个,但每个模式都针对一个可复用的设计问题,提供了成熟的解决方案。根据其用途和关注点的不同,设计模式通常被划分为三大类:

创建型模式(Creational Patterns)

关注对象的创建过程,旨在将对象的创建与使用解耦,提高系统的灵活性和可扩展性。

👉 典型代表:单例模式、工厂方法、抽象工厂、建造者、原型模式。结构型模式(Structural Patterns)

关注类与对象的组成结构,旨在通过灵活的组合方式实现更高效的模块协作。

👉 典型代表:适配器、装饰器、代理、桥接、组合、外观、享元模式。行为型模式(Behavioral Patterns)

关注对象之间的通信与职责分配,旨在提高对象间协作的灵活性。

👉 典型代表:观察者、策略、责任链、命令、模板方法、状态、迭代器等。设计原则

更多关于设计模式原则的内容,可以参考我的文章《漫谈模式之几大设计原则》。

问题

在这个阶段,一下子好多设计模式要去接受,其实挺容易遗忘的。可以结合生活的一些示例来加深记忆。

初学阶段,需要同时接触许多设计模式,信息量较大,容易产生遗忘。为了加深理解和记忆,可以结合生活中的实际例子进行联想和学习。

比如:

建造者模式

适配器模式

中介者模式

模版方法模式

享元模式

备忘录模式

... ...

📌 阶段体感经过这个阶段的学习,对设计模式已有了较为宏观的认知,能够识别出许多模式的基本结构和用法。然而,整体理解仍停留在“知道怎么写”的层面,缺乏对设计动机、适用场景和优劣权衡的深入思考。

以两个常见模式为例:

✅ 单例模式(Singleton)了解了单例的基本实现方式,例如:

使用 private 构造函数防止外部实例化;提供静态方法 getInstance() 获取唯一实例;区分懒汉式和饿汉式的实现;懒汉式中可能使用 synchronized 保证线程安全;优化方式如 DCL(Double-Checked Locking,双重检查锁),配合 volatile 关键字防止指令重排等。📖 推荐阅读:《漫谈模式之单例模式 —— 多种实现方式的思考》

✅ 建造者模式(Builder)理解了通过 Builder 构建一个完整对象的过程,知道各个组件如何组装,以及如何分离构造过程与表示逻辑。

📖 推荐阅读:《漫谈模式之建造者模式 —— 由来与通用写法》

... ...

至于为什么要这样?用在什么场景中?.... ? 缺少对模式真正在解决的问题的认识。

03

🎯阶段二、进阶阶段进入这一阶段后,我开始带着更多“为什么要这么做?”的问题去深入理解设计模式,尝试真正体会每种模式背后的设计思想与适用场景。为了加深理解,我主要通过以下几个方式进行学习:

1. 阅读源码 —— 在真实场景中感受模式的应用选择了多个成熟的开源项目,如 JDK、Spring、MyBatis、Guava、JUnit 等,从中观察设计模式的实际应用。通过阅读源码,不仅可以看到模式在真实开发中的使用方式,还能理解它们解决了哪些具体问题、为何如此设计。

例如:

👉 在 JDK 中,Runtime.getRuntime() 就是典型的单例模式实现。

👉 在 Spring 的 Bean 生命周期管理中,可以看到工厂模式、模板方法模式、代理模式的灵活组合应用。

如:

饿汉式单例

JDK源码中的Runtime就是饿汉式单例。

枚举单例

Guava包的MoreExecutors类中,可以看到枚举单例的示例。

内部类单例装饰者模式

MyBatis中的Cache采取的就是装饰者模式

访问者模式

jsqlparser比较典型的就是访问者模式

... ...

因此,我们可以在许多优秀的开源项目中看到设计模式的身影,这些实际案例有助于我们更直观地理解模式在不同场景下的应用方式与设计意图。

2. 思考扩展性 —— 主动分析设计选择在这个阶段,我还带着很多“为什么”去理解。比如:

🔍 对单例模式的更多实现尝试随着理解的深入,我开始关注不同场景下单例模式的实现差异,尤其是在并发环境下的安全性与性能权衡。

例如,自从 JDK 1.5 引入了 java.util.concurrent 并发包之后,我们可以尝试使用 Lock 来实现线程安全的懒加载单例:

是否可以通过CAS来实现呢?此种场景,可能会创建多个实例,但是只有一个实例会返回。

在早期JDK不支持volatile的时候,ThreadLocal也是解决D.C.L不足的一种方式。

MyBatis的ErrorContext使用ThreadLocal来完成的,其确保了线程中的唯一。

单例如何保证只创建一个对象?

以Java为例,创建对象除了New之外,还可以通过反射Reflection、克隆Clone、序列化/反序列化完成,所以,如果要保证真正只有一个对象,需要规避这些“陷阱”。

漫谈模式之单例模式(破坏和防护的思考)

... ...

3. 记录笔记 —— 梳理思路、沉淀理解比如,在阅读 Guava 源码时,我注意到 Suppliers 类中有一个 memoize 方法,其本质上也是一种懒加载的单例实现。发现这一点后,我便把它记录下来,作为对单例模式应用的一种实际补充案例。

另外,也可以通过ppt在组内分享或者写Blog/公众号来加强设计模式的记忆。如:

分享一个PPT: 聊聊设计模式

漫谈模式之创建型模式小结

漫谈模式之结构型模式小结

回过头看,感觉当时写的真的好简单。

📌 阶段体感进入这个阶段之后,我对设计模式的理解更加深入,不仅掌握了其结构和实现方式,也逐渐体会到每种模式真正解决的问题以及适用的场景。与此同时,也开始带着更多思考去探究背后的设计哲学。

比如:

简单的单例和静态方法有什么区别?使用单例的优势在哪里?访问者模式的本质是什么?它为什么要引入 double dispatch(双重分派)?……等等。这些问题促使我不再满足于“用法”,而是进一步追问“为什么要这样设计?还有没有其他可能?”,也正是在这样的反思中,设计模式的“味道”才真正开始显现出来。

04

🎯阶段三、实践阶段模板方法模式

曾经在实际项目中遇到过这样一个场景:我们需要对接多个不同的税务扣缴渠道,每个渠道的输入报文格式各不相同,但整体的处理流程却高度相似。

代码语言:javascript复制1、必要的账号有效性检查

2、个别场景特殊性的验证

3、插入扣税记录

4、调用扣税服务(如dubbo)

5、根据不同的调用结果,处理不一样的逻辑

... ... 其他流程,如发送MQ消息如果每个渠道的交易逻辑都单独编写一套流程,不仅会造成大量冗余代码,还容易引发流程不一致、维护成本高等问题。

为此,我们需要从业务的共性出发,进行如下改造:

统一输入参数标准化

将各渠道交易的入参统一封装为一个标准对象 PayDetailRecord,作为整个处理流程中的核心数据结构。引入统一的模板流程

使用模板方法模式,将通用的处理流程抽象到父类中,不同渠道通过子类实现各自的个性化处理,主要集中在构建 PayDetailRecord 的部分。...

下面是一个简化的伪代码示例:

统一扣税对象

代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;

import java.io.Serializable;

public class PayDetailRecord implements Serializable {

private static final long serialVersionUID = 8693597750287467725L;

... ...

}模版

代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;

public abstract class AbstractPayTaxTemplate {

public void handle(PayCallback callback, PayContext payContext, PayDetailRecord record) {

//1、必要的账号有效性校验

this.processBasicValidation(payContext, record);

//2、子类个性化校验

this.processCustomizedValidation(payContext, record);

//3、插入扣税记录日志

this.insertPayRecordLog(payContext, record);

//4、调用扣税是服务, 如dubbo服务

int result = this.callPayService(callback, payContext, record);

/**

* 5、根据不同的调用结果,处理不一样的逻辑

*/

if(result == 0) {

//调用结果未知, 插入一个异步任务继续重新去调度调用扣税服务

this.insertAsyncTask(payContext, record);

}else {

//使用callback

callback.updatePayResult(payContext, record, result);

}

... ...

}

//public abstract void doUpdatePayResult( PayContext payContext, PayDetailRecord record, int result);

protected void processBasicValidation( PayContext payContext, PayDetailRecord record) {

System.out.println("AbstractPayTaxTemplate# processBasicValidation ... ... ");

}

protected void processCustomizedValidation( PayContext payContext, PayDetailRecord record) {

//留空,子类根据需要重写即可

//System.out.println("AbstractPayTaxTemplate# processCustomizedValidation ... ... ");

}

protected void insertAsyncTask( PayContext payContext, PayDetailRecord record) {

System.out.println("AbstractPayTaxTemplate# insertAsyncTask ... ... ");

}

protected void insertPayRecordLog( PayContext payContext, PayDetailRecord record) {

System.out.println("AbstractPayTaxTemplate# insertPayRecordLog ... ... ");

}

protected int callPayService(PayCallback callback, PayContext payContext, PayDetailRecord record) {

System.out.println("AbstractPayTaxTemplate# callPayService ... ... ");

return 1;

}

}

回调函数

代码语言:javascript复制package com.wangmengjun.learning.designpattern.cases.template.v2;

public interface PayCallback {

void updatePayResult( PayContext payContext, PayDetailRecord record, int result);

}

简单示例:

代码语言:javascript复制

public class Main {

public static void main(String[] args) {

AbstractPayTaxTemplate template = new Trade001();

template.handle(new PayTaxCallback(), new PayContext(), new PayDetailRecord());

}

}经过这样的改造后,每个渠道的扣税交易只需继承统一的模板类(如 AbstractPayTaxTemplate),并根据需要重写个性化的方法即可。这样不仅减少了重复代码,也大大提升了系统的可扩展性与可维护性。

这正是模板方法模式的一种典型应用:提炼通用流程,封装变化点,在复用的同时保留必要的灵活性。

再举一个例子:

某次,开发团队搭建了一个统一的日志中心,希望各个应用在日志输出上保持一致性。为此,他们在设计日志工具类 LogUtil 时,并不提供传统的 info(String message) 方法。

为什么?

因为单个字符串参数往往难以满足实际业务中多样的日志需求,尤其是当不同应用对日志字段的关注点各不相同时,简单的多参数方法也会显得力不从心。

于是,日志中心统一定义了一个标准的 LogRecord 对象,来承载结构化日志信息。该对象包含了如:

traceId(链路追踪)bizType(业务类型)elapsedTime(耗时)……等关键字段。通过统一封装后的 LogRecord,不仅便于日志分析、归档、监控,还支持各业务模块灵活扩展字段,兼顾规范性与灵活性。

代码语言:javascript复制public class LogUtils {

... ...

public static void info(LogRecord record) {

... ...

}

... ...

}此时,LogRecord 的构建就可以采用建造者模式(Builder Pattern)来实现,使各应用能够根据自身需求灵活设置不同字段,既清晰又具备良好的可扩展性。

当然,设计模式的应用远不止于此,还有很多典型的场景值得探讨。本文不再逐一展开,后续将通过其他专题文章进行深入分享。

05

🎯阶段四、沉淀阶段📌 逐步沉淀,形成自己的理解体系进入这一阶段后,更重要的是对所学内容进行归纳整理,逐步构建出一套属于自己的设计模式认知框架。以创建型设计模式为例,我的理解如下:

🏭 工厂模式(Factory Pattern)关注创建什么对象(What)。

工厂方法模式通过抽象类与多态机制,让子类决定具体创建哪种对象,具备良好的扩展性。抽象工厂模式进一步对“工厂”本身进行抽象,用于创建一组相关联的对象,适用于多产品族场景。

🧱 建造者模式(Builder Pattern)关注如何一步步构建对象(How)。

通过分步骤设置属性,适用于创建过程复杂、属性较多且存在构建顺序要求的对象。

🔒 单例模式(Singleton Pattern)确保只创建一个对象(Only One)。

线程范围内的单例可通过 ThreadLocal 实现;在分布式环境中,则需借助分布式锁等机制确保全局唯一性。

🧬 原型模式(Prototype Pattern)通过克隆(Clone)的方式创建对象。

适用于对象创建成本高、需要频繁复制的场景,尤其当对象结构复杂时,使用原型可提升性能。

... ...

📌 持续扩展,补充设计模式知识体系可以将设计模式理解为在特定问题中反复验证有效的解决“套路”。随着业务复杂度的提升和系统架构的演进,GoF 提出的 23 种经典模式已经难以覆盖所有实际场景。

因此,在 GoF 模式的基础上,业界也不断衍生出一系列具有指导意义的“非经典”模式,用于解决更细分或更现代化的问题。例如:

Null Object 模式:避免出现空指针判断,通过返回一个“什么也不做”的对象来简化逻辑。Servant(雇工)模式:将多个对象共有的行为抽离到独立的“服务类”中,降低代码重复。Specification(规则)模式:将业务规则封装为独立对象,并支持规则组合,用于提升系统的灵活性与可读性。无环访问者模式(Acyclic Visitor):是对传统访问者模式的改良,解决访问者与被访问者之间强耦合的问题。……

这些补充型模式为我们提供了更多维度的思考方式,在面对复杂或新型业务需求时,能够帮助我们找到更加合适的设计方案。

📌 通用方法

比如单例模式是否可以定义一个抽象类来实现,把创建类型交由子类实现,如:

代码语言:javascript复制public abstract class Singleton {

private T mInstance;

protected abstract T create();

public final T get() {

synchronized (this) {

if (mInstance == null) {

mInstance = create();

}

}

return mInstance;

}

}再比如:观察者模式,能不能用一个抽象类呢? 多个主题topic,可以采用ConcurrentHashMap来存储,等等实现。

... ...

06

🎯学习设计模式小结不知不觉已经写了不少内容,最后再补充几点关于常见误区与学习建议,希望能对大家有所帮助。

📌 常见误区模式不是万能的

每种设计模式都有其特定的适用场景,并非所有问题都需要引入模式。适合的才是最好的,盲目使用只会增加设计复杂度。理解模式本质,而非死记结构

学习设计模式的关键在于弄清楚它试图解决的核心问题,而不是仅仅记住它“长什么样”。不要急于“套模式”

面对一个业务场景时,先弄清楚问题本质和边界,再考虑是否可以用某种设计模式来优化设计。如果强行套用,只会南辕北辙。

简而言之:先理解业务,再思考设计,再决定是否用模式。

📌 学习建议循序渐进,反复回顾

设计模式的理解是逐步深化的过程。随着项目经验和业务场景的积累,每次回顾都会有不同的体会。保持好奇心,主动交流

多和他人探讨设计思路。别人的实现方式和设计选择,可能正好是你知识体系的补全点。多读源码,勤做笔记

阅读优秀开源项目(如 JDK、Spring、MyBatis 等)能帮助你更直观地理解设计模式在真实业务中的应用。遇到经典用法,及时记录下来——好记性不如烂笔头。扎实基础,比模式更重要

当你真正理解了面向对象四大特性(封装、继承、抽象、多态),掌握了基本的设计原则,并具备对业务本质问题的抽象能力时,你会发现——在项目中你已经在自然地使用设计模式了。就先写到这里吧。如果你也在学习设计模式,欢迎留言交流心得,互相启发、共同进步。

/ END /

[an error occurred while processing the directive]

Copyright © 2088 星游活动站 - 新服开区专属福利 All Rights Reserved.
友情链接