5.多用组合和少继承
目录介绍
- 1.先回答上篇思考题
- [1.1 上篇遗留三道题](#1.1 上篇遗留三道题 "#11-%E4%B8%8A%E7%AF%87%E9%81%97%E7%95%99%E4%B8%89%E9%81%93%E9%A2%98")
- [1.2 企鹅会飞的决策事故](#1.2 企鹅会飞的决策事故 "#12-%E4%BC%81%E9%B9%85%E4%BC%9A%E9%A3%9E%E7%9A%84%E5%86%B3%E7%AD%96%E4%BA%8B%E6%95%85")
- [1.3 维度正交的爆炸](#1.3 维度正交的爆炸 "#13-%E7%BB%B4%E5%BA%A6%E6%AD%A3%E4%BA%A4%E7%9A%84%E7%88%86%E7%82%B8")
- [1.4 灵魂五连问](#1.4 灵魂五连问 "#14-%E7%81%B5%E9%AD%82%E4%BA%94%E8%BF%9E%E9%97%AE")
- 2.从一个翻车说起
- [2.1 鸟类继承谜题](#2.1 鸟类继承谜题 "#21-%E9%B8%9F%E7%B1%BB%E7%BB%A7%E6%89%BF%E8%B0%9C%E9%A2%98")
- [2.2 层级越拆越乱](#2.2 层级越拆越乱 "#22-%E5%B1%82%E7%BA%A7%E8%B6%8A%E6%8B%86%E8%B6%8A%E4%B9%B1")
- [2.3 问题的根因](#2.3 问题的根因 "#23-%E9%97%AE%E9%A2%98%E7%9A%84%E6%A0%B9%E5%9B%A0")
- 3.继承的三宗罪
- [3.1 强耦合](#3.1 强耦合 "#31-%E5%BC%BA%E8%80%A6%E5%90%88")
- [3.2 层级膨胀](#3.2 层级膨胀 "#32-%E5%B1%82%E7%BA%A7%E8%86%A8%E8%83%80")
- [3.3 静态绑定](#3.3 静态绑定 "#33-%E9%9D%99%E6%80%81%E7%BB%91%E5%AE%9A")
- 4.组合解题
- [4.1 用接口拆能力](#4.1 用接口拆能力 "#41-%E7%94%A8%E6%8E%A5%E5%8F%A3%E6%8B%86%E8%83%BD%E5%8A%9B")
- [4.2 用组合复用代码](#4.2 用组合复用代码 "#42-%E7%94%A8%E7%BB%84%E5%90%88%E5%A4%8D%E7%94%A8%E4%BB%A3%E7%A0%81")
- [4.3 鸟类组合方案](#4.3 鸟类组合方案 "#43-%E9%B8%9F%E7%B1%BB%E7%BB%84%E5%90%88%E6%96%B9%E6%A1%88")
- 5.绘图程序对比
- [5.1 继承的爆炸](#5.1 继承的爆炸 "#51-%E7%BB%A7%E6%89%BF%E7%9A%84%E7%88%86%E7%82%B8")
- [5.2 组合的优雅](#5.2 组合的优雅 "#52-%E7%BB%84%E5%90%88%E7%9A%84%E4%BC%98%E9%9B%85")
- [5.3 类图对比](#5.3 类图对比 "#53-%E7%B1%BB%E5%9B%BE%E5%AF%B9%E6%AF%94")
- 6.如何选择
- [6.1 三大判据](#6.1 三大判据 "#61-%E4%B8%89%E5%A4%A7%E5%88%A4%E6%8D%AE")
- [6.2 继承适用场景](#6.2 继承适用场景 "#62-%E7%BB%A7%E6%89%BF%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF")
- [6.3 组合适用场景](#6.3 组合适用场景 "#63-%E7%BB%84%E5%90%88%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF")
- 7.设计模式映射
- [7.1 装饰者模式](#7.1 装饰者模式 "#71-%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F")
- [7.2 策略模式](#7.2 策略模式 "#72-%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F")
- [7.3 模板方法](#7.3 模板方法 "#73-%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95")
- 8.总结与延伸
- 9.综合实战案例
- [9.1 商品体系需求](#9.1 商品体系需求 "#91-%E5%95%86%E5%93%81%E4%BD%93%E7%B3%BB%E9%9C%80%E6%B1%82")
- [9.2 继承版类爆炸](#9.2 继承版类爆炸 "#92-%E7%BB%A7%E6%89%BF%E7%89%88%E7%B1%BB%E7%88%86%E7%82%B8")
- [9.3 能力组合重构](#9.3 能力组合重构 "#93-%E8%83%BD%E5%8A%9B%E7%BB%84%E5%90%88%E9%87%8D%E6%9E%84")
- [9.4 类图与动态能力](#9.4 类图与动态能力 "#94-%E7%B1%BB%E5%9B%BE%E4%B8%8E%E5%8A%A8%E6%80%81%E8%83%BD%E5%8A%9B")
- [9.5 留下三道思考题](#9.5 留下三道思考题 "#95-%E7%95%99%E4%B8%8B%E4%B8%89%E9%81%93%E6%80%9D%E8%80%83%E9%A2%98")
- 10.认知跃迁总结
| # | 章节 | 主题 | 关键案例 | 关键图表 |
|---|---|---|---|---|
| 01 | 面向对象设计思想 | 范式之别 | 双 11 雪崩订单系统 | 网状 vs 线性流程图 |
| 02 | 面向对象的特性 | 封装/抽象/继承/多态 | 钱包余额对不上排查 | vtable 内存布局图 |
| 03 | 接口vs抽象类比较 | 何时选哪个 | 日志器 47 次改动复盘 | 决策树、模板时序图 |
| 04 | 接口而非实现编程 | 依赖抽象不依赖具体 | 云迁移 6 万行连锁修改 | 三步重构演化图 |
| 05 | 多用组合和少继承 | 继承陷阱与组合优雅 | 企鹅会飞继承翻车 | 类爆炸 vs 正交组合 |
| 06 | 设计原则全景图 | SOLID + 23 模式 | 8000 行上帝类失控 | SOLID 关系图 |
| 07 | SOLID 原则案例汇 ⭐ | SOLID 在战场上 | 团队三年 SOLID 之争 | 滥用现场扫描图 |
| 08 | 反模式与坏味道 ⭐ | 30+ 嗅觉清单 | PR 被驳回 17 次 | 坏味道地图 |
| 09 | 重构十二式实战 | 12 个最常用手法 | 1200 行结算函数救援 | 重构节奏循环 |
| 10 | 可测试性设计 ⭐ | OOP 的硬验收 | 95% 覆盖率上线即崩 | 测试金字塔 |
| 11 | DDD 与战术建模 ⭐ | 从代码到业务 | 需求文档 70% 失真 | 限界上下文图 |
| 12 | 综合实战图片框架 ⭐ | 完整系统实现 | 图片处理系统实战 | 完整架构图 |
1.先回答上篇思考题
1.1 上篇遗留三道题
上一篇 04.接口而非实现编程 末尾留下了三道题:
- 🟢
Map<Carrier, CarrierTracker>vsList<CarrierTracker>本质区别? - 🟡
SfTracker应该extends SfApiClient还是 含有SfApiClient字段? - 🔴 三家快递 80% 共同逻辑如何复用?
本篇要回答的是后两道题:
| 题 | 本篇答案 |
|---|---|
| 🟢 | Map = 根据快递公司路由 、List = 多实现同时生效。这是「路由 vs 广播」两种多态语义 |
| 🟡 本篇答 | 含有字段(组合)。理由看§02 「继承的三宗罪」 |
| 🔴 本篇答 | 提炼能力、用组合 ,三家都拼装 [认证, 限流, 重试, HttpCaller],而不是控制一个 RestApiBase 抽象类 |
这三道题几乎在问同一个问题:当你看到两个类"很像"时,是该走继承还是该走组合?
1.2 企鹅会飞的决策事故
某动物园业务系统中发生了一起「企鹅起飞」的荐题。
java
public abstract class AbstractBird {
public void fly() { /* 默认会飞 */ }
public void layEgg() { ... }
public void chirp() { ... }
}
public class Penguin extends AbstractBird { /* 使用默认 fly */ }
代码主人以为「企鹅不会飞」是常识,谁都不会调。直到一名实习生写了:
java
zoo.allBirds().forEach(Bird::fly); // 果然,企鹅也"飞"起来了
后果是企鹅馆的展示屏出现了"企鹅正在飞行"的字样。这不是笑话,是在 GitHub 能搜到的真实事故调例。
这里骨子里不是「Penguin 应该重写 fly() 让它 throw 异常」,那只是「补丁」。
骨子里的错误是「把 fly() 放进了所有鸟都能看到的地方」,也就是继承体系的"公共套餐"设计。不适合的能力被「继承」这个机制强加到了子类身上。
企鹅会飞是"继承给了不该有的能力",单一能力错配。但工程中更常见的噩梦是反面:一个对象天然拥有多种能力(会飞、会走、会游),继承树却无法在单一层级里同时容纳它们。单一能力错配是尴尬,多重能力正交是爆炸。下面看一个更极端的案例。
1.3 维度正交的爆炸
企鹅的问题是「继承给了不该有的能力」。但继续沿着鸟类往下想,问题会从"尴尬"升级为"爆炸"。
现实中的鸟的能力矩阵,每只鸟拥有的能力都不同:
企鹅: 会游泳、会叫、(不会飞)
麻雀:会飞、 会叫、(不会游)
鸭子:会飞、 会游泳、会叫
鸵鸟: 会叫、(不会飞、不会游)
如果坚持用继承树表达这三种能力的所有合法组合:
erlang
AbstractBird
├── FlyableBird
│ ├── Sparrow (飞+叫)
│ └── ...
├── SwimmableBird
│ ├── Penguin (游+叫)
│ └── ...
├── TweetingBird (只叫)
├── FlySwimBird
│ └── Duck (飞+游+叫)
├── FlyTweetBird (飞+叫、不游)
├── SwimTweetBird (游+叫、不飞)
└── FlySwimTweetBird (飞+游+叫)
3 种能力,产生 2³ = 7 种可能的中间抽象类。再加「会潜水」「会上树」?每多一种能力维度,中间类的数量翻一倍。
这不是"设计得不够好",是用 is-a(继承)去建模正交能力组合,数学上注定爆炸。
实际重构中,开发者经历了五次挣扎:
重构 1:为 Duck 抽 FlySwimBird → 出现了 SwimTweetBird(企鹅场景),继续抽
重构 2:当 Sparrow 需要时抽 FlyTweetBird → 中间类开始膨胀
重构 3:表面能跑,但 PM 说"加一种'会潜水'的鸟" → 继承树重来
重构 4:改为 interface CanFly / CanSwim / CanTweet → 发现 3 种能力的实现代码里,普遍存在重复逻辑(如所有飞行实现都要做「起飞前高度校验」)
重构 5:最终落定为 「能力接口 + 能力对象组合」 → class Duck { flyer; swimmer; tweeter; }
五个版本走完,结论只有一句:单一能力错配用继承可以打补丁,正交能力组合用继承是死路。
1.4 灵魂五连问
arduino
Q1 ── 为什么继承、这么学院派的工具,在工程里却如此危险?
└─→ §02 三宗罪
Q2 ── 组合不也是依赖吗?为什么耦合更低?
└─→ §03.1 / §03.2 能力维度划分
Q3 ── 什么时候继承反而是最佳选择?
└─→ §05.2 继承适用场景
Q4 ── 设计模式中哪些是"伪装成继承的组合"?
└─→ §06 三个模式反面例
Q5 ── 「能力组合」与「领域驱动」有什么关系?
└─→ §08 末尾伏笔,对接第 11 篇 DDD
2.从一个翻车说起
2.1 鸟类继承谜题
定义抽象类 AbstractBird,所有鸟都继承它,并提供 fly() 方法:
java
public abstract class AbstractBird {
public void fly() { /* 默认会飞 */ }
}
public class Sparrow extends AbstractBird { /* 麻雀,会飞 */ }
public class Ostrich extends AbstractBird { // 鸵鸟,不会飞
@Override public void fly() {
throw new UnsupportedOperationException("鸵鸟不会飞");
}
}
第一处尴尬出现:鸵鸟仍然继承了 fly() 方法,只能靠抛异常"撇清" 。这违反里氏替换原则,客户端拿到一个 AbstractBird 调用 fly(),结果 RuntimeException。
2.2 层级越拆越乱
为了精确建模,再加一层:
markdown
AbstractBird
├── AbstractFlyableBird(会飞)
│ ├── Sparrow
│ └── Crow
└── AbstractUnflyableBird(不会飞)
├── Ostrich
└── Penguin
需求继续叠加:"是否会叫?是否会游泳?",继续按维度切,类的数量呈笛卡尔积爆炸:
ini
能力组合 = 飞? × 叫? × 游? = 2 × 2 × 2 = 8 个抽象类
再加哺乳/卵生 = 16 个
再加......
2.3 问题的根因
根本错误 :用 is-a 关系(继承)去建模 has-a 关系(能力)。鸵鸟"是一种鸟"成立,但"是否会飞"是能力维度,不该用类型层级表达。
3.继承的三宗罪
3.1 强耦合
子类与父类是编译期绑定:
java
// 父类改一行:
public abstract class AbstractBird {
public void fly() {
System.out.println("拍翅膀"); // 后来改为日志埋点
flap();
}
}
// 所有子类的 fly() 行为都被影响,但子类作者可能毫不知情
子类透视到了父类的内部实现,这正好破坏了封装。
3.2 层级膨胀
每多一个正交维度 ,层级就要乘上一倍。这不是"复用",是自虐。
3.3 静态绑定
继承结构在编译期就钉死,运行时不可改:
java
// 想让一只鸵鸟暂时"会飞"(魔法状态)→ 不可能
ostrich.fly(); // 永远抛异常,除非改类层级重新编译
4.组合解题
4.1 用接口拆能力
把"能力"用接口表达:
java
public interface Flyable { void fly(); }
public interface Tweetable { void tweet(); }
public interface Swimmable { void swim(); }
每只鸟按需实现:
java
public class Sparrow implements Flyable, Tweetable { /* ... */ }
public class Ostrich implements Tweetable, Swimmable { /* 不实现 Flyable */ }
public class Penguin implements Swimmable { /* ... */ }
接口让能力维度正交,加一种能力只需加一个接口,不动任何已有类。
4.2 用组合复用代码
接口只声明,没复用代码。把行为实现抽成"能力对象",再组合 + 委托:
java
public class FlyAbility {
public void fly() { /* 拍翅膀飞行的通用逻辑 */ }
}
public class TweetAbility {
public void tweet() { /* 鸣叫逻辑 */ }
}
public class Sparrow implements Flyable, Tweetable {
private final FlyAbility fly = new FlyAbility(); // 组合
private final TweetAbility tweet = new TweetAbility(); // 组合
@Override public void fly() { fly.fly(); } // 委托
@Override public void tweet() { tweet.tweet(); }
}
4.3 鸟类组合方案
继承的三个职能,类型表达、多态、代码复用,被分别交给:
| 职能 | 替代方案 |
|---|---|
| 类型表达(is-a) | 接口(has-a / can-do) |
| 多态 | 接口多态 |
| 代码复用 | 组合 + 委托 |
5.绘图程序对比
上面用鸟类展示了「能力维度正交」的解法,用接口 + 组合取代继承树。下面换个场景再看一遍:当两个维度完全正交且同等重要时,继承的爆炸会更快。绘图系统里「形状」和「颜色」就是这样的正交维度。
5.1 继承的爆炸
绘图系统:形状 × 颜色 → 类爆炸:
java
class RedCircle extends Circle { /* ... */ }
class BlueCircle extends Circle { /* ... */ }
class RedRectangle extends Rectangle { /* ... */ }
class BlueRectangle extends Rectangle { /* ... */ }
// 加一种颜色 → 形状数 × 1 个新类
// 加一种形状 → 颜色数 × 1 个新类
5.2 组合的优雅
把"颜色"作为正交维度,组合进形状:
java
interface Color { void apply(); }
class Red implements Color { public void apply() { /* 红 */ } }
class Blue implements Color { public void apply() { /* 蓝 */ } }
abstract class Shape {
protected final Color color; // 组合
Shape(Color color) { this.color = color; }
abstract void draw();
}
class Circle extends Shape {
Circle(Color c) { super(c); }
void draw() { color.apply(); /* 画圆 */ }
}
class Rectangle extends Shape {
Rectangle(Color c) { super(c); }
void draw() { color.apply(); /* 画矩形 */ }
}
// 使用
Shape s = new Circle(new Red());
类的数量从 N×M 降为 N+M。这正是桥接模式的精髓。
5.3 类图对比
6.如何选择
6.1 三大判据
继承的安全姿势:层级 ≤ 2 层,关系稳定,父类自己写。三者缺一就改组合。
6.2 继承适用场景
| 场景 | 例子 |
|---|---|
| 模板方法骨架 | Wallet 父类提供 consume 流程,CreditWallet 覆写 isAllowed |
| 框架钩子 | 抽象类定义生命周期,子类填空(如之前文章中的 AbstractPaymentGateway.doPay) |
| 浅层稳定层级 | 一条继承链 ≤ 2 层,且未来不会出现正交维度 |
6.3 组合适用场景
| 场景 | 例子 |
|---|---|
| 能力维度正交 | 鸟(飞/叫/游)、商品(可发货/预售/限购/组合) |
| 行为可运行时切换 | Order 持有 DiscountPolicy,随时换策略 |
| 可叠加多个能力 | 装饰者模式,给商品动态叠加限购+预售 |
| 跨业务领域复用 | RateLimiter 被支付网关和商品查询各自持有 |
7.设计模式映射
组合优于继承,并不是抽象口号,23 种设计模式中绝大多数都基于组合:
7.1 装饰者模式
动态叠加能力,以商品系统为例,同一个 SKU 在不同阶段可能需要动态附加"限购"、"预售"标签:
java
Product p = new BaseProduct("SKU-101", price);
p = new PurchaseLimitedDecorator(p, maxPerUser=2); // 叠加限购
p = new PreSaleDecorator(p, deliveryDate=...); // 再叠加预售
// p 仍然是一个 Product,调用方完全不知它被装饰了几层
每层都"组合"前一层,行为可在运行时拼装。靠继承做不到。
7.2 策略模式
java
public class Order {
private DiscountPolicy discount; // 组合策略
public void setDiscount(DiscountPolicy p) { this.discount = p; } // 运行时切换
}
策略对象作为字段被持有,热替换毫无压力。
7.3 模板方法
模板方法是继承的合法用法,父类定义骨架,子类填空:
java
abstract class HttpClient {
public final Response send(Request req) {
beforeSend(req);
Response r = doSend(req); // 子类填
afterSend(r);
return r;
}
protected abstract Response doSend(Request req);
}
这种"骨架 + 钩子"的固定流程,正是继承的擅长场景。你在 03 篇AbstractPaymentGateway.doPay 里见过完全相同的结构 ,支付网关的公共骨架用抽象类,差异化的 doPay 留给子类填空。
8.总结与延伸
| 维度 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a(编译期) | has-a(运行期) |
| 耦合度 | 强(父类暴露给子类) | 弱(仅依赖接口) |
| 灵活性 | 静态、不可换 | 动态、可热插拔 |
| 复用粒度 | 整块复用 | 细粒度组合 |
| 复杂度 | 易爆炸 | 类多但关系扁平 |
"多用组合少用继承" 不是"杜绝继承",而是把继承用在对的场景,浅层、稳定、可控。其他时候,组合永远是更安全的默认选项。
到此为止,前 5 篇构成了一条主线:
有了对象、特性、契约、依赖与组合,就具备了写出"高内聚低耦合"代码的全部砖头。但砖头怎么砌成稳健的房子?这需要更高层的指导原则,SOLID 五大原则与 23 种设计模式。
下一篇 06.设计原则全景图 将以此前案例为线索,串联 SOLID 与设计模式,把面向对象设计推向工程落地。
9.综合实战案例
主线接力,04 篇货运推送用接口解了「调用方锁死」,本篇要解「能力拼装」:商品类型体系。
9.1 商品体系需求
电商上架需求:
markdown
1. 普通实物商品:需要发货
2. 虚拟商品(课程/会员卡):不发货,不运费
3. 亲邮商品:走包裹个量限制
4. 预售商品:需推迟发货
5. 组合商品:含多个子商品
6. 后续会不断增加「会员专享」「限购」「需预约」等能力...
9.2 继承版类爆炸
工程师以为「这不是类的经典选择题吗」,当场画了一棵类继承树:
markdown
Product
├─ PhysicalProduct
│ ├─ NormalPhysical
│ ├─ PreSalePhysical
│ ├─ OverseasPhysical
│ └─ BundlePhysical
└─ VirtualProduct
├─ Course
└─ Membership
第一次迫重构出现在 PM 说「虚拟会员卡也能预售」:
arduino
PreSaleVirtualProduct?
OverseasVirtualProduct? // 如果也要呢?
BundleVirtualProduct? // 组合 × 虚拟...
在添加第 4 个能力「限购 」后,以简单乘法计算:4 个能力×2 个大类 = 16 个叶子类型。「加一项能力」的代价是「叶子类型翻倍」。
这是「能力中多选」场景用继承的必然下场:正交能力的乘积爆炸。
9.3 能力组合重构
重构的核心是三个判断:
- 这些变化点是"is-a"还是"can"?,都是 "can"(可发货/可限购/可预售)
- 有多少维度?,有3+个独立变化维度
- 是否运行期可变?,同一 SKU 在不同阶段可能被动态加上「限购」
三个都是 "是",「能力组合」是唯一答案。
java
// 能力接口(只表达「能干什么」)
public interface Shippable { ShippingPlan plan(); }
public interface PreSalable { LocalDateTime deliveryDate(); }
public interface PurchaseLimited{ int maxPerUser(); }
public interface Bundling { List<Product> children(); }
// 商品仅是"能力代理人"
public class Product {
private final String id;
private final Money price;
private final List<ProductCapability> caps; // 组合
public boolean is(Class<? extends ProductCapability> c) {
return caps.stream().anyMatch(c::isInstance);
}
public <T extends ProductCapability> T as(Class<T> c) {
return caps.stream().filter(c::isInstance).map(c::cast).findFirst().orElseThrow();
}
}
// 调用例
if (product.is(Shippable.class)) {
ShippingPlan p = product.as(Shippable.class).plan();
...
}
深刻变化:
| 场景 | 继承版 | 组合版 |
|---|---|---|
| 加一个能力 | 叶子类×2 | 加1个接口 |
| 动态上架「限购」 | 需重新创建实例 | caps.add(new PurchaseLimited(...)) |
| 测试 | 继承树越深 mock 越难 | 能力独立 mock |
| 业务表达力 | OverseasPreSaleBundlePhysical |
[跨境, 预售, 组合, 可发货] |
9.4 类图与动态能力
注意关系使用了菱形空心o-- (组合/聚合)而非三角<|--(继承)。这不是 UML 语义的细节,是设计哲学的本质差别。
9.5 留下三道思考题
答案在第 06 篇开头揭晓。
🟢 易 :上面的商品设计里,Product.caps 我写了类型为 List<ProductCapability>。为什么不写成 Set 或 Map<Class, Capability>?答案其实是"可以都写,但三者的语义不同",你能说出区别吗?
🟡 中 :「限购」能力可以被运行期动态加上 。但这样一来,Product 变得可变。怎么扣住「不变量」又保证「运行期可加能力」?写下你的技术方案。
🔴 难 :现在设计中「Product.is(Shippable.class)」这种调用依然是「类型检查」。你认为这是设计问题吗?如果是,能不能不需要类型检查也能表达『可发货』这件事? 提示:这道题是「设计原则」与「多态」的交叉,是下一篇《设计原则全景图》的入口问题。
10.认知跃迁总结
回到开篇企鹅会飞、鸟类能力正交爆炸、商品体系类爆炸三件事。它们表面不同,本质都是「谁都不能预言『未来会出现哪些能力组合』」。继承需要设计者提前看准层级表,而组合允许心安理得说「以后再说」。
一句话:继承是面向过去的联姻,组合是面向未来的联姻。代码的生命周期里"未来不可预言"是常态,于是在多数场合里"多用组合」胜在了"少用继承"。
到这里,你已经拥有了面向对象的全部「砖块」:对象、特性、契约、依赖、组合。但砖块怎么砥成稳健的房子?这需要一份带骨架的"施工手册",下一篇 06.设计原则全景图 会从「吃 8000 行上帝类」的事故进入 SOLID 与设计模式的全景。