开头
上次在内部技术分享的时候,我让团队里几个刚工作一两年的同学说说自己用过哪些设计模式。结果很有意思,大家脱口而出的基本上都是单例、工厂、策略这三个,再往后就要想很久。面试的时候人人能背出二十三(GoF)种模式的名字,可真到了写代码的时候,能主动用上的其实很少。
这个现象我特别理解。我刚工作那会儿也是这样,觉得设计模式是一个个孤立的"标准答案",学一个记一个,用的时候却对不上号。后来代码写多了,踩的坑多了,再回头看才发现:设计模式不是背出来的,是"长"出来的——当一个变化反复出现,当一个需求改起来越来越痛,你就会自然想到某种模式。它本质上是一套应对"变化"的成熟思路,是前人把"怎么改代码才不痛苦"这个问题的答案沉淀了下来。
这篇文章把我这些年工作中真正高频用到的模式做一个梳理。不追求面面俱到把二十三种都讲一遍,而是挑那些在真实项目里出现频率最高、解决痛点最直接的模式,讲清楚它们解决的问题、适用场景、常见误用,以及在新兴的AI编程(尤其是多Agent协作)场景下,这些经典模式又是怎么变形的。无论你是在准备面试、写课程作业,还是想在真实项目里把代码写得更好维护,这篇应该都能给你一些参考。
1. 先回答一个问题:设计模式到底解决的是什么
很多人学设计模式会陷入一个误区——把模式当成"高级技巧",觉得用了设计模式的代码才是好代码。这个认知偏差非常普遍,也导致了很多项目里出现"为了模式而模式"的过度设计。
1.1 找变化点,封装变化
设计模式解决的核心问题,用一句话概括就是:找到变化点,封装变化。它不是为了让代码看起来更复杂或更"高级",而是为了让未来改动时影响范围最小、成本最低。
举个最简单的例子。你写了一个订单金额计算的方法,一开始只有普通订单,代码特别直白:
java复制public double calculate(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
return total;
}
后来需求变了,会员打九折。你加了一行:
java复制if (order.isMember()) {
total *= 0.9;
}
再后来,节假日满减、VIP折上折、新用户首单特惠……这个calculate方法越来越长,if判断越堆越多,每次改促销规则都要动这个方法,改完还得担心影响其他逻辑。这时候你就触碰到了"变化点"——促销规则在变。而策略模式就是专门解决这类问题的:把变化的算法抽出来,让主流程不感知具体规则。
这就是设计模式价值最直接的体现:它让你的代码在频繁变化的需求面前,依然能保持稳定。谁在变,就把谁封装起来,然后通过组合、委托、接口来桥接稳定和变化。
1.2 模式的三个层次:思想、原则、招式
我把设计模式的学习分成三个层次,这个分层帮我自己理清了很多困惑:
| 层次 | 内容 | 作用 |
|---|---|---|
| 思想层 | 封装变化、面向接口编程、组合优于继承 | 指导性最强,是"道" |
| 原则层 | 单一职责、开闭原则、最少知识原则、依赖倒置等SOLID原则 | 连接"道"与"术"的桥梁 |
| 招式层 | 二十三(GoF)种设计模式 | 具体的代码结构,是"术" |
很多初学者一上来就背模式本身(招式层),但缺少思想和原则的支撑。这就导致一个问题:知道策略模式是什么样子,但面对一个具体业务场景时判断不出来该不该用。掌握设计模式的核心路径,是先理解"面向对象设计原则",然后在大量真实代码里反复感受"变化在哪里",最后模式自然就会浮现出来。
比如开闭原则(对扩展开放,对修改关闭)——理解了这条原则,你再看装饰器模式、策略模式、观察者模式,会发现它们都是"不修改已有代码就能增加新能力"的具体实现手段。思想通了,招式就活了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 创建型模式:最常被用错也最常被需要的三个
创建型模式解决的问题是"怎么创建对象"。这一块很多人觉得简单——不就是new一个对象吗?但实际项目里,"创建对象"这件事的复杂度经常被严重低估。
2.1 单例模式:不是用来"省内存"的
单例模式大概是争议最大的一个模式。很多人觉得它就是个"全局变量",是不好的设计。但我要说:单例本身不可怕,可怕的是把什么东西都做成单例。
单例真正的使用场景是"共享状态"和"全局唯一",而不是"省内存"。一个配置文件加载器、一个数据库连接池、一个线程池,这些对象在进程里确实只需要一份,而且需要被多处共享访问,这时候单例是合理的。
我见过最典型的使用误区,是有人拿单例来"减少对象创建的开销"。比如创建一个查询用户信息的Service,明明是无状态的逻辑对象,非要写成单例。这完全没有必要——无状态对象创建成本极低,GC完全能处理,用单例反而带来了全局共享的隐患。真正值得单例的,是有状态的、创建成本高的、需要线程安全的资源对象。
实现方式上,我推荐饿汉式或者静态内部类式,日常项目不要用双重检查锁。原因很简单:双重检查锁要考虑volatile、要考虑性能,写错概率高,收益却几乎为零。Java里枚举单例是Effective Java里推荐的写法,但实际项目中我见到的还是静态内部类最多:
java复制public class ConfigManager {
private ConfigManager() {
// 加载配置
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
private static final ConfigManager INSTANCE = new ConfigManager();
}
}
这种写法好处是:延迟加载、线程安全、代码简单。而且构造方法私有化,杜绝了外部new的可能。
2.2 工厂系列:把"创建逻辑"从业务里剥出去
工厂模式是最容易被低估的模式。很多人觉得工厂不就一个if-else返回不同对象吗,有什么好讲的?但恰恰是这个if-else,放对了位置能让代码结构无比清晰,放错了就是灾难。
工厂有三种形态,我工作中用到最多的是简单工厂和工厂方法,抽象工厂相对少一些。
简单工厂适合产品种类不那么多、创建逻辑比较集中的场景。比如一个消息推送服务,要根据渠道创建对应的Sender:
java复制public class SenderFactory {
public static Sender create(String channel) {
if ("sms".equals(channel)) {
return new SmsSender();
} else if ("email".equals(channel)) {
return new EmailSender();
} else if ("push".equals(channel)) {
return new PushSender();
}
throw new IllegalArgumentException("不支持的渠道: " + channel);
}
}
这个代码本身没什么技术含量,它的价值在于:当你要新增一个"钉钉"渠道时,只改这一个工厂类,业务调用方完全不需要动。如果这个if-else散落在业务代码各处,每新增一个渠道就要把所有调用点改一遍,那才是灾难。
工厂方法模式则是在"需要创建一批相关产品"的场景出现。比如你接入了多个支付渠道,每个渠道有各自的支付请求对象、回调处理对象、对账对象,这时候用抽象工厂的思路把"一家支付渠道"封装成一个族,业务层只面对"支付渠道"这一个抽象。
需要提醒的是:不要为了用工厂而用工厂。如果创建逻辑只是一个简单的new,没有任何变化趋势,那直接用构造器就是最好的设计。工厂的价值在于它把"变化的创建逻辑"集中管理,而不是把"不变的创建过程"复杂化。
2.3 建造者模式:参数多到构造函数写不下去的时候
建造者模式在真实代码里出现频率非常高,但大多数情况下用的是它的简化形态——Lombok的@Builder或者Kotlin的具名参数。我几乎没有手写过完整的建造者类,因为现代语言和工具已经把这个模式内化了。
但它背后的思想值得理解:当一个对象的构造参数很多、且很多参数有默认值时,构造函数和setter都不是好方案。构造函数会导致调用方必须记住参数顺序,可读性差;setter则让对象处于"可修改"状态,违背了不可变性。
建造者模式的本质是通过一个独立的Builder对象,让参数设置变得可读、可链式调用,最后统一构建一个不可变对象。这在配置类、复杂领域对象、不可变数据传输对象(DTO)上特别适用。
比如一个报表查询条件对象,可能有时间范围、维度列表、筛选条件、排序方式、分页信息等等,直接构造函数签名叫人根本看不懂:
java复制ReportQuery query = ReportQuery.builder()
.startDate("2024-01-01")
.endDate("2024-12-31")
.dimensions(Arrays.asList("渠道", "地区"))
.filters(someFilter)
.sortBy("revenue")
.build();
一眼就知道在配置什么。这就是建造者模式在现代工程里的价值——它不复杂,但极大提升了代码的可读性和使用体验。
3. 结构型模式:把不匹配的接口和职责"粘"起来
结构型模式关注的是"类与对象如何组合成更大的结构"。这个板块里,适配器、装饰器、代理是我工作中用到最多的三个,它们的共同特点是:都在解决"现有代码和新需求不匹配"的问题,但切入点完全不同。
3.1 适配器模式:最像"翻译官"的模式
适配器模式的经典比喻就是插头转换头——你有一个Type-C的设备,但只有USB-A的接口,那就需要一个转接头把两者对接起来。代码世界里,适配器用于解决"接口不兼容"的问题。
工作中最常见的场景是接入第三方SDK。比如你定义了一套统一的支付接口:
java复制public interface UnifiedPay {
PayResult pay(PayRequest request);
PayResult refund(RefundRequest request);
}
但微信支付SDK和支付宝SDK的签名、参数、返回结果都不一样。这时候你需要为每个渠道写一个Adapter,把第三方SDK的调用包装成统一接口:
java复制public class WechatPayAdapter implements UnifiedPay {
private final WechatSdk wechatSdk;
@Override
public PayResult pay(PayRequest request) {
WechatRequest wxReq = convert(request); // 做参数转换
WechatResponse wxResp = wechatSdk.doPay(wxReq);
return convert(wxResp);
}
}
这样业务层只依赖UnifiedPay,完全不感知微信SDK的存在。哪天要换成别的支付方式,只需要新增一个Adapter,业务代码一行都不用改。
适配器最核心的心法是"不要改已有的代码"。接入新东西时不改动老接口,而是用一层Adapter做兼容,这是大项目能持续演进的关键。
3.2 装饰器模式:不继承也能加功能
装饰器模式解决的场景是:你有一个核心组件,需要在不修改它自身的情况下,动态地叠加一些增强功能。它的关键特征是同构装饰——装饰器和被装饰对象实现同一个接口,装饰器内部持有被装饰对象的引用。
Java标准库就是装饰器模式最大的使用现场。你看BufferedReader:
java复制BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("data.txt"), "UTF-8"));
InputStreamReader把字节流转成字符流,BufferedReader又给字符流加上了缓冲能力。它们都实现了Reader接口,层层包装,每层各司其职。如果你要用继承实现BufferedReader的缓冲功能,那需要为每一种Reader子类都写一个缓冲版本,类数量爆炸。装饰器模式用组合的方式优雅解决了这个问题。
我在实际业务里用过装饰器的两个地方:
一是给消息推送服务加"重试"和"埋点"能力。核心发送逻辑不变,外面包一层带重试的装饰器,再包一层带日志埋点的装饰器,调用方用的还是同一个Sender接口,但能力已经叠加了。
二是给缓存系统加"穿透保护"。核心缓存读取逻辑不变,加一个装饰器,查到空值的时候做特殊处理,防止缓存穿透打到数据库。
装饰器模式和代理模式的区别,很多初学者容易混淆。简单区分:装饰器关注的是"增强功能",代理关注的是"控制访问"。装饰器层层包装,对调用方透明;代理则是站在你面前做拦截,比如权限控制、懒加载、远程调用。
3.3 代理模式:Spring AOP的底层逻辑
代理模式在Java生态里的重要性不用多说,Spring AOP、MyBatis的Mapper代理、RPC框架的远程调用代理,底层全是它。
代理模式的核心是:不直接访问目标对象,而是通过一个代理对象来间接访问。这个代理可以在调用前后插入额外逻辑——权限校验、日志记录、事务管理等。
Spring AOP本质上就是一个动态代理框架。你定义一个@Transactional注解,Spring会在运行时生成一个代理对象,在方法调用前开启事务,调用成功后提交,异常后回滚。业务代码完全不需要关心事务逻辑。这就是代理模式的价值——把横切关注点从业务逻辑中剥离出来。
理解动态代理的关键在于"运行时生成类"这个概念。JDK动态代理基于接口,CGLIB基于继承,它们在运行时生成新的类,在调用方法时通过InvocationHandler或MethodInterceptor拦截。虽然具体的字节码生成技术很复杂,但使用模式的思路是一致的:让代理对象在不修改目标代码的前提下,改变或增强目标方法的行为。
4. 行为型模式:把变化的行为从主流程里拆出去
行为型模式关注的是"对象之间的职责分配和算法封装"。这一板块在公司代码里的出现频率最高,因为业务系统的核心就是流程+规则的组合,而规则恰恰是最容易变的部分。
4.1 策略模式:消灭业务代码里的if-else
策略模式是业务代码里最实用的模式,没有之一。它的核心思想是:定义一组算法,把它们各自封装起来,并且使它们可以互相替换。
拿前面提过的订单金额计算来完整演示。第一步,定义策略接口:
java复制public interface DiscountStrategy {
double calculate(double amount, OrderContext context);
}
第二步,为每个规则实现具体策略:
java复制public class MemberDiscountStrategy implements DiscountStrategy {
@Override
public double calculate(double amount, OrderContext context) {
return amount * 0.9;
}
}
public class FestiveDiscountStrategy implements DiscountStrategy {
@Override
public double calculate(double amount, OrderContext context) {
// 满减逻辑
return amount >= 300 ? amount - 50 : amount;
}
}
第三步,在上下文里持有策略对象,并把"选哪个策略"的职责交给工厂或者由调用方决定:
java复制public class OrderCalculator {
private final DiscountStrategy strategy;
public OrderCalculator(DiscountStrategy strategy) {
this.strategy = strategy;
}
public double calculate(Order order) {
double total = ...; // 计算原始总额
return strategy.calculate(total, order.getContext());
}
}
这样一来,主流程只关心"算总额",而"怎么打折"完全交给策略。新增一条规则时,你只需要加一个新策略类并注册到工厂里,Calculator一行都不用改。
常见的误用是:策略只有一种实现,就为了"未来可能扩展"而引入策略接口。这是典型的过度设计。策略模式是在"确实存在多个可替换的算法"时才值得引入,如果当前只有一个实现,就老老实实写简单代码,等第二个策略出现时再重构。
4.2 观察者模式:事件驱动架构的基本盘
观察者模式解决的是"一对多依赖"的问题——当一个对象状态变化时,所有依赖它的对象都能收到通知并自动更新。
这个模式在咱们工作中体现得最明显的地方就是事件机制。Spring的ApplicationEvent、Guava的EventBus、各类消息队列的发布订阅模型,本质上都是观察者模式的变体或扩展。
我参与过的订单系统里就有个典型场景:订单状态变更为"已支付"之后,需要触发一系列后续动作——发短信通知用户、给财务记账、通知仓库备货、更新用户积分。如果把这几个动作直接写在订单支付完成的代码里,每次新增一个"支付后动作"都要改动核心支付流程,而且代码耦合得非常紧。
用观察者模式改造后,核心流程只发布一个"支付成功"事件:
java复制applicationEventPublisher.publishEvent(new OrderPaidEvent(this, order));
各个模块各自监听这个事件,在自己的监听器里处理自己的事:
java复制@EventListener
public void onOrderPaid(OrderPaidEvent event) {
// 发送短信通知
}
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
// 更新客户积分
}
这样订单模块不需要知道"支付后要做哪些事",新加一个动作只要新增一个监听器就完了,改动的范围被限制在新增代码里,完全不碰核心流程。这就是观察者模式开闭原则的最佳体现。
需要注意:同步事件监听会影响主流程性能,如果监听器里有耗时的外部调用,建议改成异步处理或引入消息队列。同时要注意异常隔离——一个监听器抛异常不能影响其他监听器执行。
4.3 模板方法模式:把不变的骨架定下来
模板方法模式解决的是"算法骨架不变、细节步骤多变"的场景。它在父类里定义好算法执行的顺序,把每一步的具体实现延迟到子类。
这个模式在框架代码里到处都是。Spring的JdbcTemplate,你只需要写核心的RowMapper(怎么把结果集映射成对象),连接的获取、事务的处理、异常转换这些固定流程都由模板完成。你写一个继承自AbstractRoutingDataSource的类,只需要实现determineCurrentLookupKey方法告诉框架用哪个数据源,其他获取连接、切换的逻辑都封装好了。
一个典型的例子是数据导出功能。导出Excel、导出CSV、导出PDF整个流程一模一样:查询数据 -> 转换格式 -> 写入输出流。区别只在"转换格式"这一步。模板方法模式把这个流程固定下来:
java复制public abstract class DataExporter {
public final void export(QueryParam param, OutputStream out) {
List<Data> data = queryData(param); // 固定的:查询数据
String content = convertToFormat(data); // 可变的:格式转换
writeToStream(out, content); // 固定:写输出流
}
protected abstract String convertToFormat(List<Data> data);
}
导出Excel子类只需实现convertToFormat,导PDF、导CSV同理。模板方法模式和策略模式的区别在于:模板方法是在继承体系里固定骨架,策略模式是在组合体系里替换算法。前者重在"流程固定",后者重在"算法可换"。
4.4 责任链模式:审批流、拦截器、中间件
责任链模式也是工作中出场率极高的模式。它的核心是:把处理请求的对象连成一条链,请求沿着链传递,每个节点决定自己处理还是传递给下一个节点。
Java Web开发中的Filter、Spring MVC的Interceptor、MyBatis的插件机制、Netty的ChannelPipeline,全是责任链模式的应用。一个请求进来,经过登录校验、权限校验、参数校验、业务处理,每一层各管一块,处理完交给下一层,这就是一条典型的责任链。
写业务代码的时候,我常用责任链来重构"校验逻辑"。比如下单接口要校验商品状态、库存、用户状态、风控,如果全写在一个方法里就是一大坨if-else而且新校验很难加。把每个校验做成一个Handler,链式执行,新增一个校验节点只需要新增一个Handler并接到链上。
5. 模式不只是OOP的事:从经典设计到多Agent编排
如果说前面几章讲的是软件工程里十几年稳定的经典模式,那么这一章我想聊点新鲜的——设计模式思想在AI应用开发(尤其是多Agent系统)里的变形。
5.1 经典模式在新场景的重新演绎
最近"多Agent"这个概念在AI编程领域非常火。很多人问:这跟设计模式有什么关系?其实关系大得很。你仔细看现在主流的多Agent框架,里面到处是经典设计模式的影子。
比如Agent协作里的"主从模式"(Supervisor模式),主Agent负责任务拆解、调度、汇总,子Agent(SubAgent)负责任务执行。这个结构的本质,用设计模式的话说就是将SubAgent当作一种另类的Tool,通过统一的调用接口被主Agent调用——这和策略模式里的"可替换算法"是不是很像?
再比如Agent工具注册机制,其实就是工厂模式的变体——通过一个注册表按名称创建并获取工具实例。而Agent执行流程里那些前置检查(Prompt注入防护、上下文组装)、后置处理(输出格式校验、结果缓存),你如果把它做成可以在主流程上动态叠加的组件,那就是活生生装饰器模式。
我接触到的那些架构设计得比较好的Agent应用,并不是发明了什么新东西,而是把软件工程里被反复验证过的模式,平移到LLM应用的编排层。
5.2 主从Agent模式的核心要点
具体展开说一下多Agent里最主流的Supervisor模式。它的架构大致是这样的:一个Supervisor Agent负责理解用户意图、拆解任务、调用对应SubAgent;每一个SubAgent专门处理一类子任务。
核心设计决策是"SubAgent就是Tool"。如果你把SubAgent封装成一个Tool(工具函数),那么主Agent选择工具的行为就变得极其统一——它不需要为每个SubAgent写特殊逻辑,只需要按工具调用的协议传递参数即可。这种方式和传统代码里"插件化设计"完全一脉相承。
但有几个问题是传统设计模式教程里不会讲的:
第一,上下文隔离。SubAgent通常有自己的System Prompt和上下文窗口,主Agent不能无限地把全部信息塞给SubAgent,否则上下文爆掉。这就要做"参数摘要"——只传子任务所需的精简上下文,对应到传统工程里就是接口的"最小参数原则"。
第二,结果合规校验。LLM的输出是不可控的,你让SubAgent返回JSON,它可能给你一段Markdown。所以需要在调用后加一个格式校验和修复环节,这本身就是一个"保护性装饰器"。
第三,任务超时和失败降级。LLM调用可能超时、可能返回错误格式,主Agent必须有一套兜底策略,比如重试、换SubAgent、降级为直接回答。这和我们给第三方API调用加重试机制没有任何本质区别。
5.3 模式思维比模式本身更值钱
从传统OOP到多Agent,你会发现:具体的技术栈一直在变,但模式的思维范式是稳定的。"封装变化"这一点,在Agent架构里同样适用——今天你用的是OpenAI的函数调用,明天可能换成别的模型自己的工具调用协议,如果你把工具调用封装成了统一接口,迁移成本就大大降低。
这也是为什么我在团队里一直强调:学设计模式,本质上是在锻炼一种结构化的抽象能力。这种能力不绑定于任何语言、任何框架,它会在不同的技术范式里以不同的形态反复出现。今天它化身为Spring AOP的代理机制,明天它可能是LangChain里的Agent工具注册表,后天它可能进入一套全新的编排框架。你脑子里对"这属于哪种结构"的判断越敏锐,面对新东西时就越从容。
6. 避坑指南:这些滥用模式的行为,项目里真的见过
写了这么多年代码,也review过不少同事的代码,我发现设计模式在实际落地中翻车的不在少数。这一章集中聊聊哪些是常见误用,以及怎么判断一个地方是否真的需要模式。
6.1 用模式制造抽象:过度设计的信号
最典型的问题是:为了预判未来的变化而提前抽象。比如业务里明明只有一种日志实现,非要先定义Logger接口再写实现类;只有一个订单处理器,非要先建Handler抽象类再写一个字类。这种代码就是"为模式而模式",它带来的直接后果是:
- 阅读成本上升,跳转层级变多,看起来"很架构",实则很难维护;
- 真正的需求变化方向往往和你预想的不一样,预判的抽象根本用不上;
- 年轻同事接手后不敢轻易删代码,只能继续叠加更多的抽象。
怎么避免?我给自己定的原则是:在第三次出现重复代码之前,不引入抽象。第一次写就直接写,第二次出现时评估一下是否值得抽象,第三次还出现就果断重构。频繁重构需要一个前提——你的代码结构清晰、测试覆盖足够。所以良好的设计模式和整洁代码、单元测试是一套组合拳,单拎一个出来效果大打折扣。
6.2 模式误用的典型场景
| 误用场景 | 具体表现 | 更合理的做法 |
|---|---|---|
| 无状态对象写成单例 | Service、工具类用单例 | 直接用静态方法或由容器管理Bean生命周期 |
| 策略模式只有一种实现 | 为了"未来扩展"先建策略接口 | 等第二个实现出现时再引入 |
| 观察者滥用 | 同步监听的Listener里做远程调用 | 评估异步化或消息队列兜底 |
| 工厂套工厂 | 创建逻辑本身不复杂,却建了抽象工厂 | 把对象创建交给容器(Spring)管理 |
| 模板方法强制继承 | 让实现类继承一个抽象父类,只为复用一段方法 | 优先考虑组合,避免强耦合继承 |
这里有一个常见的迷惑:Spring里到处是接口+实现类,是不是过度设计?答案是否定的。Spring的接口往往不是为了"模式"而存在,而是为了依赖注入和AOP代理的需要。Spring默认使用JDK动态代理,而JDK动态代理要求目标必须有接口;如果你写成普通类,Spring也能通过CGLIB用继承方式代理。所以你在Spring里看到一堆接口,很多时候是技术框架的约束,而非刻意套用设计模式。
6.3 实际项目里的判断标准:多问几个"所以呢"
我在review代码的时候,遇到一个用了设计模式的写法,会习惯性问这几个问题,也建议你问自己:
- 这个抽象解决的是什么"变化"?如果答不上来,那大概率是过度设计。
- 引入这个模式后,新增需求时改动量真的变小了吗?
- 抽象的隔离层本身会不会成为新的维护负担?
- 团队里其他人能不能看懂这套设计?设计模式是团队沟通语言,如果只有你一个人懂,未来就是别人的包袱。
抽象能力是设计模式的核心,但抽象本身也有成本。每一次抽象都应该以"降低未来改动的成本"为回报,如果回报不明显,那这个抽象就是负资产。
7. 给不同阶段学习者的实用建议
讲到这,模式算是讲得差不多了。最后一章写给还在学习阶段的朋友——不管你是准备"设计模式"期末考试也好,在赶设计模式大作业也好,还是工作几年想在项目中运用得更自如,我想说点掏心窝子的经验。
7.1 学设计模式,别从GoF原书开始强啃
我自己当年的体验是:GoF的《设计模式》是工具书,不是入门书,硬啃很容易中途放弃。更好的路径是这样的:
- 先掌握SOLID原则和"封装变化"的思想,这是地基;
- 从最常用的模式学起:策略、模板方法、工厂、单例、观察者、适配器、装饰器、代理、责任链,这九个覆盖了业务开发的绝大部分场景;
- 每个模式都用一个你能理解的真实场景去套——比如"我的项目里有没有if-else可以改造成策略的";
- 动手改写一小段自己的代码,用UML类图把改造前后的结构对比画出来,这一步极其重要。
画出类图在初学阶段能帮上大忙。模式的本质是结构关系,代码反而会干扰你对结构的感知。能画出清晰的类图说明你真的理解了,画不出来就说明还没吃透。
7.2 如何高质量地完成一个设计模式大作业
如果是写期末大作业,我的建议是不要试图把二十几个模式全部塞进一个系统里——那种"管理系统"式的demo我已经看腻了,一眼就知道是为了凑模式硬拼的。
选两到三个模式,用一个有真实复杂度的场景把它们有机结合起来,得分通常比面面俱到高得多。比如做一个"多功能报表导出系统":
- 模板方法模式:固定"查数据-转格式-输出"的导出流程;
- 策略模式:Excel、CSV、PDF三种导出算法互相替换;
- 工厂模式:根据用户选择的导出类型创建对应策略;
- 装饰器模式(可选):给导出加压缩、加密的增强能力。
这样的设计有清晰的层次:工厂管创建,策略管算法,模板方法管流程,装饰器管增强。每个模式承担了独立的职责,相互配合,绝不是生硬拼凑。同时配套的类图、时序图、变更前后对比分析,都能让作业的深度上一个档次。
7.3 工作几年后,怎么继续加深对模式的理解
如果你已经工作了一段时间,我的建议是换个角度:从"我会用哪个模式"转变为"我看到哪些代码坏味道"。识别坏味道的能力比背模式重要得多。长方法的代码里藏着策略模式的需求,巨型if-else里藏着责任链的需求,类爆炸的代码里藏着装饰器或组合的需求。
另外建议你给自己一个习惯:重构小步走,每提取一个接口、每抽出一个策略,都要保证行为不变、测试全绿。设计模式不是一次到位的重构,而是日复一日的小步优化积累出来的。不要指望一个周末就能把所有代码重构到"完美架构",那既不现实也没有必要。
我在实际项目里最受用的一点体会是:把设计模式当成团队沟通的语言。当你对同事说"这里用策略模式改造一下",大家脑子里会出现同一张结构图,沟通效率会高很多。反过来,如果你引用一个模式同事完全没听过,那就要考虑是不是自己过度设计了。
写在最后
设计模式这个东西,学的时候觉得是知识,用的时候觉得是工具,真正融会贯通之后,你会发现它其实是一种看问题的方式。掌握了它,写代码的时候你更容易判断哪里稳定、哪里易变,也更清楚怎么在两者之间划出边界。每个模式背后凝结的都是无数前辈踩坑后总结出的智慧,值得你花时间慢慢体会。
如果你现在正准备开始学设计模式,或者正在为一个看似混乱的代码库发愁,不妨从一个小模块开始,用模式的思想重新审视它。可能只需要改动几十行代码,你就能感受到"面向变化编程"和"面向当下编程"之间的差别。这种差别,只有亲手做过才能真正体会得到。
