资讯动态

设计模式阶段总结:创建型与结构型的对比及实战避坑

发布时间:2026/10/20 8:20:04 来源:尧图企业网站定制
“重温设计模式”这个系列写到第六篇本来按照原定计划接下来应该继续拆新的模式可我在动手写正文之前总觉得哪里不对。前五篇一口气讲了不少东西从设计原则、UML类图到创建型模式里的单例、工厂、抽象工厂、建造者、原型再到结构型模式里的适配器、桥接、组合、装饰器、外观、享元、代理。知识点密度很高但真要合在一起用脑子里的图其实是散的。所以这一篇算是我给自己、也给读者的一次阶段总结把散落的东西串成线把容易搞混的模式放到一起对比再把自己踩过的坑摆出来。这个过程不一定华丽但对真正想掌握设计模式的人一定比继续追新内容更值钱。1. 为什么偏要在六篇的位置停下来做总结1.1 不总结的学习都是“假性前进”很多人学设计模式是打卡式的今天读一个单例明天看一个工厂每个模式都配一个经典例子看完觉得懂了代码也抄了一遍过两周再问脑子只剩一个名字。这种状态我以前也有过后来才发现问题出在“纵向输入横向缺失”上。设计模式的经典分类是创建型、结构型、行为型。绝大多数教程也是按这个顺序一章一章往下讲所以学习路径天然是纵向的。但真实业务项目不会按这个顺序给你出题。一个订单模块可能同时涉及工厂方法、适配器、观察者、策略一个登录流程可能同时出现代理、模板方法、状态。如果脑子里装的只是孤立的“解题技巧”真到了现场反而不知道从哪一招下手。阶段总结的意义就是强制自己把已经学过的模式从“点”连成“网”。我不是要在这里罗列定义而是要做一个横向对比看看每个模式关注的痛点是什么、它们之间有哪些重叠、哪些模式容易被误用成另一个。这一步做完后面的行为型模式再进来时才不会被淹没。1.2 这一阶段的模式全景图先亮一下前五篇到底覆盖了哪些东西避免后面讨论时没有共同坐标。这一阶段重点落在创建型和结构型行为型只开了个头还远没到展开的时候。分类模式一句话定位核心关注点创建型单例保证一个类只有一个实例实例生命周期、全局访问点创建型工厂方法把创建对象的逻辑下沉到子类避免在调用方直接 new创建型抽象工厂创建一族相关对象产品族的一致性创建型建造者分步骤构建复杂对象构建顺序、参数约束创建型原型复制已有对象来生成新对象克隆开销、复制代价结构型适配器让接口不兼容的类协同工作接口转换、兼容旧代码结构型桥接抽象与实现分离各自变化多维度扩展结构型组合用树形结构统一叶子与容器递归处理、部分整体一致结构型装饰器动态增强对象功能组合优于继承、避免子类爆炸结构型外观为复杂子系统提供统一入口简化客户端调用结构型享元共享细粒度对象减少重复创建内存优化、对象复用结构型代理控制对真实对象的访问权限控制、延迟加载、日志埋点列完这张表我自己也松了口气。虽然这些模式名字都见过但真要一句话概括很多人是说不清的。比如桥接和适配器经常被当成一回事装饰器和代理有时也分不清。后面我会专门把这种容易搞混的组合拿出来逐个对比。2. 这一阶段到底在解决什么问题2.1 创建型模式的核心逻辑把“对象怎么来”封装起来创建型模式占了前几篇的很大篇幅因为它是很多人写代码时最常犯毛病的地方——到处 new。new 本身没有错错的是 new 的行为散落在业务代码里一旦对象来源、类型、初始化方式发生变化所有调用点都要跟着改。比如一个订单处理模块最初只有普通订单代码里直接 new NormalOrder()。后来增加团购订单、预售订单、跨境订单原来的调用点就要逐个加 if。这时候工厂方法就可以把变化收口调用方只依赖订单抽象具体创建逻辑交给工厂新增类型时只动工厂调用方完全不需要改。这个模式背后的原则是依赖倒置也是开闭原则的直接体现。抽象工厂解决的是另一类问题产品族的一致性。假设一个 UI 组件库需要同时产出按钮、输入框、弹窗而不同主题下这些组件风格必须统一。如果让客户端自己决定创建哪一套很容易出现“按钮是深色主题弹窗却是浅色主题”的错乱。用抽象工厂把同一主题的产品绑定在一起保证客户端拿到的始终是配套的一组。这里的关键不是“怎么创建单个对象”而是“怎么保证一组对象的搭配关系”。建造者做的事情又不太一样。它关注的是复杂对象的构造过程。比如一份报表对象有十几个字段有的必填有的选填字段之间还有依赖关系。直接用构造函数一长串传参调用方根本分不清哪些是必要的哪些可以忽略。建造者模式把构建过程拆成方法让调用方按步骤装配还能在 build 阶段做参数校验。我在实际项目里最推崇它的一点是一个不合格的建造过程可以被拒绝在门口而不是等你拿到一个半成品对象后才发现漏洞。原型模式相对冷门但在某些场景下非常值钱。它的核心是“复制”而不是“重建”。比如一个图形编辑器里用户复制一个复杂图形如果每次从零渲染开销很大直接用原型克隆会快得多。注意这里的复制需要区分浅拷贝和深拷贝否则克隆出来的对象还共享着同一份内部数据改一个就全乱了。2.2 结构型模式的核心逻辑类与对象如何组合结构型模式听上去很抽象其实本质上就是在回答一个问题现有的类和对象已经写好了怎么拼出新的能力又不把代码搞成一锅粥。适配器最好理解它就是现实世界里的转接头。你手里有一个旧接口的类但新系统的调用方只接受新接口两边都不想改代码那就做一个适配器把旧接口包装成新接口。这个模式在接第三方 SDK 的时候尤其常用。我见过不少团队因为嫌适配器“多包了一层”直接把第三方 SDK 的类型穿透到业务层结果后面换厂商时业务代码里到处是旧厂商的痕迹只能加班重改。用适配器把第三方接口隔离在外侧才是真正划算的投资。桥接则要回答一个更“分裂”的问题抽象和实现如果都在变怎么避免类爆炸。举例来说一个绘图程序要支持圆形、方形两种形状每种形状又要支持直线渲染和波浪渲染。用继承来组合就是 2×2 个类再加一种渲染方式就是 3×2 个类数量会指数涨。桥接的思路是把形状和渲染方式拆成两条独立的维度各自继承再用组合的方式把它们接起来。我经常用“桥”这个字来记抽象维度走在桥的这一边实现维度走在桥的那一边两边各自扩展互不污染。组合和装饰器也非常容易混淆。组合解决的是树形结构一个大部门里有子部门子部门里又有小组最终每个节点都要支持统计人数、打印名称。用组合模式把叶子节点和容器节点统一成一个接口调用方不用关心自己在操作一个小组还是一个大部门递归自会处理。装饰器解决的则是“功能增强”的问题你有一个基础组件想在运行时给它加缓存、加日志、加权限校验又不想改原来的类。装饰器一层套一层每个装饰器和被装饰对象实现同一个接口所以对外看起来还是一个组件的用法。组合是树装饰器是洋葱别搞反。外观模式则是给子系统做个门面。比如一个电商后台的订单导出功能背后涉及订单查询、库存校验、物流信息组装、文件生成、权限审核五个子系统。客户端要是直接面对这五个子系统写起来累耦合也重。用一个门面类把这一套流程串起来客户端只调一个方法简洁多了。但要注意外观模式是“简化入口”不是“禁止访问底层”。如果需要某些客户端操作底层细节门面不要拦着。享元和代理放在一起说也很合适。享元关注的是对象复用尤其适合“大量重复、粒度很细”的对象。比如文本文档里的每个字符如果每个字符都建一个完整对象内存很快被吃光用共享池把相同的字符复用一份能省下大量空间。代理关注的是访问控制你想调用真的业务对象但希望在调用前后做一些额外处理比如权限校验、延迟加载、日志记录。代理和装饰器代码上长得很像但意图不同——代理侧重“管住访问”装饰器侧重“增强能力”。后面我会专门对比。3. 几个最容易被搞混的模式横向对比一次3.1 工厂方法、抽象工厂、建造者谁是谁这三个模式都跟创建对象有关很多人看完就乱。我习惯用三个问题来分你只需要创建一个对象但不知道具体是哪个子类用工厂方法。你需要创建一组配套对象少一个都不行用抽象工厂。你要创建的是一个复杂的对象里面包含很多可选部分而且装配顺序有讲究用建造者。表格可能更直白对比维度工厂方法抽象工厂建造者创建粒度单个对象一整套相关对象单个复杂对象核心意图把 new 延迟到子类保证产品族配套控制构造过程客户端关注得到抽象产品得到配套产品组分步骤装配常见误用只有一种产品也硬上工厂把工厂方法堆到一个类里用建造者处理简单对象我举个例子。开一家餐厅工厂方法是“根据顾客点单做一道菜”抽象工厂是“按套餐标准出一整套菜汤甜点”建造者是“顾客自定义一顿饭先选主食、再加配菜、最后决定要不要饮料”。三者都能创建对象但服务的场景完全不同。实战里如果发现自己在一个工厂类里写了一堆 if/else 去创建不同类型的对象然后又发现这些对象之间还有组合关系那大概率是该上抽象工厂而不是硬拡工厂方法。3.2 适配器、外观、代理中间层不是一回事这三个模式都会在客户端和真实对象之间插一个中间层所以经常被拿来做选择题。适配器的目的是“兼容”旧类代码不打算改新接口又必须满足所以写一层转换。外观的目的是“简化”复杂的子系统调用链太长门面类把流程收拢让客户端少写代码。代理的目的是“控制”你明明可以访问真实对象但你在调用前后要加权限、日志、缓存或者真实对象的创建开销很大想延迟加载所以让代理挡在前面。一个实操中的判断技巧问自己“如果没有这层中间层功能还能不能正常运作”。如果答案是不能因为接口对不上那是适配器如果答案是能只是客户端调用太繁琐那是外观如果答案是能但你想管住访问、控制时机、加一层能力那是代理。我之前看过一段代码一个同学给支付接口包装了一个类类名叫 PaymentProxy但内部把所有支付、退款、查询、对账的逻辑都收进去了还宣布说这叫“代理模式”。这其实已经偏离了代理的本意更像一个外观类。代理的核心是“和被代理对象同接口”而那个 PaymentProxy 的接口明显比底层支付服务大一圈这不是代理是门面。命名和意图一致代码评审的时候才能少吵架。3.3 装饰器、继承和代理三选一该怎么选装饰器和继承都能给类加功能这也是经典考题。继承是静态的在编译期就定死了装饰器是动态的可以在运行时按需组合。继承一旦深下去子类爆炸是必然的。我们要提供一个带缓存、带日志、带格式化的数据读取器用继承就得写缓存Reader、日志Reader、格式化Reader、缓存日志Reader……排列组合会越来越多。用装饰器就简单了数据读取器套缓存装饰器再套日志装饰器再套格式化装饰器想要哪个功能就往外面包哪层。装饰器和代理的对比更隐蔽。两者都遵循“和真实对象同一个接口”都可能在调用前后做事。区别在意图装饰器是给对象加能力调用方知道自己在用增强后的对象代理是控制访问有时候调用方甚至不知道存在代理。例如权限校验、远程调用这些是代理的活加功能、附加行为是装饰器的活。我自己的经验是写代码前先在注释里写一句“我为什么要包这一层”如果写不出来那一层大概率是多余的。4. 怎么判断该用哪个模式一套实操判断法4.1 先找变化点再匹配模式模式选型不是按模式表一个个试而是先读懂业务里的变化点。变化点通常藏在这样的描述里“以后可能会新增订单类型” → 工厂方法或策略模式前者管创建后者管行为。“以后可能会新增主题风格而且每个风格包含一系列组件” → 抽象工厂。“对象构造参数很多而且容易配错” → 建造者。“我们要接第三方的接口但不想让业务层依赖它的类型” → 适配器或外观。“某个功能要经常开关注册、日志、限流” → 装饰器或代理。“系统里有成千上万个结构相似的对象内存吃紧” → 享元。“客户端直接访问真实对象太危险需要经过审批” → 代理。这套做法的关键不是死记模式而是先承认一个事实代码中的变化是常态。谁的变化频率高谁就应该被抽象边界保护起来。如果某个模块一年也不可能有第二个实现强行上个抽象除了让阅读者多绕一层没有任何收益。4.2 从“痛点”反向找模式有些人觉得找变化点很难那反过来走把现在代码里的痛点列出来对照着找。痛点一是“new 得满地都是类型一多就想骂人”。解决办法不是马上补工厂而是先把所有 new 的位置找出来。如果它们都是为同一类接口创建不同实现那就是工厂方法如果这些 new 出来的对象必须成套出现那就是抽象工厂。痛点二是“新接的库接口和业务接口对不上只好写一堆胶水代码”。胶水代码散落在各个调用点将来换库时特别痛苦。把胶水收拢到一个类里对外输出业务接口就是适配器。如果这个库本身包含大量复杂步骤你还想简化调用那就是外观。痛点三是“给一个核心对象叠加功能子类越来越多”。每加一个功能就写一个子类这种味道太明显了。把所有可选功能拆成装饰器运行时自由组合既能保留核心对象简洁又能让功能组合数趋于无限。痛点四是“想给老方法加日志、加权限又不敢动老代码”。这时候先想清楚是控制访问还是增强功能。如果是控制访问用代理如果是增强功能用装饰器。别看到“在方法前后加东西”就一律代理意图不对后续维护的人会看晕。4.3 一个订单模块的选型示例写一个简化例子假设某公司的订单模块要扩展具体需求有三条支持多种订单类型接入新的支付渠道支付前后要埋日志。订单类型多用工厂方法很自然每个订单子类实现自己的校验和计算逻辑工厂根据类型创建对应子类。接入新支付渠道业务层不想依赖渠道 SDK 的专属类型这里用适配器最合适。每个渠道写一个 Adapter实现统一的 PaymentGateway 接口将来再换渠道只需要新增一个适配器业务层纹丝不动。支付前后埋日志如果按“静态继承”来设计订单类会越来越臃肿。日志这个功能和订单类型本身没有关系它是横向切入的关注点更适合做成代理或者装饰器。具体是用代理还是装饰器就看埋日志是不是想控制对订单支付的访问如果只是单纯记录调用时间和结果装饰器足够如果还涉及权限校验、限流熔断那就偏向代理。最后我选择的是装饰器因为日志功能不改变调用方式只是给支付动作裹了一层增强。这个例子没有引入任何高深技巧但已经覆盖了工厂方法、适配器、装饰器三种模式。现实项目里模式往往就是这样组合出现的而不是一次性上一堆。每加一个模式我都会先验证一个核心问题这一步是不是真的让某个变化点更容易处理了如果答案模糊宁可不动。5. 实战中踩过的坑写给你们当参考5.1 过度设计一个接口五个实现类每个实现类只有两行我在评审代码时经常看到这种东西定义一个 IHandler 接口下面挂五个 Handler 实现类每个实现类只有一两行而且目前项目里只有一个实现会被真正用到。问同学为什么要这样写得到的回答往往是“为了以后扩展”。这个理由本身没错但没有变化点的扩展就是幻想式抽象。我的建议是先写简单代码等到真的出现第二个变体再做提取抽象。设计模式很多时候是重构的终点不是代码的起点。第一次写一个普通类完全没问题出现相似逻辑后再通过重构收口比你一开始凭想象力铺开一堆抽象要稳得多。判断是否过度设计的标准也很简单把当前这个抽象删掉会不会影响任何现有功能如果不会而且以后也没有明确的需求推着它变化那就先别加。5.2 单例滥用把全局可变状态全塞进单例单例模式可能是被误解最深的一个。很多人一看到“某个工具类只需要一个实例”就把它做成单例然后在里面放了一堆可变状态用户信息、缓存 Map、配置对象。这样做的后果是什么所有代码都能全局修改这份状态排查问题的时候根本不知道是哪一块动了数据测试也没法隔离同一个测试跑两遍结果可能都不一样。我个人的经验是单例更适合无状态的服务或者资源型对象比如线程池、连接池、日志管理器。有状态的数据尤其是业务上下文数据别往里塞宁可显式传递也别用全局单例兜着。现在很多框架都支持依赖注入直接用容器管理生命周期比手写 Singleton.getInstance() 干净得多。记住全局访问点解决的是“访问便捷”不是“状态共享”。5.3 工厂类变成“万能工厂”工厂方法用一段时间后有些人会犯一个错把所有创建逻辑都丢到同一个工厂类里。慢慢的工厂类的 create 方法参数越来越多类型判断越来越多方法体越长越长最后变成一个比业务类还难维护的上帝类。为什么会这样因为创建不同类型对象的逻辑虽然被收口了但没有被拆分。解决的办法不是继续往一个工厂里塞而是考虑拆分为多个工厂或者用抽象工厂按产品族隔离或者把装配过程交给建造者。另外如果 create 方法的参数超过三四个建议用参数对象封装或者直接分析一下是不是有多重职责挤在这一个方法里。代码评审时看到工厂类超过两百行我一定会提醒同事先别急着写新功能把这个工厂拆了。5.4 模式扎堆调用栈深得看不清模式可以组合使用但不是越多越好。有一次我排查线上问题顺着调用栈一路往下点结果看到代理套着装饰器装饰器里又调了适配器适配器再进外观前前后后六层包装每一层都只是薄薄的转发代码。问题出来的时候你根本分不清到底是哪一层出了问题打日志要逐层加性能影响也被放大了。这里有一个度的问题外层包装是为了隔离关注点但包装层级太多调试和理解的成本会迅速上升。我的建议是如果一条核心调用链的包装层数超过四层就要反思是否可以把部分逻辑合并或者让门面类直接承担一部分职责。代码是给人读的其次才是给机器跑。模式是工具不是勋章不是为了在代码评审里显得自己很厉害。6. 阶段总结剩下的思考从“认识模式”到“识别气味”6.1 学模式不是背字典而是培养重构直觉说了这么多我最大的体感是设计模式的学习不能停留在“我知道有这个模式”的层面要慢慢转变成“闻到代码坏味道时能想起某个解法”。比如看到一个类的职责太多你会想到单一职责和外观看到接口参数对不上你会想到适配器看到大量规则分支你会想到策略或工厂。这种直觉不是靠背定义得来的而是靠大量阅读别人的代码、加上亲手重构得来的。我建议每个学模式的人都准备一个自己的案例库。不用多每个模式配一个真实业务里的例子就够了。这个例子可以是从网上看到的也可以是自己项目里重构出来的记录时写清楚三件事当时遇到了什么问题为什么会想到用这个模式用了之后改善在哪里。这种记录比抄十遍示例代码有效得多。6.2 我给初学者的几个不成熟建议第一别着急把二十三个模式全部看完。先吃透创建型和结构型然后在项目里找到一处可以用的地方小范围应用观察效果再回来对比学习资料印象会深好几倍。第二写代码的时候先按直觉写写完再拿设计原则去检查有没有到处 new、是不是接口对不上、有没有子类爆炸有坏味道就重构重构时自然会遇到合适的模式。第三多去代码评审里看别人怎么用模式。很多时候“看别人用错了”比“看别人用对了”更能教会你模式的使用边界。6.3 对下一阶段的安排行为型模式在这个系列里还没有系统展开观察者、策略、模板方法、状态、责任链这些都是业务代码里出现频率非常高的模式。我打算在下一篇开始逐个拆开聊但不打算按教科书顺序平铺而是按“业务场景驱动”的方式来讲先抛一个真实会遇到的需求再一步步推导出对应的模式结构。这样可能更适合实战派也和我这一阶段的总结思路保持一致。阶段总结写到这里我自己也重新梳理了一遍从创建型到结构型的知识脉络。回头再看前五篇的内容很多地方有了新的理解。希望这篇总结对你也有一点帮助也欢迎把你心里容易混淆的模式拿出来讨论下一阶段我们一起把行为型这关也过了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑