SOLID五大设计原则:重构优雅代码与架构的底层规范

  很多人学完封装、继承、多态、组合,仍会写出"有类无设计"的代码:一个类什么都干、改需求就改老代码、子类不能替父类、接口又肥又乱、高层模块死盯底层实现。根因不是没背过SOLID缩写,而是没把五大原则当成排查坏味道、约束协作关系、落地设计模式的架构标尺。

  本文将逐条拆解单一职责、开闭、里氏替换、接口隔离、依赖倒置。每条原则给出核心定义、业务场景、反例与正例,并讲清原则之间的联动。所有设计模式的本质,都是对SOLID的合理落地与极致优化。

一、为什么先吃透SOLID,再谈设计模式

  设计模式解决的是"怎么组织对象协作",SOLID解决的是"协作该不该这么组织"。没有SOLID,工厂、策略、装饰器往往变成生搬硬套的"模式表演"。有了SOLID,模式才有判断依据,该不该拆、能不能扩、能不能替、接口该不该肥、依赖该指向谁。

五大原则可记作一条架构主线:

原则 英文 核心目标 一句话口诀
单一职责 SRP 变更原因唯一 一类一事
开闭 OCP 扩展开、修改关 加新不改旧
里氏替换 LSP 子类可替换父类 能替才叫继承
接口隔离 ISP 接口小而专 不强迫用不了的
依赖倒置 DIP 依赖抽象不依赖细节 高层不认底层

  它们不是彼此割裂的教条,SRP拆得好,OCP才扩得开;LSP/ISP把继承与接口守住边界;DIP把协作重心翻到抽象上。策略、工厂、适配器、模板方法等模式,本质上都是在强化这条链路。

二、S:单一职责原则(SRP)

2.1 核心定义:一个类只有一个引起它变化的原因

  Robert C. Martin的原意是:一个模块应对一个参与者(actor)负责,即引起该类修改的业务动机应当唯一。类里方法可以很多,但这些方法必须服务于同一职责边界。

架构价值是高内聚,把不同变化源拆开,改结算不影响登录,改短信通道不影响订单落库。这是后续OCP、DIP能成立的前提------职责混杂时,任何"扩展"都会牵一发而动全身。

2.2 业务场景

  电商订单模块同时承担:创建订单、计算优惠、发短信通知、写操作日志、导出Excel。需求上线后,运营改短信文案就要动订单类,财务改导出格式也要动订单类,这是典型的多变化源撞车。

2.3 反例:一个类扛全场

反例

java 复制代码
public class OrderService {
    public void createOrder(Order order) {
    // 1. 校验库存
    // 2. 算价落库
    // 3. 发短信
    // 4. 写审计日志
    // 5. 同步导出报表字段
    }
}

  表面"一个入口很方便",实则通知、审计、报表任意一方变更,都要打开核心交易类------耦合的是变化源,不是方法个数。

2.4 正例:按变化源拆分,再组合协作

java 复制代码
public class OrderRepository {
    public void save(Order order) { /* 持久化 */ }
}

public class PriceCalculator {
    public Money calc(Order order) { /* 计价 */ return null; }
}

public class OrderNotifier {
    public void notifyCreated(Order order) { /* 短信/站内信 */ }
}

public class AuditLogger {
    public void log(Order order) { /* 审计 */ }
}

public class OrderAppService {
    private final OrderRepository repo;
    private final PriceCalculator calculator;
    private final OrderNotifier notifier;
    private final AuditLogger auditLogger;

    public OrderAppService(OrderRepository repo, PriceCalculator calculator,
                           OrderNotifier notifier, AuditLogger auditLogger) {
        this.repo = repo;
        this.calculator = calculator;
        this.notifier = notifier;
        this.auditLogger = auditLogger;
    }

    public void createOrder(Order order) {
        order.setPayable(calculator.calc(order));
        repo.save(order);
        notifier.notifyCreated(order);
        auditLogger.log(order);
    }
}

  OrderAppService只编排流程,各细节类各自应对自己的变化。这既是SRP,也是上一章强调的组合复用:用has-a拼职责,而不是用继承硬塞能力。排查的时候可以问自己,例如改短信文案要不要动这个类?改计价规则要不要动?改存储引擎要不要动?若答案里有两个"要",就开始拆。

三、O:开闭原则(OCP)

3.1 核心定义:对扩展开放,对修改关闭

  开闭原则指的是:软件实体(类、模块、函数)应该允许通过新增代码扩展行为,而尽量避免为了新业务去改已经稳定的旧代码。它不是禁止一切修改,而是要求你把"会变的"抽成抽象,让稳定流程只依赖抽象。策略模式、工厂模式、模板方法,几乎都是OCP的工程化表达。

3.2 业务场景

  支付渠道从支付宝、微信扩展到云闪付:若每次加渠道都在if-else里插入分支,旧逻辑会被反复打开------回归成本陡增。正确做法是:统一支付抽象,新渠道=新实现类。

3.3 反例:用修改堆需求

例如:

python 复制代码
class PayService:
    def pay(self, channel: str, amount: float):
        if channel == "alipay":
            print(f"支付宝支付 {amount}")
        elif channel == "wechat":
            print(f"微信支付 {amount}")
        elif channel == "unionpay":  # 每次加渠道都改这里
            print(f"云闪付支付 {amount}")
        else:
            raise ValueError("unknown channel")

  PayService永远不会稳定:每个新渠道都是一次对核心类的侵入修改。

3.4 正例:抽象入口+新增实现

python 复制代码
from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None: ...

class AliPay(Payment):
    def pay(self, amount: float) -> None:
        print(f"支付宝支付:{amount}")

class WeChatPay(Payment):
    def pay(self, amount: float) -> None:
        print(f"微信支付:{amount}")

class UnionPay(Payment):
    def pay(self, amount: float) -> None:
        print(f"云闪付支付:{amount}")

class PayService:
    def do_pay(self, payment: Payment, amount: float) -> None:
        payment.pay(amount)

# 扩展:只新增类,不改 PayService
PayService().do_pay(UnionPay(), 99.0)

  多态提供"统一调用、差异实现";OCP要求你把这种差异固化在扩展点上。没有抽象边界,多态只是写法。有了扩展点,多态才是架构能力。

3.5 与封装的关系

  上一章讲封装是"隔离变化"。OCP是隔离变化的工程化目标。变化被封在新类里,旧调用方接口不变。若属性、行为、依赖都裸奔在外,根本谈不上"对修改关闭"。

四、L:里氏替换原则(LSP)

4.1 核心定义:子类型必须能够替换其基类型

  里氏替换强调,凡是使用父类型的地方,换成其子类型后,程序行为语义仍应成立,不能悄悄破坏前置条件、后置条件或不变式。

LSP不是"能编译/能跑就行",而是"替换后约定仍成立"。违反LSP的继承,是上一章说的伪is-a:看起来像继承,实际是披着继承外衣的功能缝合。

4.2 业务场景与经典陷阱

  "正方形是矩形"在数学上成立,在可变对象模型里常不成。矩形允许独立改宽高,正方形改宽必须联动改高。若业务代码依赖"改宽不影响高",用正方形替换矩形会行为错乱,这是继承复用压过语义正确的典型坏味道。

4.3 反例:伪替换

python 复制代码
class Bird {
    public void fly() {
        System.out.println("飞翔");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("企鹅不会飞");
    }
}

// 调用方假定所有Bird都能fly
void migrate(Bird bird) {
    bird.fly(); // 传入Penguin直接炸裂
}
python 复制代码
class Rectangle:
    def __init__(self, w, h):
        self.w, self.h = w, h

    def set_width(self, w):
        self.w = w

    def area(self):
        return self.w * self.h

class Square(Rectangle):
    def set_width(self, w):
        self.w = self.h = w  # 破坏了"只改宽"的约定

def stretch(rect: Rectangle):
    rect.set_width(rect.w + 10)
    # 调用方以为面积只按宽度变化;正方形会连带改高

4.4 正例:按真实能力建模,不强行继承

python 复制代码
from abc import ABC, abstractmethod

class Flyer(ABC):
    @abstractmethod
    def fly(self): ...

class Walker(ABC):
    @abstractmethod
    def walk(self): ...

class Sparrow(Flyer, Walker):
    def fly(self): print("麻雀飞翔")
    def walk(self): print("麻雀行走")

class Penguin(Walker):
    def walk(self): print("企鹅行走")

def migrate(flyer: Flyer):
    flyer.fly()  # 只接受真正会飞的类型

  或用组合表达能力,而不是把所有鸟塞进一个会飞的父类。能替换,才叫正当继承。不能替换,就该拆接口或改组合。

4.5 排查步骤

  1. 子类是否削弱了父类承诺?
  2. 子类是否收紧了输入、放宽了错误输出?
  3. 文档/测试里对基类的断言,子类是否仍成立? 有一条"否",LSP就不能用。

五、I:接口隔离原则(ISP)

5.1 核心定义:不应强迫客户端依赖它不需要的接口

  ISP针对的是"胖接口",一个巨大接口塞满所有方法,实现者不得不空实现或乱抛异常,调用者也被无用方法污染。正确做法是按客户端需要拆成多个小而专的接口。

  注意ISP拆的是"角色接口",不是机械地一个方法一个接口。拆到客户端刚好够用、实现者刚好能兑现。

5.2 业务场景

  打印设备体系:有的机器只会打印,有的能扫描传真。若统一强制实现print/scan/fax,简易打印机只得写下假实现,这既违反ISP,也会连带违反LSP(替换后行为失真)。

5.3 反例:胖接口逼空实现

python 复制代码
interface MultiFunctionDevice {
    void print(String doc);
    void scan(String doc);
    void fax(String doc);
}

class SimplePrinter implements MultiFunctionDevice {
    public void print(String doc) { System.out.println("打印:" + doc); }
    public void scan(String doc) { throw new UnsupportedOperationException(); }
    public void fax(String doc) { throw new UnsupportedOperationException(); }
}

5.4 正例:按角色拆分

python 复制代码
from typing import Protocol

class Printer(Protocol):
    def print(self, doc: str) -> None: ...

class Scanner(Protocol):
    def scan(self, doc: str) -> None: ...

class SimplePrinter:
    def print(self, doc: str) -> None:
        print(f"打印:{doc}")

class OfficeAllInOne:
    def print(self, doc: str) -> None: ...
    def scan(self, doc: str) -> None: ...
    def fax(self, doc: str) -> None: ...

def do_print(device: Printer, doc: str) -> None:
    device.print(doc)  # 调用方只依赖自己需要的能力

  Python的Protocol/鸭子类型很灵活,但也更容易写出"同名方法乱共享"。ISP要求在设计上先划清角色,再谈灵活。

5.5 记忆表

症状 往往违反 改法
实现类大量UnsupportedOperationException ISP+LSP 拆接口
调用方import了一堆用不到的方法 ISP 收窄依赖接口
改一个客户端无关功能却牵动多方编译 ISP / SRP 按角色与变化源拆分

六、D:依赖倒置原则(DIP)

6.1 核心定义:高层模块不应依赖低层模块,二者都应依赖抽象

  依赖倒置把常识里的依赖方向翻过来,业务编排(高层)不要直接new具体短信网关、具体数据库驱动(低层)。两边都依赖抽象(接口/协议)。控制反转(IoC)与依赖注入(DI)是DIP的常见落地手段,但DIP本身是设计方向,不是框架专属。

  上一章的依赖、关联、聚合、组合,描述的是耦合强度。DIP进一步规定:稳定的抽象应处在依赖箭头的中心。

6.2 业务场景

  通知中心:订单完成后发短信。若OrderService直接依赖AliyunSmsClient,换供应商就改核心业务;若双方都依赖Notifier抽象,替换通道只需换实现与装配。

6.3 反例:高层钉死低层细节

python 复制代码
class AliyunSmsClient:
    def send(self, phone: str, content: str):
        print(f"[Aliyun] {phone}: {content}")

class OrderService:
    def __init__(self):
        self.sms = AliyunSmsClient()  # 高层直接依赖细节

    def complete(self, order):
        # ...完成订单
        self.sms.send(order.phone, "订单已完成")

6.4 正例:依赖抽象,细节可替换

python 复制代码
from typing import Protocol

class Notifier(Protocol):
    def send(self, phone: str, content: str) -> None: ...

class AliyunSmsNotifier:
    def send(self, phone: str, content: str) -> None:
        print(f"[Aliyun] {phone}: {content}")

class OrderService:
    def __init__(self, notifier: Notifier):
        self.notifier = notifier

    def complete(self, order):
        self.notifier.send(order.phone, "订单已完成")

# 装配点决定具体实现,业务核心不感知供应商
svc = OrderService(AliyunSmsNotifier())

  DIP让OCP真正可执行,扩展点(新Notifier实现)能插进去,是因为高层从来没绑死某个低层类。

6.5 与四种对象关系的对照

关系 常见误用 DIP 建议
依赖 方法临时new具体类 参数接收抽象,或从工厂/容器取抽象
关联/聚合 成员变量直接持有具体实现 成员类型声明为接口/协议
组合 内部写死具体部分且无法替换 对可变部分组合抽象,或在边界注入

十、总结

  SOLID 的核心从来不是五条英文字母,而是一条可执行的架构纪律:

  • 单一职责把变化源拆开,形成高内聚单元。
  • 开闭要求用新增实现承接变化,稳住已验证逻辑。
  • 里氏替换守住继承与多态的契约底线。
  • 接口隔离让抽象小而专,避免胖接口污染。
  • 依赖倒置把依赖方向扭向抽象,让高层业务摆脱低层细节。
相关推荐
饼干哥哥2 小时前
Vibe Coding 出海首月收割 100+用户爆赚美金,怎么做?
后端·架构·代码规范
ting94520002 小时前
纯技术硬核解析:Timbal AI 全栈底层架构、引擎原理与生产级 AI 应用落地实现
人工智能·架构
行者全栈架构师2 小时前
大模型批量生成代码的质量衰减:为什么 AI 越写越敷衍?根因分析与系统性解决方案
后端·架构·代码规范
小帽子_1232 小时前
储能 EMS 拓扑架构设计:单机、集群、微网多场景架构对比
架构
乐观的Terry2 小时前
1、为什么要自己造发布系统
java·spring boot·后端·spring cloud·架构
饮茶三千2 小时前
电子保函模板编辑器实战二:HTML→FTL 编译转换的设计与实现
前端·vue.js·设计模式
咏方舟【长江支流】2 小时前
【开源】跨语言·跨平台·跨数据库(2) —— 用宝轻量框架设计必读
数据库·架构·开源·ai编程·咏方舟-长江支流·用宝框架
Kel2 小时前
Node.js 本质:从全脉络视角理解运行时原理
javascript·设计模式·node.js