1. 评审了上百份设计模式作业和面试简历,我发现多数人卡在同一个地方
每年设计模式期末、设计模式大作业这两个词一上热搜,我就知道又有一批同学正对着UML类图发愁。说实话,我这些年看过的设计模式相关作业、开源PR和面试代码,少说也有几百份,能真正把模式用对场合的人,可能两成都不到。
大部分人的状态是这样的:单例模式的懒汉饿汉倒背如流,工厂模式和抽象工厂的区别能写出八百字小论文,但一拿到真实需求——比如"把一段订单状态流转的逻辑改得可维护一些"——瞬间回到面向过程编程,一个switch下去恨不得写两百行,运维看到都摇头。
问题出在哪?不是大家不努力,而是学设计模式的方式错了。当时背的是"定义+结构图+代码例子",代码例子还是网上抄的Cat extends Animal、Pizza工厂那种玩具例子。等你到了真实项目里,面前是Spring Boot的Bean容器、MyBatis的Mapper代理、Android的Binder通信,你根本对不上号——因为真实框架里的设计模式全都披着"封装后的外衣",和你背的教科书形态差着十万八千里。
所以我今天想换一个讲法。不按GoF那本红书从头到尾念经,而是把23种设计模式放在"真实业务演进"这条线上重新拆一遍。你会看到:模式不是拿来背的,是拿来"填空"的——哪里感觉代码散发着坏味道,哪里就有某个模式在等着你。
这篇文章适合三类人:正在为设计模式期末/大作业发愁的学生,写了两三年业务代码开始觉得系统难维护的CRUD选手,以及在面试前想真正建立设计模式体系的人。我会尽量用Java作为示例语言,但思路对所有面向对象语言通用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先扔掉GoF分类,用"变化点"重新理解23种模式
2.1 GoF三大分类为什么容易把人带沟里
课本上把23种模式分成创建型、结构型、行为型三大类,这个分类本身没错,但它是给"目录查阅"用的,不是给"学习理解"用的。
你想想,当老师说"建造者模式属于创建型模式"时,你记住了什么?你记住了"它属于创建型"。但当你面对一个实际需求时,你能根据"这是创建型"推出"所以我应该用建造者"吗?根本推不出来。分类标签只告诉你在图书馆哪一排书架找书,没告诉你什么情况下需要读哪本书。
我的建议是:忘掉GoF的分类,换成"变化点"思维。什么叫变化点?就是你的程序里,哪个部分的需求最可能变,哪个部分就该被单独封装起来。设计模式本质上就是一本"封装变化"的武功秘籍——每个模式都在回答同一个问题:你这段代码里,什么东西以后会变?
以订单金额计算为例。今天只有一个普通订单,你直接写amount = price * count完事。明天来了个会员折扣需求,你加了行if (isVip) amount *= 0.9。后天又来了个满减活动,你继续往方法里塞if。到了大后天,这个方法的圈复杂度能把你自己绕晕。这时候你回头看,会发现"计价规则"就是变化点。怎么处理?策略模式:把每种计价规则封装成一个类,让它们可以互相替换。
2.2 一张表看清23种模式各在防什么"变"
为了让你有全局观,我先把23种模式按"它们封装的变因"重新归一下类。你会发现这样归类之后,模式之间的关系一下子清晰多了。
| 变化的维度 | 对应模式 | 一句话概括 |
|---|---|---|
| 对象怎么创建 | 单例、原型、工厂方法、抽象工厂、建造者 | 把"创建谁、怎么创建、何时创建"的控制权从业务代码中剥离 |
| 对象间怎么协作 | 策略、状态、命令、观察者、中介者、责任链、模板方法、迭代器、访问者、解释器、备忘录 | 把对象间的交互逻辑从硬编码改为可扩展的约定 |
| 对象的结构怎么组合 | 适配器、装饰器、代理、外观、组合、桥接、享元 | 把类与类的静态关系改成可灵活组装的结构 |
注意看,这三行和GoF三大类的覆盖范围基本一样,但阅读视角完全不同。GoF分类是"这些模式在结构上有什么共同特征",变化点分类是"你的代码哪里在变,去对应的行里找模式"。
打个比方:GoF分类好比按菜系给你列菜单——川菜、粤菜、鲁菜;变化点分类好比按场景给你推荐——想开胃去哪个菜系、想吃清淡去哪个菜系。解决的是"点菜"这个真实决策问题。
2.3 用变化点定位模式的三个步骤
实战中我用这个方法,只需要三步。
第一步:找变化点。把需求里"未来可能调整"的部分列出来。比如:支付方式可能增加、折扣规则可能变化、订单状态流转可能复杂、报表导出格式可能增多。
第二步:定变化时机。如果只有一个实现且永远不会变,别用模式,直接写。如果有两个以上实现,或者明确知道下个迭代就要加需求,再考虑模式。
第三步:选模式。看变化发生在创建阶段、组装阶段还是交互阶段,回到上表对应行挑选。
举个例子,我在一个电商项目里接到需求:"接入新的物流查询接口,但第三方返回的数据格式与现有系统不一致。"变化点是"外部接口数据格式不统一",发生在组装对接阶段,对应的模式就是适配器。加一个LogisticsAdapter,把第三方数据转换成系统内部标准VO,问题解决。
这套方法的威力在于,它逼你先思考"哪里会变",再思考"用什么模式"。顺序一换,思路就通了。
3. 创建型模式实战:把"new"的权力收归己有
创建型模式的共性是:它们都盯着new关键字。new本身不是坏东西,坏的是new散落在业务代码的各个角落。一旦创建逻辑变了,你要把所有new的地方全部找出来改一遍,这就是典型的"修改分散"坏味道。
3.1 单例模式:双重检查锁的坑与枚举方案
先聊最常被问的单例。单例解决的是"全局只需要一个实例"的场景——配置文件读取器、数据库连接池、Spring容器本身。
教科书里通常给双重检查锁写法:
java复制public class ConfigManager {
private static volatile ConfigManager instance;
private Properties props;
private ConfigManager() {
// 加载配置文件
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
}
这里最关键的是volatile关键字。很多人漏写,以为synchronized就够了。其实synchronized保证的是原子性和可见性,但instance = new ConfigManager()在JVM里是三步操作:分配内存、初始化对象、把引用指向内存。这三步可能被指令重排,导致线程A先赋值了引用但还没执行完构造函数,线程B进来发现instance != null直接拿去用,拿到一个半初始化对象。
不过说实话,如果你在写新代码,我更推荐用枚举实现单例:
java复制public enum ConfigManager {
INSTANCE;
private Properties props;
public void load(String path) {
// 加载逻辑
}
}
枚举天然单例、天然线程安全、天然防反序列化破坏,代码量还少。Joshua Bloch在《Effective Java》里也明确推荐过,这不是什么冷门技巧,只是很多教材没更新。
3.2 工厂三兄弟:从if-else到开闭原则
工厂方法、抽象工厂、简单工厂这三兄弟经常被人搞混。我的理解方式是这样的:
- 简单工厂:一个工厂类里用switch/if分支创建不同产品。它不是GoF 23种模式之一,但很常用。
- 工厂方法:把"创建哪个产品"下沉到子类。基类定义创建接口,子类决定实例化谁。
- 抽象工厂:创建的不是单个产品,而是一族产品。比如"支付处理族"里有支付宝处理器、微信处理器、银联处理器。
真实项目里,我处理支付渠道的需求是这么做的:先定义一个PaymentFactory接口,里面放createPayment(channel)方法,然后微信支付工厂、支付宝工厂各自实现。业务代码只依赖PaymentFactory接口,新增渠道时新增工厂类,老代码一行不用动。
但这里要提醒一句:抽象工厂不要一上来就用。如果你的产品族只有一个类型,用抽象工厂等于拿大炮打蚊子,徒增类数量。我就见过有人为了三五个产品硬写二十几个类,评审会上大家看着类爆炸图谁也不说话。
3.3 建造者与原型:复杂对象与对象克隆的实践场景
建造者模式解决的是"对象参数太多、构造方法炸了"的问题。
java复制public class HttpRequest {
private String url;
private String method;
private Map<String, String> headers;
private byte[] body;
private int timeout;
// ...
private HttpRequest(Builder builder) {
this.url = builder.url;
// ...
}
public static class Builder {
private String url;
// ...
public Builder url(String url) {
this.url = url;
return this;
}
public Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
现在用起来是new HttpRequest.Builder().url("https://api.example.com").timeout(5000).build(),比十多个参数的构造方法清晰得多。但注意,如果你的对象就三五个字段,别用建造者,直接构造方法或setter就够,模式贵在匹配场景。
原型模式在Java里最典型的落地是Object.clone()加Cloneable接口。但它有个深水区:默认的clone()是浅拷贝,对象里的引用类型字段还是同一个。处理深拷贝时,我一般不用手写clone链,而是直接序列化再反序列化——用Jackson或Gson把对象转成JSON再转回来,简单粗暴,但要注意性能,高频调用场景不适用。
4. 结构型模式实战:把类的形状打磨成可维护的样子
结构型模式关注的是"如何把类和对象组织成更大的结构"。翻译成人话:当你的代码变成一堆纠缠不清的类时,怎么把它梳理得清爽、可扩展。
4.1 适配器、外观与代理:三个"中间层"用错的代价
这三个模式都是"中间层",但解决的问题完全不同。适配器是为了"接口不兼容"——你的代码期望A接口,第三方库只提供B接口,加个适配器把B转成A。外观是为了"简化复杂子系统"——你不想让调用方跟五个模块直接耦合,提供一个门面方法搞定。代理是为了"控制访问"——你不希望把真实对象直接暴露给客户端,加个代理层管权限、管延迟加载。
很多读者一开始区分不了三者的核心差异。我提供一个判别法:适配器是"不得不接",外观是"主动封装",代理是"代为管控"。
举实际案例。公司要做统一短信发送平台,底层接了三家短信服务商,每家SDK的API都不一样。这时候适配器模式上场,我定义了一个SmsSender接口,然后写AliyunSmsAdapter、TencentSmsAdapter、YunpianSmsAdapter,业务层只面向SmsSender编程。将来换服务商,新写一个Adapter就行,业务代码无感。
外观模式的典型例子是java.util.logging或者SLF4J。你调用logger.info("..."),背后管着格式器、处理器、过滤器一大堆组件,但你完全不用关心。这就是一个门面。
代理模式最出名的案例是Spring AOP。你标注@Transactional,Spring会生成一个代理对象,在调用你的业务方法前开启事务,方法结束提交或回滚。你感觉不到,是好事,说明代理做到了透明。
但中间层别乱加。我见过一个新来的同事,为了"以后可能扩展日志权限",给一个只有两个方法的Service加了代理+接口+实现类三个文件,最后评审会上被我们一致毙掉。没有明确的需求驱动,中间层就是负担。
4.2 装饰器与组合:Java I/O为什么被奉为教科书案例
以前我在另外一篇文章里写过怎么分析复杂系统,那些方法论放在Java I/O这棵类树上同样适用。你看下面这段代码:
java复制BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8));
很常见的写法,但里面藏了两个模式。FileInputStream是源头,InputStreamReader是适配器(把字节流转字符流),BufferedReader是装饰器(给Reader增加缓冲功能)。
装饰器模式的核心魅力在于"层层叠加能力"而不修改原类。这里的关键是:装饰器和原类实现同一个接口或继承同一个抽象类。你看BufferedReader和FileInputStream虽然都在Reader体系里生根发芽,但运行时他们构成嵌套结构。
好处是自由组合。要缓冲就包BufferedReader,不要就去掉;要带行号就再套一层LineNumberReader。你没法想象如果用继承来实现这些组合,得写出多少个排列组合类——这就是装饰器碾压继承的原因。
组合模式解决的是"部分-整体"结构。最经典的例子是文件和文件夹:文件夹里可以装文件,也可以装子文件夹,两者都应该支持delete()、getSize()这类操作。
我在做权限系统菜单树时用过组合模式。菜单节点分两种:叶子节点(具体功能项)和容器节点(菜单目录),但对外都实现MenuComponent接口,统一提供render()、getPermissionCode()等方法。前端渲染和权限校验直接递归整棵树,代码非常整洁。
4.3 桥接与享元:维度拆分与细粒度共享的两把钥匙
桥接模式是我觉得最被低估的结构型模式。它解决的核心问题是"多维变化"——比如一个消息通知系统,发送渠道有短信、邮件、站内信,消息类型有普通通知、告警、营销。如果把渠道和类型组合起来用继承实现,你需要3×3=9个类。
有了桥接模式就不一样了。把"发送渠道"和"消息类型"拆成两个独立的维度,都走组合而不是继承:
java复制public abstract class Notification {
protected MessageChannel channel;
public Notification(MessageChannel channel) {
this.channel = channel;
}
public abstract void notify(String content);
}
public class AlertNotification extends Notification {
public AlertNotification(MessageChannel channel) {
super(channel);
}
@Override
public void notify(String content) {
channel.send("[告警] " + content);
}
}
渠道实现类SmsChannel、MailChannel自己维护发送逻辑。以后新增一个"语音通知"类型,只需新增一个VoiceNotification类,渠道复用;新增一个渠道,也只加一个Channel类,类型复用。类数量从乘法变为加法。
共享的意义在于"复用细粒度对象"。我在做报表系统时,导出大量单元格对象,每个单元格包含字体、颜色、边框等属性。如果每次创建几百个不同样式的Font对象,内存会吃紧。这时候享元模式出场:相同样式的Font对象共享同一个实例,内部状态(字体名、字号、是否加粗)放在享元内部,外部状态(文字内容)由客户端传入,不放进Font对象。
一个朴素但有效的判断方法是看"对象的差异点"在哪里:差异点如果能外部传入,就可以共享内部状态。这种思路在缓存、线程池、对象池里到处都是,理解了享元,很多框架的设计意图就一目了然了。
5. 行为型模式实战:对象之间的协作靠"约定"而不是"干预"
行为型模式是我面试时最爱问的部分。因为创建型和结构型解决静态问题的偏多,行为型解决的才是"运行时对象之间怎么打交道"这种动态问题。
5.1 策略、状态与模板方法:算法家族的三种演进方式
策略模式是入门必学。它的本质是"把算法封装成独立的策略对象,运行时可替换"。Spring框架里最经典的实践就是Resource接口,ClassPathResource、FileUrlResource、ByteArrayResource这些不同的资源访问策略通过同一个接口暴露。
网上那些DiscountStrategy的Demo我就不重复了,我说一个容易被忽视的细节:策略模式一定要搭配"策略工厂"或者"策略注册表"一起用,否则客户端还是在用if-else选策略,变换了战场没换打法。
我自己常用的方案是Spring的ApplicationContext.getBeansOfType,一次性拿到所有策略Bean,用一个Map按channelType存起来:
java复制@Component
public class PayStrategyRegistry {
private final Map<String, PayStrategy> strategyMap;
public PayStrategyRegistry(List<PayStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(
s -> s.getChannelType(),
Function.identity()
));
}
public PayStrategy getStrategy(String channelType) {
return strategyMap.get(channelType);
}
}
这样新增支付渠道只需实现PayStrategy接口并注册为Spring Bean,业务代码零改动,完美契合开闭原则。
状态模式和策略模式,新手容易混淆。二者的结构几乎一样——都是把逻辑拆到多个子类里,通过组合持有接口。区别在于:策略模式是客户端主动选择算法,哪个策略之间互相独立,无状态迁移;状态模式是根据状态自动触发行为,并且状态之间会流转。
我做过一个工单系统,工单状态有:待分配、处理中、已解决、已关闭。每个状态下能执行的操作不同,操作后状态会跳转。这个场景用状态模式非常合适。我把状态定义成枚举或类,每个状态实现handleEvent(WorkOrderContext context, WorkOrderEvent event)方法,内部负责自己的行为并调用context.setState(newState)完成流转。整个工单的状态机清晰又优雅,后来产品想加"重新打开"功能,我只要加一个ReopenedState并修改对应状态的流转逻辑就行。
模板方法模式解决的是"算法骨架固定,步骤实现可变"的场景。它倒着用就是"控制反转"的一种体现——父类定义流程,子类填充细节。
我在数据同步工具里就用模板方法定义了全流程:
java复制public abstract class AbstractDataSyncer {
public final void sync() {
validateConfig();
SourceData data = fetchSourceData();
TransformResult transformed = transform(data);
TargetResult result = pushToTarget(transformed);
logSyncResult(result);
}
protected abstract void validateConfig();
protected abstract SourceData fetchSourceData();
protected abstract TransformResult transform(SourceData data);
protected abstract TargetResult pushToTarget(TransformResult data);
protected void logSyncResult(TargetResult result) {
// 默认日志实现,子类可覆盖
}
}
每个数据源(MySQL、API、FTP)写一个子类,只关心自己怎么取数、转换、推送,同步骨架完全复用。注意这里final关键字很关键,它保证子类不能改动业务流程的顺序——这是设计上的一种强制约束。
5.2 观察者与责任链:从事件驱动到请求传递
观察者模式可能是设计模式里应用最广、也最容易被"裸写"的一个。它的本质是"发布-订阅":主题对象状态变化时,自动通知所有注册过的观察者。
Spring的事件机制就是完美实现。ApplicationEventPublisher发布事件,@EventListener注解监听事件——你在业务代码里发一个OrderPaidEvent,然后所有关心"订单支付成功"的模块(短信通知、积分累计、库存更新)各自监听处理。模块之间完全解耦,新增一个关心方不影响发布者和其他监听者。
Android里观察者模式也到处都是。Button.setOnClickListener就是一个观察者注册,LiveData的observe方法也是,BroadcastReceiver本质上还是——组件与组件之间通过系统来发布广播、注册接收、响应事件。
我在做IM系统的私聊消息推送时也用了观察者思路:每个用户的在线会话维护一个SessionListener列表,收到新消息时遍历推送。不过这里有个天然的坑:监听器没有在合适时机注销。Fragment.onDestroyView里忘了解绑,就会导致内存泄漏,在Android里尤其致命。
责任链模式解决的是"多个对象都有机会处理请求,把处理者串成一条链"。我们日常写过滤器时几乎都会遇到它——Servlet的FilterChain、Spring的HandlerInterceptor、MyBatis的Interceptor,全都是责任链思想的实现。
我在网关项目里做参数校验时用过:一个校验链由长度校验、格式校验、白名单校验、敏感词校验四个节点组成,每个节点通过next.handle()把请求传递给下一环。好处是新增一个校验规则不用改旧代码,加一个节点就行,维护起来很清爽。
但责任链有个需要注意的地方:链上节点顺序很重要,而且容易形成"死链"——某节点忘了调next,请求流程就在那截断了。我一般会约定:每个处理器必须明确是"终止"还是"放行",并在文档里画清链路顺序,防止后人改出bug。
5.3 命令、迭代器、备忘录等:小模式的大用途
经常有人问:"访问者、中介者、解释器这些是不是面试造火箭,实际用不上?"其实不然,这些冷门模式在某些场景下反而是解药。
命令模式我在做"操作撤销重做"时真的救过命。编辑器里用户执行的操作——插入文字、删除表格、调整缩进——都封装成一个个Command对象。每个命令有execute()和undo()方法,操作历史栈里压入Command,Ctrl+Z就出栈调用undo()。如果没有命令模式,撤销会变成一场精心策划的混乱,因为你需要记住每个操作的反操作细节,而命令对象把这一切都封装了。
迭代器模式的精髓不是"遍历接口",而是"遍历与实现解耦"。你用for-each遍历ArrayList和HashSet的时候,不需要关心底层是数组还是链表还是哈希表,这就是迭代器的功劳。如果哪天你的自定义集合类想让外部也支持for-each,实现Iterable接口就可以了。
备忘录模式在做"草稿箱"、"快照恢复"时很实用。我在做表单编辑器时,每次用户输入暂停两秒,就自动存一版草稿快照。快照对象保存了表单完整状态,用户误关页面重新打开后能恢复到最近快照。这本质上就是备忘录模式:发起人是表单状态,备忘录是快照,负责人是草稿箱管理器。
中介者模式最适合"多对多交互"的混乱场景。以前有一个客服系统,用户、客服、主管、质检四个角色互相通知,直接对象引用会形成网状依赖。引入一个ChatMediator作为中介后,所有消息都发给中介,再由中介分发给对应角色,网状结构变成星状结构,依赖关系一目了然。很多消息中间件如MQ,某种意义上是中介者的分布式增强版。
访问者模式比较抽象,我用的场景是"在同一批对象上增加新操作"——比如给报表里所有节点增加一个"导出JSON"动作。把操作写在Visitor里,每个节点作为被访问者回调visitor对应方法。缺点是节点类要加accept方法,第一次用会绕,需要在合适场景下才划算。
解释器模式就别硬用了。真正需要你写语法解释器的场景非常少,除非你在做规则引擎、SQL解析器、模板引擎这类底层组件。学习它主要是理解"上下文+语法树+非终结符解析"的组合思想,对理解Lombok、MyBatis等框架源码有帮助。
6. 从"读过模式"到"写出模式":四个台阶的刻意练习
前面讲了原理和案例,但我知道,绝大多数人还是卡在"我看懂了,但不会用"。这个坎绕不过去,只能靠练。我把自己的练习路径总结成四个台阶,按顺序踩,基本能完成从"读者"到"作者"的转变。
6.1 第一步:在现有框架源码里找模式
先不急着写代码,先做"找茬"训练。把自己工作的框架源码翻开,挨个找设计模式。
Spring框架里,BeanFactory就是工厂模式的祖师爷,ApplicationContext是它的扩展;AopProxyFactory根据配置生成JdkDynamicAopProxy或CglibAopProxy,这是工厂方法模式;TransactionTemplate包了一个事务管理的模板方法;Environment体系里一堆PropertyResolver是策略模式;EventMulticaster在广播事件,是观察者模式。
即使在Android源码里也能找到大量模式:BroadcastReceiver是观察者,Intent传递数据的方式有点组合和原型结合的味道,LayoutInflater在解析XML时构建View树就是组合模式构建器,Binder的代理机制不用说了,Handler消息循环里Looper是个单例。
找茬训练的最大价值是,让你明白"模式不是虚构的培训班知识点,而是真实世界的共同演化结果"。一旦你在Spring、MyBatis、Android源码里看到了它们,以后你再见到新框架,就能看出套路来,读源码效率翻倍。
6.2 第二步:用模式重构一段烂代码
找茬之后,动手改造自己以前写过的代码。选那些你最想销毁的模块——基本上都有明显的坏味道。
如果你的代码里有一堆if-else判断类型,考虑策略模式或状态模式。如果你的一个类有七八个职责、方法巨长,考虑拆成多个小类,也许中间会用上外观模式提供统一入口。如果你频繁在业务代码里new对象,考虑工厂方法或建造者。
我记得自己刚学设计模式时,拿一个老项目里300多行的OrderService开刀。里面订单创建、支付、取消、确认收货全揉在一个方法里,if嵌套看得人头皮发麻。我花了一整天,拆出OrderStateMachine处理状态流转,抽出PayStrategy处理多渠道支付,把通知逻辑用Spring事件解耦,把参数组装用建造者重写。改完以后,核心方法从300行缩到不到50行,一眼能看懂。
那次重构让我明白了一件事:设计模式不是装饰,而是给代码"减负"的工具。同一份功能,设计良好的代码在增加新需求时,你只需要"加类、加配置",而不是"改老代码",这就是我追求的效果。
6.3 第三步:在真实需求中识别"重构信号"
新需求来的时候,先不急着动手,先问自己一个问题:这段代码在未来三个月内,可能要面对几种变化?
我在具体复盘自己负责的订单流程时,经常问:"如果下周产品说,我要加一个拼团优惠,改动的地方会是多少?"如果答案超过两处,说明设计有问题,模式该上场了。
真实业务中的信号很明确:
- 看到长长的if-else或者switch分支,闻到"变化分支"的坏味道,考虑策略或工厂。
- 看到一个
new关键字出现在业务方法深水区,考虑把创建过程上提到工厂。 - 看到对象被改得面目全非,参数太多,考虑建造者模式。
- 看到组件和组件之间强耦合,谁都离不了谁,考虑中介者或观察者。
- 看到数据转换逻辑散落各处,考虑适配器。
这里有一个非常重要的原则:你不需要一次性引入模式,可以在演进中重构。这部分我特别强调一点:别学太多模式去生搬硬套。你手头的业务,变化点通常只有两三个,能用到的模式可能就四五个,剩下的模式只需要知道存在、知道结构、知道适用场景,等真遇到的时候再深入学习。
6.4 第四步:用反模式审视自己的代码
最后这个台阶,其实是我在面试和带人时最强调的一步:别成为"模式中毒者"。设计模式的初衷是简化代码,不是复杂化代码。如果一个模式让你的代码多出十来个类,但并没有真正带来可维护性和扩展性,那它就是"为了模式而模式"。
反模式信号有哪些呢?我举几个自己常纠正的例子:
- 空转的接口:接口只有一个实现类,也没有任何未来扩展迹象,却硬编出接口+实现类两套结构。这通常是过度设计。
- SSL(字面量闪烁):看到常量就用策略模式去拆,看到集合就用迭代器模式去封装,最后业务逻辑被打散到十几个类里,阅读困难。
- God Object:一个类引用了十几个Manager、Service、Helper,DTO上一层又一层包装,这种类基本上已被模式堆成了屎山。
真实项目里,设计模式应该让类数量降下来、职责清晰、阅读负担减轻。我在代码评审时最常说的一句话是:"你这里少一点模式,代码反而更好看。"不要怕自己用少了模式,要怕自己为了炫技把代码搞复杂。
7. 面试和期末大作业中怎么答出"高级感"
7.1 为什么"背定义"的答案永远拿不了高分
设计模式面试,网上最多的标准答案是:
单例模式是一种创建型模式,它的目的是保证一个类仅有一个实例,并提供一个全局访问点。通过将构造函数私有化……
这段话错吗?没错。但如果是面试官,我听完内心毫无波澜。因为它只证明了你读过《Head First设计模式》的第X章。在真实代码评审里,没人关心你能背出多少定义,大家关心的是:这个模式在你的项目里解决了什么问题?你为什幺用这个而不是那个?
期末大作业同理。老师们看多了"XXX系统的设计与实现"里面硬塞六个设计模式。如果每个模式都是单独一个Demo类,跟系统其他部分毫不相关,老师一眼就能看出来。你要做的,是让模式"长在"你的系统里面——它们是系统演化的自然选择,而不是作业的标签。
7.2 面试官真正想听到的回答框架
我在面试别人时,如果问到设计模式,我希望听到的是这个框架:
- 场景:我在做一个XX系统,里面遇到了XX问题。
- 痛点:原来的代码用了if-else实现了功能,但加需求时改动太多。
- 方案:我引入XX模式,把变化的这部分封装起来。
- 效果:后来新增XX需求时,我只加了一个类/改了一个配置。
- 取舍:这个模式带来了一些额外成本,比如类数量变多了,但换来的可维护性是我们团队当时更看重的。
比如你说策略模式,不要只说"我用了策略模式",要说"我负责的支付模块有微信、支付宝、银联三种渠道,原先在Controller里用switch分发,每次加一个新渠道要把整个方法从头看一遍。后来我用策略模式+Spring注入,把每种渠道的支付封装成一个Strategy类,用一个Map按渠道类型注册。上一个季度新对接了云闪付,我一行老代码都没改,就加了一个类。"
这个回答一出来,里面包含的不只是策略模式,还有工厂模式(Map注册表)、开闭原则,以及你真正在实战中思考过。这才是"深度解析"应该有的味道。
7.3 期末大作业怎么让模式融入系统
给学生党一个设计模式大作业的实操建议。不要做一个"图书管理系统"然后硬套模式。选一个业务规则稍微复杂一点的场景,比如"在线订餐平台"或"审批工作流引擎",让模式自己冒出来。
拿在线订餐平台举例:
- 不同商家类型(快餐、正餐、火锅)做不同的折扣计算 → 策略模式。
- 订单有已支付、制作中、配送中、已完成、已取消五种状态,每种状态下不同操作有不同的响应 → 状态模式。
- 新订单产生后,通知后厨打印机、通知骑手App、通知用户 → 观察者模式。
- 外卖配送费按距离和重量计算 → 模板方法或者策略。
- 对接多家第三方物流 → 适配器模式。
这个结构下,每个模式都有明确的业务存在感。写报告的时候,重点写"没有这个模式会怎样,有了之后改需求时的对比",这正是老师想看到的"深度"。
8. 我最想让你带走的一句话
如果让我用一个词概括23种设计模式,我会选"约定"。它们本质上是软件开发社区经过长期博弈形成的一系列"约定",告诉我们:哪些变化该封装、对象之间该怎样协作、结构该怎么搭。
写代码这些年,我最大的体会是:设计模式不是银弹,它不能把你从烂代码里救出来,但它能给那些"表达能力不足"的类划清边界。模式出现在需要它们的地方时,代码会自己说话;模式出现在错误的地方时,类图会变成一团废线。
所以每次遇到一个问题,我都先问自己:"这坨代码里,什么才会变?"找到那个变化点,再去翻模式目录。23种模式背得滚瓜烂熟不如判断力准确——你那颗随时准备"封装变化"的心,才是这篇长文真正想交给你的东西。
