第10章 · 趣味学习

设计模式:前人踩坑总结的武功秘籍

微服务架构落地后,小明发现团队写的代码五花八门——同样是创建对象,有人new、有人反射、有人工厂;同样是扩展功能,有人继承、有人包装、有人硬塞。代码越写越乱,改一个功能牵连一堆类。

大牛哥递给他一本《设计模式:可复用面向对象软件的基础》:"这是四人组(Gang of Four, GoF)踩了无数坑总结的武功秘籍,23招式,专治各种代码乱象。不过——学的是思想,不是死记硬套。"

📖 10.1 设计模式概述

设计模式概念源自建筑师Christopher Alexander——"模式=描述一个不断发生的问题和该问题的解决方案"。GoF四人组将其引入软件领域,定义:对在特定场景下解决一般设计问题的类和互相通信的对象的描述。通俗说=某一类问题的通用解决方案。

设计模式四要素

  • 📛 模式名称:名不正则言不顺
  • 问题:该模式意图解决的问题,超出就不该用
  • 💡 解决方案:解决问题的方法描述
  • 📊 效果:更具体的效果描述

学习设计模式两个注意

  1. 学思想而非死记——设计模式蕴含软件设计思想,和架构风格一致(MVC既是设计模式也是架构风格)
  2. 不能滥用——不相宜场合乱用有害无益,过度使用让架构混乱难维护。设计模式主要解决对象间通信和依赖的结构关系

23个GoF模式速览(按三大分类):

创建型(解决对象创建问题):

  • 🏭 Factory Method——延迟创建,子类决定创建哪个类
  • 🏗️ Abstract Factory——一致接口创建一系列对象
  • 🧱 Builder——逐步构造复杂对象,创建与表示分离
  • 🖨️ Prototype——深复制原型创建新对象
  • 💎 Singleton——保证一个类仅有一个实例

结构型(解决类与对象的组织结构):

  • 🔌 Adapter——接口转换,解决不相容
  • 🌉 Bridge——抽象与实现分离,独立变化
  • 🌳 Composite——树形组合,单个与组合一致
  • 🎨 Decorator——动态为方法增加功能
  • 🚪 Facade——一组类的一致访问接口
  • 🪶 Flyweight——共享大量细粒度对象
  • 🕶️ Proxy——访问代理,控制访问

行为型(解决类与对象如何通信):

  • 📜 Interpreter——解释器解释句子
  • 📋 Template Method——操作模板,步骤在子类实现
  • ⛓️ Chain of Responsibility——对象链传递请求
  • 📨 Command——请求封装为对象
  • 🔢 Iterator——顺序访问集合元素
  • 🤝 Mediator——中介对象封装通信,降低耦合
  • 📸 Memento——捕获对象状态,不破坏封装
  • 👁️ Observer——状态变化广播给观察者
  • 🔄 State——状态改变时改变行为
  • 🎯 Strategy——算法变化独立于客户
  • 🚶 Visitor——不改变元素类定义新操作

J2EE设计模式(GoF之后):Intercepting Filter(截取请求预处理)、Session Facade(Session Bean封装业务逻辑接口)等。

设计模式与软件架构的关系:架构更倾向整体全局描述软件组成(4+1视图等),设计模式更侧重类与类、对象与对象的关系。两者蕴含相同思想——消息总线架构风格与Observer模式"神似"。

💡 秒懂技巧:设计模式分类口诀——创建型管"怎么造",结构型管"怎么搭",行为型管"怎么聊"。面向对象设计三问:如何管理系统中的对象(创建型)?如何组织类与对象(结构型)?类与对象如何通信(行为型)?——三类模式分别回答这三个问题。

🏗️ 10.2.1 Abstract Factory:抽象工厂

意图解决的问题new ClassName()造成类名硬编码,无法根据运行环境动态加载相同接口但实现不同的类实例。为不同运行环境实现抽象类子类后,普通创建方式=代码与运行环境强绑定。

模式描述:AbstractFactory接受Client"订单"→用不同"车间"(ConcreteFactory)→根据"产品模型"(AbstractProduct)→生产特定"产品"(Product)。车间与产品一一对应。CreateProduct()方法与AbstractProduct一一对应。

效果:可配置的、动态的对象创建。提高移植性,尤其是多平台/多功能配置版本时。

相关讨论:核心思想=① 可配置的对象创建方法提高移植性和复用性 ② 充分利用多态特性避免硬编码。Java中可用面向接口方式简化——ProductFactory提供getProduct(productName),所有产品实现Product接口。增加新实现Product接口的产品不需要修改任何代码

🎮 实战场景:饿了么么需要支持多种支付渠道(支付宝/微信/银行卡)。用抽象工厂——PaymentFactory接口有createPayment()方法,AlipayFactory和WechatFactory分别创建对应实现。切换支付渠道时只需改配置,不改代码。

💎 10.2.2 Singleton:单件模式

意图解决的问题:一些服务类有且仅有一个实例——短消息服务、打印机服务、系统配置环境控制。全局变量只能保证编码时用了唯一实例,系统扩张后无法保证。

模式描述:构造函数设为protected/private防止外部直接初始化→通过getInstance()获得唯一实例→只创建一次uniqueInstance。两种初始化策略:Lazy Initialization(延迟初始化,第一次调用时创建)和Early Initialization(提前初始化,类加载时创建)。

注意三点

  • 仅保证单一系统内单一实例。分布式构件(多JVM下的EJB)上面的方法不能保证全系统唯一
  • 避免滥用——很多过度设计的Singleton同static工具类一样没意义,反而降低效率
  • 不支持继承——C++可用Template定义Singleton模板,Java/C#需逐一实现

🎨 10.2.3 Decorator:装饰模式(油漆工)

意图解决的问题:类功能不够强需要扩展?最简单=继承出新类。但会产生大量子类,类层次结构复杂混乱。

模式描述:DecoratorComponent和ConcreteComponent继承同一个抽象类Component,拥有相同接口。ConcreteDecorator1和ConcreteDecorator2是具体装饰类。运行期根据客户选择具有不同行为特征——动态扩展

效果:动态扩展功能,避免大量子类,比继承更灵活。随时根据需求产生新装饰类,避免高层类复杂性。但大量使用会造成很多接口类似的小装饰对象,可维护性下降。

相关讨论:JDK的I/O API大量使用Decorator——new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")))就是层层装饰。开源项目displaytag也以Decorator方式扩展。

Decorator像"给手机贴膜加壳"——手机(Component)不变,今天贴防蓝光膜(Decorator1),明天加充电壳(Decorator2),后天贴防窥膜(Decorator3)。随时可加可拆,不需要为每种组合造一个新手机型号。但如果每个装饰只在一处用、处理也很简单,就没必要用Decorator了。

🚪 10.2.4 Facade/Session Facade:门面模式

意图解决的问题:客户程序用到多个子系统,需要了解每个子系统接口,结构混乱,耦合度大,扩展维护困难。

模式描述:在原有系统前增加一层,封装子系统接口,对外提供一致访问接口。封装的是子系统的内部结构

Session Facade(J2EE扩展):EJB是分布式构件,访问=远程调用。即使Servlet和EJB在同一台机器,也要通过127.0.0.1访问,效率低。用Session Bean封装业务逻辑→减少远程调用次数→降低网络负载→提高性能。每次访问Session Bean=完成一次业务操作。

🎮 实战场景:饿了么么下单流程涉及用户服务+商户服务+库存服务+支付服务+配送服务。不用Facade的话客户端要分别调用5个服务、了解5套接口。用Facade——OrderFacade提供placeOrder()一个方法,内部协调5个子系统。客户端只知一个接口。

🤝 10.2.5 Mediator:中介者模式

意图解决的问题:复杂系统中对象相互通信,相互依赖。改一个影响多个,耦合关系造成可维护性降低。

模式描述:一组对象(Colleague子类)通过中介类(ConcreteMediator)通信。ConcreteMediator继承抽象Mediator,实现通信接口,封装Colleague之间的通信。

类比:Mediator像人的大脑——眼睛发送"前面有坑"消息→大脑接收→向脚发送"停止前进"消息。走路时和开车时,同样"前面有状况"的消息,大脑的响应不同(脚停/踩刹车)。

效果:降低Colleague间耦合,单个Colleague变化不影响其他对象。但Colleague多且关系复杂时,ConcreteMediator会变得非常复杂。仅在较小范围内使用,否则中介类难维护会抵消优点。

典型应用:GUI——窗体中多个需要通信的对象(部门下拉列表变化→人员下拉列表跟着变)。用Mediator封装通信。

👁️ 10.2.6 Observer:观察者模式

意图解决的问题:对象状态变化是独立的(A变化不会自动反映到B),但很多对象相互关联——如股票变化需同时反映到实时行情、技术分析图表。硬编码关联=紧密耦合。

模式描述:Subject(被观察者)保存观察者列表observers,通过attach/detach动态添加/移除观察者。状态变化时notify()通知所有观察者。ConcreteObserver继承自Observer抽象类。Subject了解有谁在观察但不了解观察者要做什么——抽象耦合

效果:动态增减观察者简单。可实现消息广播。但广播副作用=增加系统负载,任何消息都被自动发送到每个观察者,未必都必要。

使用注意

  • setState()后自动notify()会导致连续多次状态变更触发多次通知→可能造成并发问题。可改为客户程序执行一系列变更后手动调用notify(),但不能保证一定被通知
  • 避免广播风暴——update()中绝不能更新自己观察的Subject的状态,否则消息无穷循环

Smalltalk的MVC架构就用了Observer——Model变化传递到View。Observer模式实现一对多依赖的抽象耦合,广泛应用。

Observer像微信公众号订阅——公众号(Subject)不认识每个订阅者(Observer),只知道有人订阅了。发文时所有订阅者收到推送(notify)。取消订阅(detach)就不再收到。但如果你关注了100个公众号,每篇推送都收到——信息过载=广播的副作用。不能在收到推送后又去发自己的推送,否则你推我推→消息死循环

🔍 10.2.7 Intercepting Filter:筛选器模式

意图解决的问题:MVC Web应用中,客户请求到达控制器前需预处理(身份验证、请求来源验证、解码等)。放控制器里=控制器复杂且难维护。

模式描述:FilterManager调度FilterChain→拦截请求→FilterChain中过滤器依次预处理→最后一个过滤器完成后→FilterManager转发给实际目标(Target)。

效果:预处理逻辑和真正处理逻辑分离。Target只关心具体逻辑,预处理放FilterManager。解除两类处理耦合,扩展修改预处理容易。

相关讨论:与Decorator类似——都在处理对象前增加一层扩展操作,但实现差别大。可让Filter更通用可配置——不同请求用不同FilterChain,可用工厂模式动态创建Filter对象。这就是模式组合使用的例子。

🎮 实战场景:饿了么么API网关——每个请求进来先过筛选器链:① 身份验证Filter(检查token) → ② 限流Filter(防止刷接口) → ③ 日志Filter(记录请求) → ④ CORS Filter(跨域处理) → 到达真正的业务控制器。要加新预处理?加个Filter就行,不用改业务代码。

🎯 10.3 设计模式总结

本章详细讨论了7种模式:Abstract Factory、Singleton、Decorator、Facade/Session Facade、Mediator、Observer、Intercepting Filter。但设计模式远不止这些——23种GoF模式外,很多学者还在不断总结。

学习设计模式最重要的是理解,而非生搬硬套。每种模式都包含良好的设计思想——隐藏内部细节、降低耦合度等。观察人类社会也能发现类似模式:人是封装良好的对象(感官+行动=接口);望远镜/电话扩展能力(Decorator);中介所提供交流平台(Mediator);新闻机构让你了解最新状态(Observer);经纪人是Proxy;组织结构是Composite……

使用中经常把几种模式综合——如Intercepting Filter+工厂模式。架构设计师要综合利用设计模式消除系统混乱与耦合。

⚠️ 不能滥用设计模式!尤其简单系统中:对象都用工厂模式创建、工具类都设计成Singleton、两个对象间通信还硬加Mediator——这些做法只毫无价值地提高复杂度,反而不利理解与维护。系统架构设计师可以在重构时根据需要逐步应用设计模式改良系统。模式会引发你的思考——它不是硬性解决方法,而是一种思路。
老王
小明,23个模式我都背下来了,是不是就成架构师了?
小明
Kent Beck说"1+1>2",模式的价值在于融会贯通。背23个模式不等于会用——就像背了菜谱不等于会做饭。Martin Fowler说:"模式和业务构件的区别就在于模式会引发你的思考"。真正的架构师知道什么时候用、什么时候不用
🎬 故事结局:小明把设计模式用到了关键场景——支付渠道用Abstract Factory切换、订单服务用Facade统一接口、推荐引擎用Observer做数据变化通知、API网关用Intercepting Filter做预处理。但他也忍住了不滥用——简单的CRUD接口没硬套工厂模式,两个类的通信没加Mediator。

老王最后问:"架构设计师最重要的能力是什么?"

小明笑了笑:"不是知道所有的模式和方法,而是知道在什么场景下选择什么、放弃什么。从软件生命周期到开发模型,从需求分析到架构设计,从质量属性到设计模式——架构师的工作始终围绕三个字:选、妥协、平衡。"

十章学完,从"手工作坊"到"现代工厂",从瀑布到敏捷,从模块设计到架构评估,从质量属性到设计模式。饿了么么从3人小作坊成长为微服务架构的现代系统。架构师之路永无止境——但每一步都算数。📚