GOF设计模式笔记:从策略到模板方法,构建代码架构思维
1. 为什么值得专门整理一份GOF笔记写代码写了这些年回头看看真正让我从“能跑就行”进化到“设计得还行”的转折点就是认真啃了一遍GoF的《设计模式》。不过说句实话光看书是不够的。那本书英文原版四百多页每一段都精炼得跟宪法条文似的看的时候觉得自己懂了合上书写代码一动手还是老一套。真正让这些模式在我脑子里扎根的是我花了两周时间用自己的项目代码和踩过的坑重新整理了一份GOF笔记。这份笔记不追求覆盖全部23个模式的每一个字而是把每个模式对应的场景、代码骨架和最容易翻车的地方提炼出来形成一份可以随时翻阅的手册。这份笔记适合谁我觉得只要你写过一年以上面向对象代码日常在跟类、接口、继承打交道并且隐约觉得“代码越来越难改”“加一个新功能要动好多地方”那这份笔记的思路就很值得参考。不管你是做Java、C#、Python还是GoGo语言虽然不走传统继承路线但大部分模式的思想照样能落地整理一份自己的GOF笔记本质上就是给自己建立一套“代码设计的决策清单”。另一个很重要的原因是网上关于设计模式的文章太多了但大部分都犯了一个毛病用动物园或者餐厅点餐的例子讲模式。例子是好例子可一到真实项目中就不知道怎么套了。所以我的笔记里每个模式我都强行找了一个真实系统中的映射哪怕牵强一点也比停留在概念上强。当你把抽象的模式和具体的代码场景绑在一起记忆会牢固得多。2. 笔记框架怎么组织别照抄书上的章节整理笔记的第一步不是记录而是搭框架。GOF原书是按创建型、结构型、行为型三大类分的这种分法学术上很严谨但对于写代码的人来说并不一定是最高效的检索方式。因为很多时候你打开笔记是想问“我现在这个需求该用什么模式”而不是“创建型模式有哪些”。我的笔记框架是按“解决什么问题”来组织的分成五组对象怎么创建、类与对象怎么组合、行为怎么复用和扩展、外部依赖怎么隔离、复杂流程怎么管理。这五组不是我凭空想的是根据我做过的几个中大型项目的实际痛点归纳出来的。比如“外部依赖怎么隔离”这一组收录了门面模式、适配器模式、代理模式——它们解决的问题都是“别让你的核心代码跟外部东西耦合太深”。每一组下面我的笔记结构是固定的五段式一句话说清模式本质不超过三十个字用自己的话。解决什么具体问题必须配上我遇到过的真实场景哪怕是很小的场景。代码骨架最小可运行的代码片段不搞花活就是核心结构。变体与取舍这个模式在实际工程里常见的调整方式。反模式预警什么情况千万别用这个模式。这套结构我建议你也直接用因为它的核心逻辑是“从问题出发”而不仅仅是“从模式出发”。比如说当你遇到“我想给一个已有的类加功能但又不想改它的代码”这个问题时你能直接翻到“行为怎么复用和扩展”这一组到装饰器模式和策略模式里去选而不是把一个创建型模式硬套上去。还有一点要提醒的是笔记不要写成抄书。我见过很多人整理笔记就是把书上的定义复制一遍然后用荧光笔划重点。那样的话整理笔记这个动作本身没有给你带来任何增量价值。真正有价值的笔记是你把别人的知识用自己的代码、自己的项目语境重新翻译了一遍。哪怕你翻译得不太准确这个“翻译”的过程本身就是学习和内化。3. 核心模式深度拆解我的笔记里最常用的六个3.1 策略模式最容易被“过度设计”的模式策略模式我这几年用得非常频繁特别是做支付对接的时候。那时候接入过微信、支付宝、银联每种支付方式的参数校验、签名算法、回调处理都不一样但对外暴露的动作又都是“支付”和“回调处理”。最初的实现当然是if-else后来加上银行卡支付、余额支付之后一个方法里出现了四五个if分支每个分支还有几十行的逻辑已经没法看了。用策略模式改完之后结构一下子清晰了。核心思想其实就一句话把算法封装成独立的类让它们可以互相替换调用方只面向接口编程。我的笔记里记的代码骨架大概是这样的public interface PayStrategy { void pay(String orderId, BigDecimal amount); void handleCallback(String rawCallbackData); } public class WechatPayStrategy implements PayStrategy { Override public void pay(String orderId, BigDecimal amount) { // 微信支付特有逻辑生成预支付单、调起支付 } Override public void handleCallback(String rawCallbackData) { // 微信回调验签逻辑 } }注意一点策略模式不是用来消灭if-else的。实际上在获取具体策略对象时你仍然需要一个选择逻辑——要么用工厂、要么用Map注册。我用的是在支付工厂里维护一个MapString, PayStrategy初始化时把所有策略注册进去调用时直接按支付渠道名取这才是实践中更常见的做法。策略模式最大的坑是“小需求也套策略”。如果你只有一个实现未来一年也看不到第二个实现的可能那就别用策略模式直接写一个普通类方法就够了。我刚开始学的时候犯过这个错误给一个只有一种实现的计算逻辑套了策略接口结果多写了一堆空接口纯粹是自找麻烦。3.2 观察者模式事件驱动的最朴素形态在做一个电商中台项目的时候订单创建之后要触发一堆后续动作发短信、发App推送、更新库存、给推荐系统发送行为数据。起初这些动作都是直接写在订单服务里加一个动作就要改订单服务的代码而且改动还要重新回归测试整个订单流程非常痛苦。观察者模式解决的就是这种“一个事件发生之后多个对象需要响应但响应方不应该被事件源硬编码”的问题。我用Spring的事件机制落地过也用纯Java实现过。纯Java版本的骨架是这样的public class OrderEventManager { private final ListOrderEventListener listeners new ArrayList(); public void registerListener(OrderEventListener listener) { listeners.add(listener); } public void publishOrderCreated(Order order) { for (OrderEventListener listener : listeners) { listener.onOrderCreated(order); } } }后面接新功能的时候爽多了。比如后来要加一个“订单完成后赠送积分”的需求我只需要新增一个PointsListener注册进去订单核心代码一行都不用改。观察者模式也有要注意的地方最典型的问题是监听器的执行顺序。发布事件时监听器的调用顺序如果不可控可能出现“先发短信通知用户后减库存”这样的问题。另外如果一个监听器抛异常其他监听器还会不会执行我的经验是除非明确知道顺序和异常隔离的必要性否则最好用线程池把每个监听器丢到独立线程去执行或者至少在整个循环外套一层try-catch避免一个监听器的故障拖垮整个链路。这一点在团队协作时尤其重要因为别人写的监听器不受你控制。3.3 工厂模式把对象创建从业务代码里剥出去工厂模式是23个模式里最容易被滥用、也最容易被误解的。它的本质不是“帮你创建对象”而是“把创建对象的时机和方式从使用者那里隔离开”。我以前维护过一个多数据源的报表系统支持从MySQL、Oracle、ES三种数据源拉数据。每种数据源都要做连接、做查询语法适配、做结果集转换。报表业务代码不应该关心自己连接的是什么数据库它只需要说“给我一个数据查询器”。这就是抽象工厂的典型场景。实践中最常用的还是“简单工厂反射注册”的组合。用这种方式新增一种数据源只需要实现接口并注册不需要修改工厂的代码public class QueryExecutorFactory { private static final MapString, QueryExecutor REGISTER new ConcurrentHashMap(); static { REGISTER.put(mysql, new MysqlQueryExecutor()); REGISTER.put(oracle, new OracleQueryExecutor()); } public static QueryExecutor getExecutor(String type) { QueryExecutor executor REGISTER.get(type); if (executor null) { throw new IllegalArgumentException(unsupported type: type); } return executor; } }需要注意工厂模式解决的是“创建逻辑复杂且会变化”的问题如果你的对象构造函数就是简单的new X()没有任何变化空间那工厂就是多余的。有个判断标准我一直用如果创建对象时需要根据配置、参数、环境做一系类判断或者需要统一管理生命周期工厂才值得用。3.4 单例模式最简单的模式最容易写错单例模式每个程序员都认识但写对的人真不多。它解决的问题很单纯有些对象全局只需要一个实例比如配置管理器、线程池、连接池、日志器。但“全局只有一个”这个需求背后隐藏着三个问题线程安全、延迟加载、序列化破坏。我以前写过一个配置管理类用的就是最常见的双检锁写法看起来一点问题没有public class ConfigManager { private static volatile ConfigManager instance; private ConfigManager() {} public static ConfigManager getInstance() { if (instance null) { synchronized (ConfigManager.class) { if (instance null) { instance new ConfigManager(); } } } return instance; } }很多老手都会告诉你volatile关键字加上双检锁就够了这话在绝大多数场景下没问题。但如果你做的是框架级代码还得防止反射构造和序列化破坏。反射可以强行调用私有构造器序列化反序列化也能产生新实例。不过我还是坚持一个务实的原则绝大多数业务系统双检锁volatile就已经足够了。为了防御完全不可能出现的反射攻击而写上几百行枚举实现属于为了炫技而设计。单例模式真正的负面影响不是写错而是被滥用。当一个对象承担了太多职责你让它变成单例就等于把这份重量焊死在全局了。我见过把业务Service写成单例的而那个Service内部还存了可变的业务状态后来线上就出现了“A用户的请求把B用户的数据给覆盖了”这种诡异事故。所以我在笔记里专门给自己加了一条警告单例只适合无状态或者状态全局一致的对象有状态的业务对象千万别用单例。3.5 装饰器模式比继承更灵活的功能扩展给一个类加功能最朴素的想法是继承但继承是静态的、编译期绑定的而且会把一整套父类行为都继承下来造成大量无用的暴露。装饰器模式是在“不修改原类代码、不改变调用方式”的前提下给对象动态添加职责。最典型的就是Java IO。BufferedInputStream就是InputStream的一个装饰器它不改变读字节这个核心行为只是加了一层缓冲。我用装饰器比较多的地方是给支付接口加日志、加耗时统计、加重试机制。这些都是横切关注点不应该侵入核心支付逻辑。装饰器的实现核心就是组合转发。骨架大概是public class LoggingPayDecorator implements PayStrategy { private final PayStrategy delegate; public LoggingPayDecorator(PayStrategy delegate) { this.delegate delegate; } Override public void pay(String orderId, BigDecimal amount) { long start System.currentTimeMillis(); try { delegate.pay(orderId, amount); } finally { System.out.println(pay cost (System.currentTimeMillis() - start) ms); } } }但装饰器有个麻烦的地方包多了之后对象嵌套层次很深Debug的时候调用栈看起来非常吓人。而且如果装饰器之间还有依赖关系比如重试装饰器依赖于日志装饰器先填了某些上下文代码管理的复杂度就会直线攀升。我的经验是装饰器适合3层以内的轻量横切逻辑超过3层就要考虑用责任链或者代理框架比如Spring AOP来解决了。3.6 模板方法模式把流程定死细节留开模板方法模式我的理解是“父类写剧本子类当演员”。它是GUI框架和流程引擎里非常常见的模式在业务系统里也经常遇到比如一个审批流程大致骨架是提交→校验→业务处理→通知但每种审批单的校验规则和处理逻辑都不同。父类把这个四步流程固定下来子类只实现每一步的差异部分。写模板方法模式的时候我觉得两个关键细节决定成败第一骨架方法的访问权限应该设计成final防止子类不小心重写了整个流程。我见过一个团队父类的骨架方法没锁死结果有人为了图省事直接把整个模板流程在子类里重写了一遍后来流程改了要改五六个子类简直是灾难。第二差异方法尽量抽象成“钩子”而不是“必须实现的步骤”。比如“是否执行某一步骤”这种判断用isXxxNeeded()这样的钩子方法默认返回true子类按需覆盖。这样新增子类时的负担最小不会为了不关心的步骤写空实现。当然模板方法不是银弹。如果你的“流程骨架”本身也会变比如有时候三步有时候五步那模板方法就僵化了。这时候用策略模式把“流程”本身作为策略传入反而更灵活。模式之间本来就是可以组合的关键是看变化的维度在哪里。4. 用生活化类比帮助记忆一份私人的模式隐喻表整理笔记的过程中我发现一个规律那些我能记住的模式不是因为背得熟而是因为我在脑子里建立了一个鲜明的“画面”。抽象的概念一旦绑定了具体的图像记忆就牢固了。所以笔记里我专门建了一张“模式隐喻表”每个模式配一个自己的比喻。策略模式 换轮胎。轮胎的尺寸、花纹可以换但换轮胎的过程顶起车、拧螺丝不变。观察者模式 订阅报纸。报社不知道你有多少人订阅但报纸一出刊所有订户都会收到。工厂模式 食堂打饭。你不用管今天师傅炒了什么菜只要说“打一份套餐”师傅按菜单做出来给你。单例模式 公司的唯一打印机。全公司共享这一台不允许私自再装一台。装饰器模式 穿衣服。穿外套不改变你是“人”这个本质但给你增加了保暖功能。模板方法模式 做菜谱上固定步骤的菜备料、切菜、炒制、装盘但每个厨师具体怎么做不同。适配器模式 旅行转换插头。你的设备插头是两脚的墙上插座是三孔的转换插头让两者匹配。代理模式 经纪人或助理。电话先打到经纪人那里经纪人过滤之后才转给本人。责任链模式 层层审批的报销单。从组长到经理到老板每个人只处理自己能处理的金额。迭代器模式 翻书翻阅。不管书是厚是薄只要顺着页码一页一页翻就行。这些比喻不追求百分之百准确但它们帮我完成了从“抽象描述”到“直觉理解”的转换。你在整理笔记时也可以做类似的事情关键是用自己熟悉的生活场景而不是网上的标准比喻。记忆心理学里有个说法叫“精细编码”意思是说把新知识和已有的知识经验绑在一起记忆效果远好于机械重复。我深以为然。5. 实战中最容易踩的坑五个血的教训5.1 为了模式而模式我刚学设计模式的时候最大的毛病就是“手里拿着锤子看什么都是钉子”。学了策略模式就想着把if-else全改了学了单例就想着把所有Manager类都搞成单例。结果代码变得极其绕一个业务逻辑要穿过五六层抽象才能看懂。设计模式的核心是“在适当的时候解决适当的问题”不是代码风格的装饰品。一个原则我后来一直遵守**没有两个以上的可能变体或替代方案就不用引入模式。**比如前面说的支付策略是因为真的有四五个渠道且还在不断增加才值得用策略。如果你只有一个实现就老老实实写普通方法等第二个实现真的出现了再重构也不迟。还有一个判断标准非常好用**如果以后要改这个需求改动点是集中在一处还是会散落到很多地方**模式的引入应该是让“改动点”尽量集中的。如果重构完之后加一个功能反而要动更多文件那这个模式大概率用错了。5.2 接口设计得太重业务系统里最常见的反模式是“万能接口”。一个接口里塞了五六个方法新实现类为了凑数不得不写空实现或者抛异常。这在适配器和策略模式里特别常见。比如我见过一个团队设计的消息发送策略接口里面同时包含了sendEmail、sendSms、sendAppPush三个方法。结果短信策略实现类里sendEmail直接抛UnsupportedOperationException推送给策略实现类里sendSms也是空方法。这就是接口隔离原则被破坏的典型案例。正确做法是拆分成三个独立接口或者用“实现类按需选择”的方式设计比如使用默认方法default method时不要滥用因为默认方法本质上会让接口承担太多隐性职责。5.3 忽略了模式的代价每个模式都有代价只是很多教程不告诉你。策略模式的代价是多了一堆类和接口类爆炸观察者模式的代价是调用链不确定性出问题难排查代理模式的代价是性能损耗和调试困难单例模式的代价是全局状态和测试困难。所以我在笔记每个模式的“反模式预警”里都会写清楚引入这个模式后我实际付出的额外成本。比如观察者模式我付出的代价是有一次一个监听器抛了个NPE导致事件发布链路中断后面的监听器全没执行。那次线上故障之后我强制规定所有监听器必须实现异常隔离。这个教训后来就成了笔记里很重要的一条提醒。5.4 模式落地时没有考虑团队认知水平这是个很多人忽略的问题。设计模式是团队协作的工具而不是一个人的自嗨。如果你用了责任链模式但团队里大多数人从来没写过责任链那你就要考虑代码的可维护性。一个模式用得再优雅如果别人看不懂他修改的时候最可能做的事情就是绕过你的优雅设计在旁边直接加一个if-else。我的做法是重要项目里引入不常见的模式之前先给团队做一次简短的分享把核心思想和代码结构讲一遍同时在代码注释里写好“为什么这么做”的上下文。技术分享这种事看似费时间实际上能省下未来大把的沟通成本。另外代码Review的时候也要特别关注“模式使用是否克制”这个问题如果引入的模式只是为了好看而没有解决实际问题就果断退回去。5.5 学完就忘模式需要刻意练习最后一个坑也是大多数人学设计模式失败的原因——没有刻意练习。看书、抄代码、背定义这些都属于“输入”而真正让模式内化的是“输出”。所谓输出就是找几个真实的需求场景强迫自己用不同的模式去实现然后对比它们各自的优劣。比如我做过一个练习同样的“运费计算”需求分别用策略模式、装饰器模式、责任链模式、模板方法模式写一遍然后从可扩展性、可读性、性能、测试难度四个维度打分。做一遍这样的对比比看十篇文章都管用。因为这些模式在你手里真正“交过手”了它们的边界在哪里各自的优势是什么你就会有一个切身的体会。6. 从GOF笔记到架构思维设计模式的进阶路径整理完GOF笔记之后我最大的感受是**设计模式的终点不是记住模式而是形成架构思维。**所谓架构思维就是当你看到一个需求的时候脑子里自然浮现出的不是某一个具体模式而是一幅权衡图变化在哪里、稳定在哪里、该用什么机制去隔离变化。拿一个最简单的例子说用户要求“增加一个充值渠道”。没有架构思维的开发第一反应是“在业务代码里加一个if-else分支”。有架构思维的开发第一反应是“充值渠道是一个变化点应该用一个稳定的机制来隔离它”。这时候他们可能会想到策略模式、工厂模式、或者依赖注入的方式但不管具体用什么核心的思路是一致的识别变化封装变化隔离变化。设计模式就是这个过程的工具箱。23个模式本质上是23种常见的“变化点封装方式”的总结。你在笔记上花的时间越多对这些封装方式的理解就越深遇到新问题时做判断的速度就越快。进阶的第二步是从模式上升到原则。阅读GOF笔记的时候你会发现很多模式背后重复出现的几条原则面向接口编程而不是面向实现、组合优于继承、开闭原则、依赖倒置。这些才是比模式更底层的思想。掌握了一个模式你解决的是一个问题掌握了背后的原则你可以自己发明模式解决一类问题。我之前在做日志系统的时候需要设计一个“多级缓存写入”的机制。一开始套模板方法后来发现缓存层数不确定又改成了责任链但写起来总是别扭。最后回过头来想想其实核心原则就是“每一个缓存层只关心自己这层的写入策略同时把请求往下传”。想通了这一点具体用什么模式反而不重要了——我用了一个非常类似责任链但又不完全一样的自定义结构反而更贴合当时的场景。这就是从“模式思维”升级到“原则思维”之后的自由度。所以我的建议是GOF笔记不是用来背的是用来越翻越薄的。第一遍整理时你可以事无巨细第二遍就只留下你自己踩过坑的模式到第三遍你甚至可以把大部分笔记删掉只留下一张“模式选择路线图”。到了那个阶段设计模式就真正成为你思维的一部分了而不是脑子里的一个负担。我个人在实际操作中的体会是这份笔记带给我最大的价值不是某个模式本身而是让我养成了一种条件反射遇到一个需求先停下来问自己“哪里会变哪里稳定怎么隔离”带着这种思维去写代码哪怕不用任何模式代码的质量也会比之前高一个台阶。整理GOF笔记表面上是在学23个模式实际上是在练一种面向变化的思维方式。