ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

设计模式分类地图:从三大类型到模式辨析与选型

设计模式分类地图:从三大类型到模式辨析与选型 带团队这几年面试是个特别好的照妖镜。你问“设计模式的分类有哪些”十个人里有八个能背出“创建型、结构型、行为型”但接着问“这三个分类各自在处理什么根本矛盾”“工厂方法和抽象工厂在真实项目里怎么选”“装饰器和代理的代码长得那么像你怎么区分”过半的人就开始含糊了。设计模式本身不难难的是脑子里缺一张“分类地图”。没有这张地图你学到的每个模式都是孤岛碰到实际问题时不知道该从哪座岛上找答案。这篇是设计模式系列的第一篇先不逐个讲单例、工厂、观察者怎么写而是把“分类”和“区别”两个地基打牢。我想聊明白这几件事23个经典模式靠什么维度组织起来三大分类各自的职责边界每一类里最容易被混淆的模式之间到底是什么关系以及拿到一个需求时怎么反推候选模式。适合刚接触设计模式、读了几章书却串不起来的读者也适合想系统梳理一遍、把零散经验归好类再往上走的开发者。语言上我会尽量少用八股味的定义多讲场景和取舍。1. 为什么必须先有“分类”才能谈“使用”1.1 看书总是忘问题出在“没有索引”很多人学设计模式都有这么个经历头几天热血沸腾把《Head First设计模式》或者GoF原版翻了一半每个模式看起来都“恍然大悟”觉得“原来还能这样写”。结果一周后再碰到具体问题脑子里只剩几个碎片隐约觉得“好像有个模式能解决但叫什么名字、怎么搭结构全还给书了”。这不是记忆力的问题是学习方式的问题。23个模式平铺在脑子里的时候你其实是在背字典偏偏字典又是最不适合死记硬背的东西。分类的价值本质上就是给大脑建索引遇到“对象创建复杂”的问题先想到“去创建型那一族找”遇到“要动态扩展又不改原类”知道目标是“结构型那一片”。有了这张索引检索范围一下子从23缩小到七八个再往下定位就快多了。我在实际带人的时候发现一个规律能把模式用对的人通常不是背得最多的那个而是脑子里有一张清晰“模式关系图”的人。这张图的第一层就是分类。1.2 GoF分类法目的为主范围为辅1994年Erich Gamma等四位作者出版了著名的《Design Patterns: Elements of Reusable Object-Oriented Software》也就是业内常说的GoF书。书里给出一套经典分类框架用了两个维度来组织23个模式。第一个维度是“按目的分”也就是大家最熟悉的三大类创建型Creational、结构型Structural、行为型Behavioral。这个维度回答的是“这个模式在解决什么阶段的问题”。第二个维度是“按作用范围分”分为类模式处理类与子类的关系基于继承和对象模式处理对象之间的关系基于组合。很多教材讲分类时只讲目的维度把第二个维度忽略掉了。但这个维度其实非常有用因为它直接指向面向对象设计里一个更根本的问题你究竟想靠继承还是组合来构建系统。后面我会专门用一章来说它。1.3 三种目的的一句话版职责我习惯用三句特别直白的话来记这三个分类创建型管的是“对象怎么来”结构型管的是“对象怎么组合”行为型管的是“对象之间怎么协作、各自的活儿怎么分”。别小看这三句话。GoF当时的分类逻辑追根究底来自面向对象系统里最朴素的三个阶段任何对象都要先被创建出来然后组装成更大的结构最后在运行过程中互相协作完成业务。每个阶段都有容易“设计腐坏”的点于是就有了对应的模式族群。理解了这条演进脉络你就不会再觉得分类是死记硬背的概念它其实是软件设计的自然逻辑。2. 创建型模式把“new”这件小事管出花来2.1 对象创建为什么是个问题有人说创建对象不就是new一下吗有什么好研究的。这是初学阶段很常见的想法也是后面很多设计问题的起点。系统变大之后裸new会带来三件麻烦事。第一调用方和具体类绑死了。你new了一个ClassA就依赖了ClassA这个具体类型。将来想换成ClassB所有new过ClassA的地方都得改牵一发动全身。第二对象创建的过程越来越复杂。很多对象不是一行new能搞定的它可能要读配置、连数据库、设置一堆默认值。如果这套初始化逻辑散落在客户端代码里每个使用方都要重复写一遍后面想加一个字段得同步改十处。第三没法统一控制。有些对象希望全局只有一个比如配置管理器有些希望同一个对象别重复创建比如重量级的连接池。如果全是裸的new这些约束根本无从谈起。创建型模式干的事情本质上就是“把怎么创建对象和谁来使用对象拆开”。调用方只面对一个工厂接口或者一个构建入口至于背后到底new了哪个类、怎么new的不需要关心。2.2 创建型家族的六个成员按GoF的正式收录创建型有五个工厂方法Factory Method、抽象工厂Abstract Factory、建造者Builder、原型Prototype、单例Singleton。另外还有一个“编外成员”简单工厂Simple Factory它因为实现太简单、不算严格的意义上的模式所以没被GoF收录但实际项目里出场率极高。我讲创建型一般都会把它一起带上。按我个人多年使用频率排序单例 工厂方法 抽象工厂 建造者 原型 简单工厂编外。这个顺序不是权威排名只是我的经验而且跟项目类型强相关如果你做的是业务系统单例和工厂一定绕不开如果你做的是基础框架、中间件抽象工厂和建造者出场机会就更多。2.3 单例其实是“控制数量”不是“方便全局访问”单例是所有模式里最基础、也最容易被误用的一个。它的核心目标是确保一个类只有一个实例并提供一个全局访问点。但很多人在项目里把它当“全局变量”的替代品什么状态都想往单例里塞最后写出一堆隐式依赖把代码搞得乌烟瘴气。这也就是为什么单例经常被一部分人称为“反模式”。我自己定了一个判断标准只有“全系统必须只有一个实例且多个实例会造成严重问题”的时候才用单例。比如配置中心、日志管理器、连接池这类资源型对象多个实例不只是浪费还可能导致数据错乱。如果只是图方便想在任何地方都能访问某个对象那应该考虑依赖注入而不是单例。2.4 工厂、建造者、原型的分工创建型家族里剩下的模式各自解决不同形态的创建问题。工厂方法/抽象工厂解决“到底创建哪个具体类”的问题把选择逻辑收口到一个工厂里客户端不感知具体类型。建造者解决“一个对象的构造参数特别多、构造过程分步骤”的问题。比如组装一台电脑CPU、内存、硬盘各自独立配置最后组装。建造者把分步构建过程封装起来避免构造器参数写到七八个。原型解决“创建对象成本高而复制对象成本低”的问题通过clone来做对象复制。实际项目里用得相对少但做对象快照、撤销重做这类功能时会遇到。这几个模式不是互相替代的关系而是针对不同场景的工具。它们的共同点是让对象的创建过程可控、可复用、不污染业务代码。3. 工厂三兄弟最让人上头的辨析现场3.1 简单工厂一个类管所有产品的创建先看最简单的一种简单工厂。它通常是一个静态方法或者一个专门的工厂类方法里根据参数switch/if返回不同产品。举一个经典的计算机器例子public class OperationFactory { public static Operation create(String operator) { switch (operator) { case : return new AddOperation(); case -: return new SubOperation(); default: throw new IllegalArgumentException(不支持的运算符); } } }调用方传入符号工厂返回对应的运算类。优点是简单直接、代码量少。缺点也很明显每新增一种运算都要打开工厂类改switch这违反开闭原则。所以它只适合产品种类少、扩展不频繁的场景或者作为局部小工具出现。3.2 工厂方法把“创建哪个产品”下沉给子类工厂方法的设计思路是把工厂自身也抽象化。你定义了一个抽象工厂类里面放一个抽象的创建方法每个具体产品对应一个具体工厂子类。新增产品时不需要改现有代码直接新增一个产品类和一个对应的工厂类即可。public abstract class LoggerFactory { public abstract Logger createLogger(); } public class FileLoggerFactory extends LoggerFactory { Override public Logger createLogger() { return new FileLogger(); } }这段代码里最关键的变化是客户端依赖的是LoggerFactory这个抽象具体创建哪个Logger由运行时到底拿到哪个子类工厂实例来决定。创建逻辑从“参数判断”变成了“类多态”扩展性好了代价是类的数量几乎翻倍。工厂方法也是GoF里少见的“类模式”因为它的扩展靠的是继承子类。3.3 抽象工厂从“一个产品”升级到“一族产品”抽象工厂解决的是“一个产品族”的创建问题。什么是产品族就是一系列配套使用、风格必须一致的产品。举个最直观的例子一套暗色UI主题包含按钮、输入框、弹窗它们必须风格统一。如果你把暗色按钮配上了亮色输入框那视觉效果是灾难级别的。抽象工厂正是为这种“整族创建”而生的。看一下典型结构抽象工厂接口里定义多个创建方法比如createButton、createInput、createDialog每个具体工厂负责创建一整族完整产品比如DarkThemeFactory创建暗色三件套LightThemeFactory创建亮色三件套。客户端只依赖抽象工厂和抽象产品完全不感知具体主题风格将来加一个新风格主题只需要新增一个工厂类现有代码完全不用动。3.4 三个工厂到底怎么选三种工厂层层递进初学者特别容易纠结。我提供一个很实用的判断逻辑如果系统里只有零星几个产品且不准备频繁扩展简单工厂完全够用别为了模式而模式。如果产品种类会持续增加且每个产品的创建逻辑差别明显用工厂方法。如果产品必须配套出现、形成多个“系列”用抽象工厂。再浓缩成一句话简单工厂用参数选产品工厂方法用子类选产品抽象工厂用“族”来选产品。这句话能解决七成的选择困难剩下三成需要结合具体代码结构去感受。3.5 一个实盘场景跨多平台的日志模块举个例子假设我要做一个日志模块一开始只输出到文件和控制台。最初我用简单工厂createLogger(file)返回文件日志createLogger(console)返回控制台日志完全够用。后来要接Kafka于是改工厂类加一个case。再后来又来了云日志服务工厂类变得越来越肥每次加类型都心惊胆战。这时候我改成工厂方法FileLoggerFactory、ConsoleLoggerFactory、KafkaLoggerFactory各自实现createLogger。以后每新增一种日志源只加一个具体日志类和一个对应工厂类旧代码一行不动。如果哪天需求变成“必须提供一整套企业级日志方案”包括文件、报警、审计那就再往抽象工厂走把配套的日志产品整族生产出来。这就是三种工厂在真实演进过程中非常典型的脉络简单工厂解决当下工厂方法应对扩展抽象工厂应对“一整套配套需求”。4. 结构型模式对象之间怎么搭积木4.1 结构型的核心命题扩展功能但不破坏现有代码结构型模式一共有七个适配器Adapter、桥接Bridge、组合Composite、装饰器Decorator、外观Facade、享元Flyweight、代理Proxy。它们共同回答的问题是已经有了一个类或一组类我想让它们组合出更强的能力但要尽量少改甚至不改原有的类。“组合优于继承”这个设计原则在这里体现得最集中。继承当然也能扩展但继承是编译期就定死的一个类只能继承一个父类多层继承后代码会变得极难维护。结构型模式更多是利用对象组合在运行期动态地组合出新的能力。4.2 适配器 vs 外观一个补接口一个做门面适配器和外观经常被放一起比较因为它们的代码结构都有点“包一层”的感觉但意图完全不同。适配器解决的是“接口不兼容”。你的系统里预期的是A接口第三方库给的是B接口两边对不上适配器就在中间做转换。常见场景是对接多个支付渠道微信支付SDK暴露的方法名和支付宝不一样你写一个Adapter把两者统一成自己系统内部的支付接口。这样上层业务代码就不用关心底层到底接的是哪一家。外观解决的是“子系统太复杂”。一个智能家居控制中心下面有灯光、空调、音响三个子系统客户端如果直接操作得先摸清每一个子系统的内部API。外观Facade提供了一个简单的“一键离家模式”关灯、关空调、关音响一次调用全搞定。外观并不改变子系统本身的接口只是给客户端提供了一个更简单的门面。一句话区别适配器是为了让“原本不兼容的接口”能一起工作外观是为了让“太复杂的子系统”用起来更简单。4.3 代理 vs 装饰器都是套了一层壳动机截然不同这俩是面试高频题因为代码形态几乎一模一样都是一个包装类抱住原始对象。两者的区别完全在意图上。代理关注的是“控制对原始对象的访问”。它不关心给原始对象增强多少新功能更关心访问时机、权限、范围。典型场景有三个远程代理本地调用一个代理实际请求发到远端服务、虚拟代理延迟加载真正需要时才创建昂贵对象、保护代理权限校验角色不够就不让你调。装饰器关注的是“增强原始对象的功能”。它给对象动态叠加新行为而且支持多层叠加。典型场景一杯奶茶加珍珠、加椰果、加奶盖每加一层就多包一层装饰器最终调用时一层层叠加增强。我自己的记忆口诀代理是“我替你做但我控制你什么时候做、谁能做”装饰器是“我包着你然后给你加料”。代码长得再像回到意图上一秒就能分清。4.4 结构型模式选型速查表需求信号优先考虑接口不兼容需要互相转接适配器内部系统太复杂想给外界一个干净入口外观不想改原类动态增强功能装饰器控制访问权限、延迟加载、远程转发代理处理树形结构菜单、目录、组织架构组合大量重复细粒度对象内存吃紧享元抽象和实现都要独立变化跨平台控件桥接这张表不是标准答案但是一个非常实用的起点。真正的项目里还要结合类图、调用关系、团队维护成本综合判断。5. 行为型模式对象们怎么“说话”和“分工”5.1 行为型是所有模式里最抽象、也最庞大的一族行为型模式共11个模板方法Template Method、策略Strategy、状态State、观察者Observer、中介者Mediator、命令Command、迭代器Iterator、访问者Visitor、备忘录Memento、解释器Interpreter、职责链Chain of Responsibility。前面已经说过创建型管“出生”结构型管“组合”行为型管的就是“对象之间怎么协作”谁调谁、谁通知谁、谁的算法可以替换、谁的责任边界在哪里。它最贴近业务规则也最依赖你对真实场景的理解。5.2 策略 vs 状态都在切换行为但发动机不一样这两个模式在UML类图上非常接近都是上下文类持有某个可变化的接口调用时委托给具体实现。但两者切换行为的方式和意图有很大区别。策略模式里客户端主动选策略。比如电商结算时的优惠计算打几折、满减、无门槛券由调用方根据营销规则决定传入哪个策略对象。上下文内部不关心策略为什么变策略之间是平行的谁也不认识谁。状态模式里状态对象内部自己决定下一个状态是谁。比如订单状态机订单在“待支付”状态收到支付成功事件后当前状态对象内部把上下文的状态切换成“已支付”。客户端对状态流转没有感知也不需要感知整个换挡过程封闭在状态类内部。我的区分口诀是策略是“甲方换方案”状态是“系统自己换挡”。一句话就能记住。5.3 观察者 vs 发布订阅两版“喇叭广播”观察者模式的经典结构是“主题观察者”。主题状态变化时主动遍历并通知所有已注册的观察者这是点对点的直接通知。发布订阅模型则是在观察者模式基础上加了一层“事件通道/调度中心”。发布者和订阅者不直接认识对方中间隔着一个总线。前端里常说的EventBus后端常用的消息队列本质都是发布订阅的体现。面试里被问“它俩到底什么区别”时我的回答通常就一句话观察者模式中主题和观察者彼此知道对方存在发布订阅模式中发布者和订阅者完全不知晓彼此中间隔着一条通道。解决了耦合问题代价是多了一个中间件的维护量。5.4 模板方法 vs 策略一个兜住骨架一个放任全盘模板方法通过继承复用代码骨架父类定义算法的步骤和顺序子类重写其中一步或几步。比如数据导出功能“读数据、转换格式、写文件”这三步整体流程是固定的但每一步具体实现可以因数据源不同而变。策略则更彻底整个算法都被抽出来客户端在运行时选用不同的策略对象算法整体可以换。区别在于你希望控制到什么程度。如果你希望“整体流程固定只允许变化某些中间步骤”模板方法更合适如果你希望“整个算法都能换着用”那就上策略。细品一下很多框架的扩展点设计其实都能看到这两者的影子。6. 第二把尺子类模式与对象模式继承和组合的分野6.1 为什么要单独提这个维度GoF的分类里除了按目的分三大类还有一种容易被忽略的维度按作用范围分类模式与对象模式。这个维度直接指向面向对象设计里最根本的两条扩展路径继承和组合。类模式通过继承来复用和扩展关系在编译期就确定了静态性很强。典型代表是模板方法和工厂方法。比如模板方法中父类定义算法骨架子类在编译期就决定如何重写某个步骤。对象模式通过组合来复用和扩展关系在运行期动态建立灵活性更高。绝大多数GoF模式都属于这一类。拿策略模式举例上下文对象持有一个策略接口引用运行期由客户端注入具体策略注入的是谁、什么时候换都是动态的。6.2 “组合优于继承”的真正含义“组合优于继承”是面向对象设计里流传最广的原则之一。背后逻辑很扎实继承带来脆弱的父子耦合父类改一个方法签名所有子类都跟着受影响而且单继承机制下一个类只能有一个父类表达力天然受限。但要注意原则说的是“优先考虑组合”不是“禁用继承”。模板方法、工厂方法就是继承的正面案例而且用得很好。关键在于判断场景继承适合“行为骨架稳定、只有局部变化”的情况组合适合“行为变化频繁、需要动态组装”的情况。判断逻辑到位了比只背口号管用太多。6.3 一张表看清GoF模式的类/对象属性分类类模式对象模式创建型工厂方法抽象工厂、建造者、原型、单例结构型适配器类适配器形式适配器对象适配器、桥接、组合、装饰器、外观、享元、代理行为型模板方法、解释器策略、状态、观察者、中介者、命令、迭代器、访问者、备忘录、职责链这张表不用背但建议留个印象。判断方式很简单如果模式主要靠继承把类与类连起来它就是类模式如果靠对象引用、接口组合来协作就是对象模式。多数模式都是后者这本身就说明了组合在现代设计里的主导地位。7. 拿到需求怎么反推模式从信号词到候选名单7.1 需求描述里的“信号词”对应表实际项目里不会有人跑到你工位上说“请用观察者模式实现一个功能”。需求是业务语言模式是技术语言中间的翻译能力才是真正的设计能力。我整理了一张信号词对照表能帮你快速定位业务描述里的信号候选模式多种做法可以互相替换运行时要自由切换策略对象的行为随内部状态变化而变化状态不修改原类的前提下给对象加功能装饰器外部接口不匹配但必须对接适配器对象状态变了要通知其他对象观察者/发布订阅创建对象过程复杂、分多步建造者对象全局只能有一个单例一种产品多个系列必须配套创建抽象工厂对象访问需要加权限、延迟或远程转发控制代理子系统太复杂想提供一个简单入口外观表格里的对应关系不是绝对的但足够作为起点。7.2 走查一个促销系统多模式组合的真实生态假设电商平台要做促销活动不同用户新用户、老用户、VIP看到的促销策略不同促销结果要实时通知多个下游系统推荐、报表、用户App其中VIP用户还能叠加多种优惠。拆一下这个系统至少涉及三类模式。优惠计算抽成多个策略类用策略模式按用户类型注入活动状态变化后用观察者模式把变更通知推给下游多个系统VIP用户的优惠叠加是层层包裹的天然适合装饰器模式。如果再配上工厂方法来创建策略对象那创建型、结构型、行为型一个不落全用上了。这个例子说明真实项目几乎不会单用一个模式往往是创建型负责生产对象结构型负责组装扩展行为型负责协作调度。这也是为什么我说先把分类学清楚比先背十个具体模式更重要——分类就是你的战术地图。7.3 警惕“为了模式而模式”最后泼一盆冷水。我见过不少代码为了套模式而套模式明明一个if-else十分钟能写完的逻辑非要抽象出三个类和两个接口美其名曰“高扩展性”。结果半年内需求根本没变过这些过度设计全部变成维护负担新同事接手时还得花时间理解这些抽象到底想干嘛。我的建议是先选用最简单的方式实现等代码出现“坏味道”再引入模式。哪些是坏味道开始到处重复、分支判断越堆越多、对象创建逻辑越来越乱、扩展一个功能要动旧代码好几个地方。模式是解决问题的工具不是KPI。该出手时出手不该出手时不要强行加戏。这篇先聊到这里。后面我计划沿着创建型、结构型、行为型三个专题逐个拆解那些最常用、也最容易被用错的模式包括每个模式的典型代码、重构过程、以及真实项目里的取舍。我自己学设计模式的经验是这套东西至少要过三遍才有感觉。第一遍看分类和例子混个脸熟第二遍在项目里踩了坑再回来看恍然大悟第三遍才真的懂一个模式的价值边界以及它和“隔壁模式”之间那条微妙的界线。希望这篇分类地图能帮你把第一遍走得比当年的我顺一点。
返回列表