跳转到内容

第三部分 · 结构型模式

一句话:结构型模式解决“对象怎么组合”的问题:接口对不上、功能要叠加、入口太复杂时,用一层薄薄的包装把它们接起来,而不是去改已有的类。

类和类写好之后,麻烦往往出在它们怎么连在一起。常见的有三种。

第一种,第三方 SDK 的接口和我们的接口对不上。假设你的订单代码一直调用 pay(金额, 订单号),现在要接入一家新的支付服务,它提供的是 charge(分, 币种, 商户号),金额单位是分,还多了两个参数。你要么在每个调用 pay 的地方都加一段转换,要么去改自己的接口。前者要改十几处,漏掉一处,那个角落就会把 50 元当成 50 分扣。

第二种,想给对象加功能,却只会用继承。一个 Notifier 要加日志,就写 LoggingNotifier;再要加重试,写 RetryNotifier;日志加重试同时要,又写 LoggingRetryNotifier。再加一个“限流”,要覆盖各种搭配就得写 7 个子类。每多一个功能,子类数量差不多翻一倍,而且功能的组合顺序是写死的。

第三种,一个操作要按顺序调七八个子系统。比如“下单”要依次调库存、优惠券、支付、物流、短信。如果每个调用处都自己抄一遍这套步骤,哪天物流多了一个必须先调用的接口,你得把所有抄过的地方全找出来改,漏一处,那条路径下的订单就发不出货。

这三种麻烦的共同点是:问题不出在某个类写得不好,而出在类和类之间的接口、层次、调用顺序。结构型模式(Structural Patterns)做的事,就是在中间加一层,让双方都不用改。

模式 一句话 流行度 先读哪个
适配器(Adapter) 把一个接口转换成调用方期望的接口 ★★★ 先读
装饰器(Decorator) 在不改原对象的前提下层层叠加功能 ★★★ 先读
外观(Facade) 给一组复杂的子系统提供一个简单入口 ★★★ 先读
代理(Proxy) 用一个替身控制对真实对象的访问 ★★★
组合(Composite) 把对象组织成树,单个和整体一样对待 ★★
桥接(Bridge) 把两个独立变化的维度拆开再组合 ★
享元(Flyweight) 共享大量重复的细粒度对象以省内存 ★

流行度的含义和第 1 章一样:★★★ 是几乎每个项目都会碰到的,★ 是知道它解决什么问题、遇到时能认出来即可。

结构型模式:问题与模式对照

左边是你会遇到的问题,右边是该看的模式和章号,一条线对应一个。

  • 你要接入的第三方接口和自己代码用的接口对不上,不想让转换代码散在每个调用处 → 看第 9 章 适配器。
  • 你想给一个对象加日志、缓存、重试,而且这些功能要能随意搭配,不想为每种搭配写一个子类 → 看第 10 章 装饰器。
  • 你的调用方每次都要依次调好几个子系统,希望只给它们一个简单入口 → 看第 11 章 外观。
  • 你想在不改调用方的前提下,加权限检查或延迟加载,控制别人怎么用某个对象 → 看第 12 章 代理。
  • 你手里的数据是文件夹套文件夹那样的树,想让调用方对单个文件和整个文件夹用同一种写法 → 看第 13 章 组合。

建议先读第 9 章适配器、第 10 章装饰器、第 11 章外观。这三个覆盖了日常开发里绝大多数用得上结构型模式的场景:接第三方服务、给已有功能叠加横切逻辑、给复杂模块包一个入口。它们彼此独立,先读哪个都行。

第 12 章代理建议在读完装饰器之后再读。代理和装饰器在代码上长得几乎一样,都是“一个对象包着另一个对象,对外接口相同”,区别在意图:装饰器是为了加功能,代理是为了控制访问。先把装饰器的写法看熟,再读代理,你会只需要关心“意图为什么不同”这一件事。

第 13 章组合在你遇到树形数据时读,比如菜单带子菜单、组织架构、文件目录。没有这类数据时,可以先放着。

第 14 章桥接和第 15 章享元可以跳过。桥接解决的是两个维度同时变化的问题,享元是为了在对象数量极大时省内存,日常业务代码里很少碰到。如果时间有限,读一下这两章开头的两行题记,知道它们解决什么问题就够了。