6.设计原则的全景图

23 阅读14分钟

6.设计原则全景图

目录介绍

#章节主题关键案例关键图表
01面向对象设计思想范式之别双 11 雪崩订单系统网状 vs 线性流程图
02面向对象的特性封装/抽象/继承/多态钱包余额对不上排查vtable 内存布局图
03接口vs抽象类比较何时选哪个日志器 47 次改动复盘决策树、模板时序图
04接口而非实现编程依赖抽象不依赖具体云迁移 6 万行连锁修改三步重构演化图
05多用组合和少继承继承陷阱与组合优雅企鹅会飞继承翻车类爆炸 vs 正交组合
06设计原则全景图SOLID + 23 模式8000 行上帝类失控SOLID 关系图
07SOLID 原则案例汇SOLID 在战场上团队三年 SOLID 之争滥用现场扫描图
08反模式与坏味道30+ 嗅觉清单PR 被驳回 17 次坏味道地图
09重构十二式实战12 个最常用手法1200 行结算函数救援重构节奏循环
10可测试性设计OOP 的硬验收95% 覆盖率上线即崩测试金字塔
11DDD 与战术建模从代码到业务需求文档 70% 失真限界上下文图
12综合实战图片框架完整系统实现图片处理系统实战完整架构图

1.先回答上篇思考题

1.1 上篇遗留三道题

上一篇 05.多用组合和少继承 末尾留下了三道题:

  • 🟢 List<Capability> vs Map<Class, Capability> vs Set 差别?
  • 🟡 运行期动态加能力与不变量怎么平衡?
  • 🔴 product.is(Shippable.class) 这种类型检查是不是设计问题?
本篇答案
🟢List=有序可重复(适合中间件链)、Set=不允许重复能力、Map<Class,Capability>=以类型为主键快查(适合本例)。都能跑,但语义完全不同
🟡「不变量」不是「字段不变」,是「业务规则始终成立」。动态加能力 → 产生新的不可变 Product 快照,原实例不动
🔴是设计问题!is(...) 是「多态的退化」。本篇§04 LSP 会告诉你:避免 instanceof 是 LSP 的隱性要求

三道题同时指向本篇的主题:如何从「可以这样写」走到「该这样写」,这正是 SOLID 的职责。

1.2 8000行上帝类现场

某中台企业系统 OrderManager.java

2019-08  init: OrderManager 247 
2020-04  +推送逻辑、+退款逻辑  1132 
2020-12  +发票、+争议、+邀请返佣  2891 
2021-09  +跨境、+多货币  4760 
2022-08  +会员体系、+积分  6204 
2023-11  +带货主播抽佣、+社区团购  8194   谁也不敢动

它看上去违反了「单一职责」,但你能一眼看出它到底违反了 SOLID 里哪几条吗?详细联调后,答案是【同时违反了五条】:

flowchart LR
    OM[OrderManager 8194行] --问题 1--> SRP[职责多达 47 个]
    OM --问题 2--> OCP[加业务总要改 OrderManager]
    OM --问题 3--> LSP[跨境、社团「伪子类」不能代替使用]
    OM --问题 4--> ISP[调用方静静依赖了几十个用不到的方法]
    OM --问题 5--> DIP[高层业务依赖底层 ORM/HTTP 实现]

这种「五条原则同时违反」的上帝类,为什么会出现? 不是工程师不努力,是在「代码越东辣、越不敢动」的状况下,选择「加个 if-else 还能发版」是本能。这种本能选择之所以能出现,就是因为背后没有 SOLID 这件「骨架」在约束肣体生长

本篇的意义:让你从今天开始,身上背五条反躺肣背心。

1.3 五次重构全头处

面对 8000 行上帝类,某团队连动 5 次重构,每次都仅能提起1条原则。这反过来成为了本篇的「介绍顺序」:

重构 1:拆 47 个职责为独立服务 → SRP生效,代码量从 8000 降到 5500

重构 2:抽象出「业务能力」插拔点 → OCP生效,加 1 个业务只加 1 个插件

重构 3:为「跨境」「社团」等「伪子类」重设并保证可替换 → LSP生效

重构 4:给调用方以「只要能力」的接口,别塮事以万能接口 → ISP 生效

重构 5:反转高低层依赖、引入 IoC 容器 → DIP 生效

最后这五条原则不是「五个独立眼镜」,是「五股同时拉着的肣体」。什么叫「同时拉着」?就是:SRP 不是为了拆小是为了代码能遵守 OCP,潜变后 ISP 才能遵守,LSP 保证不拆坏,DIP 为五条提供装配。五条原则互为因果

1.4 灵魂五连问

Q1 ── SOLID 那么各是什么关系?依顺序还是同时生效?
       └─→ §01.3 / §10 原则互为因果
Q2 ── SRP 的「一」到底多「一」?
       └─→ §02.3 边界判断Q3 ── OCP 中「对修改关闭」是不是谈什么都不能动?
       └─→ §03.3 实现路径
Q4 ── LSP 的“可替换”是只考编译能过么?
       └─→ §04 契约不变Q5 ── 为什么 23 种设计模式本质上都是「原则的结晶」?
       └─→ §07 原则与模式

2.从砖到房子

2.1 已有的砖

走过前五篇,你已经掌握:

  • 对象与过程的范式之别(01)
  • 四大特性的原理与落地(02)
  • 接口与抽象类的取舍(03)
  • 面向接口而非实现(04)
  • 多用组合少用继承(05)

这些是,单点技能。

2.2 缺什么

砌墙还需要图纸:什么样的代码算"好"?什么样的拆分是"合理"?什么时候加一个抽象层是"过度设计"?

flowchart LR
    砖[语法/特性] --> 图纸[设计原则<br/>SOLID]
    图纸 --> 房子[设计模式<br/>23 种]
    房子 --> 城市[架构风格<br/>DDD/微服务]

设计原则是判断尺:写代码时随时拿来对照,发现"违反"就提示重构。

2.3 SOLID 总览

缩写全称一句话
SSingle Responsibility一个类只做一件事
OOpen/Closed对扩展开放,对修改关闭
LLiskov Substitution子类必须能替换父类
IInterface Segregation接口要小而专
DDependency Inversion依赖抽象,不依赖具体
flowchart TB
    SOLID --> S[SRP<br/>职责单一]
    SOLID --> O[OCP<br/>开闭]
    SOLID --> L[LSP<br/>里氏替换]
    SOLID --> I[ISP<br/>接口隔离]
    SOLID --> D[DIP<br/>依赖倒置]
    S -.支撑.-> O
    L -.支撑.-> O
    I -.支撑.-> D

3.单一职责 SRP

7.1 一句话理解

一个类应当只有一个引起它变化的理由。

"变化的理由"通常对应一个角色/一种业务方向:财务部门改不动技术部门的代码。

3.2 反例与正例

// ❌ Order 类既算钱又发邮件又写库
class Order {
    Money total() { /* 计算 */ }
    void save() { /* 写库 */ }
    void sendConfirmEmail() { /* 发邮件 */ }
}

// ✓ 拆成三个职责
class Order { Money total() { /* … */ } }
class OrderRepository { void save(Order o) { /* … */ } }
class EmailNotifier { void sendConfirm(Order o) { /* … */ } }

任意改一个,编译只动一处,这就是 SRP 的工程价值。

3.3 边界判断

不要走极端,不是"一个类一个方法"。判断边界的实操技巧:

flowchart LR
    问1[这个改动<br/>是不是同一个理由] -->|是| 同一类
    问1 -->|不是| 拆出去

例子:Order.calcDiscountOrder.calcTax 都是为"算钱"服务 → 同一类; Order.save(持久化)和 Order.calcTax(业务)→ 分属基础设施与领域,应拆。


4.开闭原则 OCP

7.1 一句话理解

对扩展开放,对修改关闭。

新增功能 → 加新代码,不改旧码。

3.2 案例对照

回到 04 篇的支付例子:

// ❌ 违反 OCP
void pay(String channel) {
    if ("alipay".equals(channel)) /* … */
    else if ("wechat".equals(channel)) /* … */
}

// ✓ 满足 OCP
void pay(PaymentGateway g, Order o) { g.pay(o); }

加 PayPal → 加一个 PaypalGatewaypay 函数零改动

3.3 实现路径

OCP 不是凭空实现的,需要前面所有招式协力:

flowchart LR
    SRP --> 拆出独立扩展点
    LSP --> 子类安全替换
    DIP --> 依赖抽象不依赖具体
    抽象+多态 --> OCP[OCP<br/>扩展开放<br/>修改关闭]

5.里氏替换 LSP

7.1 一句话理解

凡是基类能用的地方,子类必须能用,且行为不会令使用方惊讶。

5.2 经典反例

// 鸵鸟继承 Bird,但 fly() 抛异常
class Ostrich extends Bird {
    @Override public void fly() { throw new UnsupportedOperationException(); }
}

void migrate(Bird bird) { bird.fly(); }   // 拿到 Ostrich → 崩溃

子类违背了父类的契约。修复方式:让 fly() 不在父类(参考 05 篇用接口拆能力)。

5.3 契约不变

LSP 的核心是契约不变性

契约子类约束
前置条件不能更严
后置条件不能更松
不变量必须保持
异常不能抛父类未声明的

违反任意一条,子类就不再是"合法替代品",OCP 也就守不住,所以 LSP 是 OCP 的安全前提


6.接口隔离 ISP

7.1 一句话理解

客户端不应该被迫依赖它不使用的方法。

6.2 胖接口拆解

// ❌ 一个胖接口什么都管
interface UserService {
    void register(User u);
    void login(String u, String p);
    void exportToExcel();
    void sendMarketingEmail();
}
// 客户端只想登录,却被迫看到导出/营销

然后优化如下所示:

// ✓ 拆成多个小接口
interface UserAuth      { void register(User u); void login(String u, String p); }
interface UserExporter  { void exportToExcel(); }
interface MarketingMail { void sendMarketingEmail(); }

6.3 与 SRP 的差异

原则着眼点
SRP只承担一种变化原因
ISP接口只服务于一类客户

二者经常被混淆,SRP 看实现,ISP 看契约,是同一思想在不同侧的体现。


7.依赖倒置 DIP

7.1 一句话理解

高层模块不应依赖低层模块;二者都应依赖抽象。 抽象不应依赖细节;细节应依赖抽象。

6.2 倒置的方向

flowchart LR
    subgraph 传统-自上而下依赖
        H1[业务层] --> L1[支付宝SDK]
    end
    subgraph 倒置后
        H2[业务层] --> Abs[支付抽象]
        L2[支付宝SDK] --> Abs
    end

依赖箭头被"倒过来",业务不再被 SDK 牵着走,反而是 SDK 来适配业务定义的抽象。

6.3 IoC 与 DI

DIP 在工程上的落地是 IoC(控制反转)+ DI(依赖注入)

// ❌ 类自己 new 依赖
class CheckoutService {
    private AlipayGateway gateway = new AlipayGateway();
}

// ✓ 依赖通过构造注入
class CheckoutService {
    private final PaymentGateway gateway;
    CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
}

Spring/Dagger/Guice 等容器把"装配"职责从业务类剥离,业务类只声明"我需要什么",谁给 由容器决定,这就是 IoC 的字面含义。


8.原则与模式

8.1 原则是骨

SOLID 给了你"判断方向"的尺子,但没告诉你具体怎么写

8.2 模式是肉

23 种设计模式正是SOLID 的具体落地剧本

模式服务的原则
工厂方法OCP、DIP
策略模式OCP(替代 if-else)
装饰者模式OCP、LSP
适配器DIP(适配老接口)
模板方法OCP(钩子)
观察者OCP(事件解耦)

8.3 23 种模式分类

flowchart TB
    23模式 --> 创建型[创建型 5 种<br/>对象怎么造]
    23模式 --> 结构型[结构型 7 种<br/>对象怎么拼]
    23模式 --> 行为型[行为型 11 种<br/>对象怎么协作]

    创建型 --> 工厂/抽象工厂/单例/建造者/原型
    结构型 --> 适配器/桥接/装饰/外观/享元/代理/组合
    行为型 --> 策略/模板/观察者/责任链/命令/迭代器/中介者/状态/备忘录/解释器/访问者
类别关注点代表案例
创建型解耦"创建"与"使用"工厂、建造者
结构型类与对象的组合关系装饰、代理
行为型对象间的协作模式策略、观察者

9.总结与下一步

flowchart LR
    OOP特性 --> SOLID
    SOLID --> 设计模式
    设计模式 --> 架构风格
    架构风格 --> DDD/微服务

面向对象设计的全景:从语言特性 → 通用原则 → 落地模式 → 架构思想,每一层都解决前一层无法回答的问题。

阶段解决
特性怎么写对象
原则怎么写得"好"
模式已知场景的最佳实践
架构大规模系统的整体协作

10.综合实战案例

主线收束接点,05 篇我们拼出了商品能力体系,本篇要为「订单总馆」加装 SOLID 五条马马。

10.1 订单总馆需求变迁

还记得 01 篇那个订单系统吗?三年后,需求变成了这样:

- 接入 5 种支付渠道(接入、动态插拔)
- 接入 4 种营销活动(可叠加)
- 下发 3 家快递(可拓展)
- 代码量增长为初版 30 倍
- 需领域事件推送、发 BI
- 跨境订单需报关、财务检查
- 遇黑五需抔升结货能力 100 倍

代码能不崩吗?要看是否遵守 SOLID

10.2 不用原则的崩塌路径

「不遵守」版很快变成开篇的「8000 行上帝类」。详细路径:

进度点现象违反
Day 30OrderService 1500 行,出现 17 个 if (channel == "alipay")OCP / SRP
Day 90「虚拟订单」丢进同一个服务、他们不需要发货但也不得不实现 ship() 招表LSP
Day 180OrderService 同时抱 MybatisMapper HttpClient KafkaProducerDIP
Day 365接口 IOrderManager 包括 47 个方法、Mock 要写 47 个 nullISP

10.3 五条原则逐个介入

第 1 步·SRP,拆子域服务:

class OrderQueryService { ... }    // 查询
class OrderPlaceService { ... }    // 下单
class OrderRefundService { ... }   // 退款
class OrderShipService { ... }     // 发货
class OrderEventPublisher { ... }  // 领域事件
class OrderBiSink { ... }          // BI 上报

第 2 步·OCP,拆出中间件插拔点:

public interface OrderInterceptor {
    void preHandle(Order order, Context ctx);
    void postHandle(Order order, Result r, Context ctx);
}
class FinComplianceInterceptor   implements OrderInterceptor { ... }
class MarketingInterceptor       implements OrderInterceptor { ... }
class CrossBorderTaxInterceptor  implements OrderInterceptor { ... }
class MetricsInterceptor         implements OrderInterceptor { ... }

第 3 步·LSP,为「虚拟订单」重设子类:

// 错误示范:VirtualOrder.ship() throws UnsupportedException ,违反 LSP
// 正确示范:
abstract class Order {
    abstract boolean needShipping();
    Optional<ShipResult> tryShip() {
        return needShipping() ? Optional.of(doShip()) : Optional.empty();
    }
}

第 4 步·ISP,拆肥接口为能力接口:

public interface OrderQuery   { ... }
public interface OrderPlace   { ... }
public interface OrderRefund  { ... }
public interface OrderShip    { ... }
// 调用方只依赖自己需要的

第 5 步·DIP,高层仅依赖抽象:

// domain 包
public interface OrderRepo  { Order load(OrderId id); void save(Order o); }
public interface PaymentGateway { ... }

// infra 包
class MybatisOrderRepo implements OrderRepo { ... }
class AlipayGateway    implements PaymentGateway { ... }
// 高层看到的依然是 domain 包里的接口

10.4 类图与原则映射

flowchart TB
    subgraph 领域层[领域层 only-interface]
        OS[OrderPlaceService]
        PG[<<interface>><br/>PaymentGateway]
        OR[<<interface>><br/>OrderRepo]
    end
    subgraph 基础设施[基础设施]
        AG[AlipayGateway]
        WG[WechatGateway]
        MR[MybatisOrderRepo]
    end
    OS --> PG
    OS --> OR
    PG --> AG
    PG --> WG
    OR --> MR

这张图上身肣 5 条原则同时生效:

  • SRP:每个服务只负责一个子域
  • OCP:加一家支付?只加一个实现类
  • LSP:所有 PaymentGateway 实现都能互换
  • ISP:领域层不包含以什么能力以外的方法
  • DIP:领域层不依赖以 ORM/HTTP/SDK 在内的底层

10.5 留下三道思考题

答案在第 07 篇开头揭晓。

🟢 易:上面架构中,我把 OrderRepo 接口放在了 domain 包、MybatisOrderRepo 实现放在 infra 包。交换位置(接口放 infra、实现放 domain)这件事身是否依然“能跑”?会违反 SOLID 中的哪一条?

🟡 中:「跨境订单」需要 extends ThirdPartySdkBase。这跳跟 04 篇现场一样 ,你会选择「领域层引入 SDK 概念」还是「领域层实体透过中转适配」?为什么?

🔴 难:本项目中 SOLID 在 8000 行上帝类上同时出错。如果要制定一个 CR 检查清单让 5 条原则能被机械发现、你会怎么设计?提示:类型列、包依赖检查、代码位置。


11.认知跃迁总结

回到开篇的 8000 行 OrderManager。如果从项目第一天起就被 SOLID 五条马马同时拉住马是事、它不可能长到 8000 行。它只会逐渐分裂、仅仅生长为一棵体系、而不是一坛腐股。

1句话:SOLID 不是面试题,是你代码上限的堆高。五条同时拉住,系统才能随需求生长不崩。

但“五条原则”本身也会被滥用、误用、过度设计。这正是下一篇 07.SOLID原则案例汇 要带你走进的「现场现场」:看它们在真实项目里被怎么用股、怎么被误叫、何时该刻意不遵守。