别院牧志知识库 别院牧志知识库
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • Python 面试题
  • 2025 面试记录
  • 2022 面试记录
  • 2021 面试记录
  • 2020 面试记录
  • 2019 面试记录
  • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • Python 面试题
  • 2025 面试记录
  • 2022 面试记录
  • 2021 面试记录
  • 2020 面试记录
  • 2019 面试记录
  • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 辨析

  • Sockets编程

  • Django

  • stackoverflow

  • Flask

  • 全栈之路

  • 面试

  • 代码片段

  • 异步编程

  • 😎Awesome资源

  • PEP

  • Python工匠系列

  • 高阶知识点

  • Python 学习资源待整理
  • 设计模式

  • 好“艹蛋”的 Python 呀!
  • FIFO | 待学清单📝
  • pip 安装及使用
  • 数据分析

  • 源码阅读计划

  • OOP

    • 面向对象之封装、继承、多态
    • 深入理解 OOP 的 SOLID 原则
      • SOLID 原则概述
      • 1. SRP:单一职责原则
        • 一、表面矛盾 vs 本质原则
        • 二、判断是否违反 SRP 的关键测试
        • 三、Python 中的合规类设计示例
        • 四、常见违规场景及解决方案
        • 五、SRP 的黄金实践准则
        • 六、为什么 Python 开发者更容易违反 SRP
        • 结论:方法多≠违反 SRP
      • 2. OCP:开闭原则
        • 一、原则本质:用抽象抵御变化
        • 二、违反 OCP 的典型场景
        • 三、符合 OCP 的解决方案
        • 四、OCP 的落地技巧
        • 五、Python 中的灵活实现
        • 六、OCP 的收益与代价
        • 总结:如何理解“修改关闭”
        • 与策略模式的关联
        • 总结:设计原则与模式的共生关系
      • 3. LSP:里氏替换原则
        • 一、LSP 本质:行为契约的继承
        • 二、违反 LSP 的经典反例
        • 三、LSP 的三大核心契约
        • 四、正确实践:飞行器控制示例
        • 五、LSP 的终极理解:可替换性是行为契约
        • 六、LSP 与设计模式的关联
        • 总结:LSP 的实践精髓
      • 4. ISP:接口隔离原则
      • 5. DIP:依赖倒置原则
        • 一、传统分层架构:高层依赖低层
        • 二、依赖倒置架构:共同依赖抽象
        • 三、依赖倒置的威力展示
        • 四、DIP 的三大实现机制
        • 五、DIP 的实践收益
        • 六、依赖倒置 vs 依赖注入
        • 七、Python 特有实践技巧
        • 终极总结:依赖倒置的本质
      • 面试常见问题
  • 关于 python 中的 setup.py
  • 并行分布式框架 Celery
  • 七种武器,让你的代码提高可维护性
  • 使用 pdb 调试 Python 代码
  • 每周一个 Python 标准库
  • 🐍Python
  • OOP
佚名
2022-03-25
目录

深入理解 OOP 的 SOLID 原则

# SOLID 原则概述

SOLID 是面向对象编程(OOP)的五大设计原则,由 Robert C. Martin 提出,旨在提高代码的可维护性、扩展性和复用性:

  1. SRP:单一职责原则
    一个类只应有一个引起变化的原因

  2. OCP:开闭原则
    软件实体应对扩展开放,对修改关闭

  3. LSP:里氏替换原则
    子类必须能够替换其基类

  4. ISP:接口隔离原则
    不应强迫客户端依赖它们不用的接口

    客户端程序不应该依赖它不需要的接口方法,一个接口所提供的方法,应该就是调用方所需要的方法

  5. DIP:依赖倒置原则
    高层模块不应依赖低层模块,二者都应依赖抽象

    面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。

    面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

# 1. SRP:单一职责原则

误区:认为"一个类只能有一个方法"
正解:关注职责的单一性(变化原因的隔离)

# 违反SRP:同时处理订单逻辑和数据库操作
class Order:
    def calculate_total(self): ...  # 业务逻辑
    def save_to_db(self): ...       # 持久化职责
    
# 遵循SRP:拆分职责
class Order:
    def calculate_total(self):
        pass

class OrderRepository:
    def save_to_db(self, order):
        pass
1
2
3
4
5
6
7
8
9
10
11
12
13

单一职责的核心在于对“职责”的粒度理解。单一职责原则(SRP)的精髓不是“一个类只能有一个方法”,而是 “一个类只应对一种类型的变更负责”。让我们通过对比分析来彻底解释这个矛盾:

# 一、表面矛盾 vs 本质原则

表面现象 本质原则 解析
一个类包含多个方法 一个类只响应一种业务变化 方法数量≠职责数量,关键在于变更原因是否相同
User 类有 login()、update_profile()等方法 所有方法都围绕用户实体的核心行为 当“用户管理规则”变化时才需修改此类
若类包含 send_email()+save_to_db() 违反 SRP:需因邮件协议或数据库变更分别修改 此时应拆分为 UserService + EmailService

# 二、判断是否违反 SRP 的关键测试

用这两个问题检验类的设计:

  1. 变更触发测试
    “修改 XXX 功能时,是否必须修改这个类?”
    ✅ 合法:修改用户密码策略时只需改User类
    ❌ 违规:修改邮件模板时被迫改User类

  2. 职责描述测试
    能否用一个业务名词描述类的职责?
    ✅ 合法:Order(订单处理)、PaymentGateway(支付对接)
    ❌ 违规:OrderAndEmailUtil(混合订单和邮件)

# 三、Python 中的合规类设计示例

# ✅ 符合SRP:所有方法只处理“用户身份认证”这一种变化
class Authenticator:
    def login(self, username, password):
        # 仅处理认证逻辑
        pass
    
    def logout(self):
        pass
    
    def refresh_token(self):
        pass

# ✅ 符合SRP:仅负责用户数据持久化
class UserRepository:
    def save(self, user):
        # 数据库操作
        pass
    
    def find_by_id(self, user_id):
        pass

# ❌ 违反SRP:混合认证、存储、通知三种职责
class UserManager:
    def login(self): ...          # 认证职责
    def save_to_db(self): ...     # 存储职责
    def send_welcome_email(self): # 通知职责
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 四、常见违规场景及解决方案

# 场景 1:上帝类(God Class)

# 违反SRP:一个类处理所有订单相关逻辑
class OrderProcessor:
    def validate_order(self): ...   # 校验
    def calculate_tax(self): ...    # 计算
    def save_to_database(self): ... # 存储
    def print_receipt(self): ...    # 打印
    def notify_warehouse(self): ... # 通知
    
1
2
3
4
5
6
7
8

修复方案:

class OrderValidator: ...          # 校验职责
class TaxCalculator: ...           # 计算职责
class OrderRepository: ...         # 存储职责
class ReceiptPrinter: ...          # 打印职责

1
2
3
4
5

# 场景 2:混合业务与技术细节


# 违反SRP:业务逻辑和技术实现耦合
class ReportGenerator:
    def fetch_data(self): 
        # 直接调用SQL查询 ❌
    
    def format_html(self): ...     # 展示逻辑
    
1
2
3
4
5
6
7
8

修复方案:


class ReportDataFetcher: ...       # 数据获取职责
class HtmlReportRenderer: ...      # 渲染职责

1
2
3
4

# 五、SRP 的黄金实践准则

  1. 业务逻辑聚焦
    类的所有方法应服务于同一个业务目标(如“订单创建”、“支付处理”)

  2. 技术隔离
    数据库操作、网络请求、日志记录等技术细节应封装到独立类

  3. 变更隔离
    当以下任一变化发生时,不应影响同一个类:

    • 业务规则变化 vs 技术实现变化

    • 前端展示变化 vs 后端计算变化

    • 数据来源变化 vs 数据处理逻辑变化

  4. 依赖方向
    高层业务类依赖抽象(接口),而非具体技术实现类

# 六、为什么 Python 开发者更容易违反 SRP

特性 风险 解决方案
动态类型 容易创建“全能类” 明确用抽象基类(ABC)定义接口
Duck Typing 忽略明确的职责边界 遵循“一个类只做一件事”的命名约束
脚本思维惯性 习惯写过程式代码 领域驱动设计(DDD)训练

# 结论:方法多≠违反 SRP

✅ 合规情况:
当类中的多个方法都服务于同一个业务职责(例如User类的所有方法都管理用户核心数据),即使有 20 个方法也不违反 SRP。

❌ 违规标志:
当修改类的原因来自不同业务维度(例如:需要因邮件服务商更换修改login()方法,或因数据库迁移修改get_profile()方法),则必须拆分。

终极判断标准:

当业务需求变化时,你的修改是否只局限于某个特定类?
如果是,则符合 SRP;
如果需要跨多个不相关类修改,说明职责耦合;
如果修改点集中在一个类的不同位置,说明职责过重。

通过这种设计,你会发现:

  • 代码变更更安全(修改点局部化)

  • 测试更容易(单一职责类的测试用例更聚焦)

  • 系统更灵活(替换技术实现不影响业务逻辑)

# 2. OCP:开闭原则

误区:认为"绝对禁止修改已有类"
正解:通过扩展(继承/组合)添加新功能,而非修改

# 违反OCP:新增形状需修改AreaCalculator
class AreaCalculator:
    def area(self, shape):
        if type(shape) == Circle:
            return math.pi * shape.radius ** 2
        elif type(shape) == Square:  # 添加新形状需修改代码
            return shape.side ** 2

# 遵循OCP:抽象Shape类,扩展无需修改计算器
class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Circle(Shape):
    def area(self): return math.pi * self.radius ** 2

class AreaCalculator:
    def area(self, shape: Shape):  # 依赖抽象
        return shape.area()  # 新增形状不影响此方法
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

开闭原则(Open-Closed Principle, OCP) 表面上看似乎与开发实践冲突(根据需求变更频繁修改代码),但其核心理念是通过架构设计减少核心逻辑的修改。下面用具体场景和代码示例解释如何理解“关闭修改”与“开放扩展”:

# 一、原则本质:用抽象抵御变化

关键概念 解释
对修改关闭 核心业务逻辑一旦完成,不应因新需求而频繁修改(避免引入风险)
对扩展开放 通过抽象层(接口/继承/组合)灵活添加新功能,无需改动已有代码
核心目标 降低系统耦合度,让新增需求通过新增代码实现,而非修改旧代码

# 二、违反 OCP 的典型场景

假设有一个支付处理类:


class PaymentProcessor:
    def process(self, payment_type):
        if payment_type == "alipay":
            self._process_alipay()   # 支付宝支付逻辑
        elif payment_type == "wechat":
            self._process_wechat()   # 微信支付逻辑

# 当需要新增银联支付时,必须修改类内部代码
processor = PaymentProcessor()
processor.process("unionpay")  # ❌ 需添加新的if分支

1
2
3
4
5
6
7
8
9
10
11
12

问题:
每次新增支付方式都要修改 process() 方法,可能破坏已有逻辑(如影响支付宝流程)。

# 三、符合 OCP 的解决方案

# 步骤 1:定义抽象接口


from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def execute(self):
        pass
        
1
2
3
4
5
6
7
8

# 步骤 2:实现具体支付策略


class AlipayStrategy(PaymentStrategy):
    def execute(self):
        print("支付宝支付逻辑")

class WechatPayStrategy(PaymentStrategy):
    def execute(self):
        print("微信支付逻辑")
1
2
3
4
5
6
7
8

# 步骤 3:核心处理器依赖抽象


class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):  # 依赖抽象而非具体实现
        self.strategy = strategy
    
    def process(self):
        self.strategy.execute()  # 核心逻辑永不修改
        
1
2
3
4
5
6
7
8

# 步骤 4:扩展新支付方式


# ✅ 新增银联支付:无需修改PaymentProcessor
class UnionPayStrategy(PaymentStrategy):
    def execute(self):
        print("银联支付逻辑")

# 使用新支付
processor = PaymentProcessor(UnionPayStrategy())
processor.process()  # 安全扩展

1
2
3
4
5
6
7
8
9
10

# 四、OCP 的落地技巧

# 1. 识别变化点

  • 将易变部分(如支付方式、通知渠道)抽象为接口

  • 稳定部分(如支付流程控制)保持关闭

# 2. 扩展方式对比

方式 描述 OCP 合规性
继承 子类重写父类方法 ⚠️ 有限支持(可能破坏父类逻辑)
组合+接口 将行为注入核心类(推荐) ✅ 完全支持
插件机制 动态加载扩展模块 ✅ 最佳实践

# 3. 何时允许修改代码

OCP 不是禁止所有修改,而是禁止:

  • 修改核心业务逻辑(如订单状态机)

  • 修改已通过测试的稳定模块

允许修改的情况:

  • 修复 bug

  • 重构非核心工具类

  • 调整接口实现(不改变抽象契约)

# 五、Python 中的灵活实现

# 方案 1:基于协议的鸭子类型(Pythonic 方式)


# 定义隐式协议(无需继承)
class PaymentProcessor:
    def process(self, strategy):
        strategy.execute()  # 只要传入对象有execute()方法即可

# 扩展新支付(无需继承统一接口)
class CryptoPay:  
    def execute(self):      # 符合协议的方法
        print("加密货币支付")

processor.process(CryptoPay())  # ✅ 无缝扩展

1
2
3
4
5
6
7
8
9
10
11
12
13

# 方案 2:使用函数抽象


class PaymentProcessor:
    def process(self, payment_func):  # 接收函数作为策略
        payment_func()

# 扩展只需定义新函数
def apple_pay():
    print("Apple Pay逻辑")

processor.process(apple_pay)  # ✅ 函数即策略

1
2
3
4
5
6
7
8
9
10
11

# 六、OCP 的收益与代价

收益 代价
降低回归测试成本 前期设计复杂度增加
提升系统稳定性(核心模块不变) 过度抽象会导致代码冗余
新功能通过插件式开发快速上线 不适用于极少变化的场景

黄金准则:

在第三次遇到相同类型的需求变更时进行抽象
(参考:"Three Strikes and You Refactor" 原则)


# 总结:如何理解“修改关闭”

场景 是否违反 OCP 原因
修改核心类内部逻辑 ❌ 违反 可能影响已有功能
新增实现类扩展功能 ✅ 符合 通过添加代码实现需求,核心类如 PaymentProcessor 无需改动
修复基础工具类 bug ✅ 允许 不属于业务逻辑扩展
重写抽象接口 ⚠️ 谨慎(语义兼容) 需保证所有实现类仍满足契约

终极答案:
开闭原则不是禁止修改代码,而是通过架构设计将修改隔离在“变化层”(如新增UnionPayStrategy),
从而保护“稳定层”(如PaymentProcessor.process())像基础设施一样坚固可靠。

这就像升级手机 APP:

  • 扩展开放 → 安装新 APP 增加功能(无需拆解手机硬件)

  • 修改关闭 → 手机硬件本身保持稳定

# 与策略模式的关联

开闭原则(OCP)和策略模式本质上是理念与实现的关系——策略模式是实践 OCP 最典型的代码设计手段之一。下面通过对比分析揭示它们的关联:

# 一、核心关系图解

# 二、策略模式如何实现 OCP

# 策略模式的三要素
组件 作用 OCP 贡献点
策略接口 定义行为抽象(如 PaymentStrategy) 扩展点:新策略实现此接口即可
具体策略 实现不同算法(如 AlipayStrategy) 扩展单元:新增策略=新增类
上下文类 持有策略并执行(如 PaymentProcessor) 稳定核心:永不修改业务主逻辑

# OCP 落地流程

  1. 隔离变化点 → 将支付方式抽象为接口

  2. 封装行为 → 每种支付作为独立策略类

  3. 委托执行 → 上下文类调用策略接口

  4. 扩展功能 → 新增策略类而非修改上下文

# 三、对比:OCP 是目标,策略模式是工具

维度 开闭原则 (OCP) 策略模式 (Strategy Pattern)
性质 设计原则(抽象理念) 设计模式(具体实现模板)
关注点 架构级:模块如何应对外部变化 代码级:如何组织类与对象交互
实现方式 可通过多种模式实现(策略/观察者等) 通过接口+多态实现行为动态替换
典型场景 支付扩展/通知渠道切换/算法更新 需要运行时切换算法的场景

# 四、策略模式之外的 OCP 实现方式

OCP 不局限于策略模式,其他常见实现手段包括:

# 1. 观察者模式
# 开放扩展:任意新增观察者
class Newsletter:
    def __init__(self):
        self._subscribers = []

    def add_subscriber(self, subscriber):  # 扩展点
        self._subscribers.append(subscriber)

    def publish(self, msg):
        for sub in self._subscribers:
            sub.receive(msg)  # 核心逻辑不修改

class EmailSubscriber:
    def receive(self, msg):
        print(f"邮件发送: {msg}")

# 扩展新观察者无需修改Newsletter
class SMSSubscriber:
    def receive(self, msg):
        print(f"短信发送: {msg}")
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2. 装饰器模式

class DataFetcher:
    def fetch(self):
        return "原始数据"

# 扩展功能通过装饰器叠加
class CacheDecorator:
    def __init__(self, fetcher):
        self._fetcher = fetcher
        self._cache = None

    def fetch(self):  # 不修改原类代码
        if not self._cache:
            self._cache = self._fetcher.fetch()
        return f"[缓存] {self._cache}"

# 使用
fetcher = CacheDecorator(DataFetcher())
print(fetcher.fetch())  # [缓存] 原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3. 插件架构

# 核心系统扫描并加载插件
class PluginSystem:
    def __init__(self):
        self.plugins = []

    def load_plugins(self):
        # 动态发现插件(无需修改代码)
        self.plugins = discover_plugins()  

    def run(self):
        for plugin in self.plugins:
            plugin.execute()  # 核心执行逻辑稳定
1
2
3
4
5
6
7
8
9
10
11
12

# 五、关键结论:OCP 与策略模式的关系

  1. 策略模式是 OCP 的“金牌实践者”

    • 当变化点是算法或策略时(如支付/排序方式),优先选择策略模式
  2. OCP 的实现不唯一

    • 根据场景选择合适模式:

      • 行为扩展 → 策略模式

      • 事件响应 → 观察者模式

      • 功能叠加 → 装饰器模式

      • 系统扩展 → 插件架构

  3. 违反 OCP 的信号
    当出现以下代码时,意味着可能需引入策略模式:

    if type == "A": do_A()
    elif type == "B": do_B()  # 每新增类型都需修改此处
    
    
    1
    2
    3

# 六、综合示例:用策略模式实践 OCP

假设需要计算不同国家的税费:

# 策略接口(开放扩展的基石)
class TaxStrategy(ABC):
    @abstractmethod
    def calculate(self, amount: float) -> float:
        pass

# 具体策略(扩展单元)
class USATaxStrategy(TaxStrategy):
    def calculate(self, amount):
        return amount * 0.08  # 美国税率

class ChinaTaxStrategy(TaxStrategy):
    def calculate(self, amount):
        return amount * 0.06  # 中国税率

# 上下文类(对修改关闭的核心)
class OrderProcessor:
    def __init__(self, tax_strategy: TaxStrategy):
        self.tax_strategy = tax_strategy

    def process_order(self, amount):
        tax = self.tax_strategy.calculate(amount)  # 稳定不变
        print(f"税额: {tax:.2f}")

# 扩展新国家(无需修改OrderProcessor)
class JapanTaxStrategy(TaxStrategy):
    def calculate(self, amount):
        return amount * 0.1  # 日本税率

# 使用
processor = OrderProcessor(JapanTaxStrategy())
processor.process_order(100)  # 税额: 10.00

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 总结:设计原则与模式的共生关系

设计原则 实现该原则的典型模式 解决的核心问题
开闭原则 策略模式/观察者/装饰器 功能扩展不改动核心代码
依赖倒置 依赖注入/适配器模式 解耦高层模块与底层实现
单一职责 外观模式/代理模式 拆分臃肿类的职责

最终回答:
策略模式是实现开闭原则最直观的工具,它通过将可变行为抽象为接口,使得新增功能只需扩展新类而非修改已有类。
但 OCP 作为原则更具普适性——您可以用任何合理架构实现“开放扩展,封闭修改”的目标,策略模式只是其中一种优雅的代码组织形式。

# 3. LSP:里氏替换原则

误区:认为"只要继承就是 LSP"
正解:子类必须保持父类行为约定

# 违反LSP:正方形重写setter破坏矩形行为
class Rectangle:
    def __init__(self, w, h):
        self.width = w
        self.height = h
    
    @property
    def area(self):
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)
    
    @Rectangle.width.setter  # 破坏父类契约
    def width(self, value):
        self._width = self._height = value

# 使用示例(会出错)
rect = Square(5)
rect.width = 10  # 预期面积=50,实际=100(行为不一致)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

里氏替换原则(LSP)的核心矛盾点:“完全替换”不是指子类方法实现必须与父类相同,而是强调子类必须遵守父类的行为契约。下面通过代码示例和设计哲学彻底解析这个原则:

# 一、LSP 本质:行为契约的继承

关键概念 含义
语法替换 子类拥有父类所有方法签名(参数/返回值类型兼容) → 编译器不报错
行为契约替换 子类方法需满足: 1. 前置条件不强于父类
2. 后置条件不弱于父类
3. 不修改父类禁止修改的状态

# 二、违反 LSP 的经典反例

# 场景:正方形(Square)继承长方形(Rectangle)


class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    def set_width(self, w):  # 契约:可独立修改宽
        self._width = w
    
    def set_height(self, h): # 契约:可独立修改高
        self._height = h
    
    def area(self):
        return self._width * self._height

# 正方形"是"长方形? 数学成立,编程违反LSP!
class Square(Rectangle):
    def set_width(self, w):
        super().set_width(w)
        super().set_height(w)  # 强制修改高 → 破坏父类契约!
    
    def set_height(self, h):
        super().set_height(h)
        super().set_width(h)   # 强制修改宽 → 破坏父类契约!

# 测试函数(适用于任何Rectangle子类)
def test_resize(rect: Rectangle):
    original_area = rect.area()
    rect.set_width(10)          # 预期:只修改宽
    rect.set_height(20)         # 预期:只修改高
    expected = original_area * 2 # 预期面积变为2倍? ❌
    assert rect.area() == expected

# 长方形测试通过
rect = Rectangle(5, 5)
test_resize(rect)  # ✅

# 正方形测试崩溃
square = Square(5, 5)
test_resize(square)  # ❌ AssertionError: 200 != 50 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

LSP 违规分析:

  1. 父类契约:set_width() 只修改宽度

  2. 子类行为:set_width() 同时修改高度 → 违反前置条件

  3. 导致结果:通用函数test_resize()对子类产生意外行为

# 三、LSP 的三大核心契约

# 1. 前置条件 (Preconditions)

子类方法不能强化输入条件(参数限制更严格)


# 父类契约:接受任意正整数
class PaymentService:
    def pay(self, amount: int):
        assert amount > 0, "金额需>0"
        ...

# ✅ 合法子类:前置条件更宽松(实际仍满足父类条件)
class DiscountPayment(PaymentService):
    def pay(self, amount: int):  # 不检查amount>0 → 仍满足父类条件
        ...

# ❌ 非法子类:强化前置条件
class StrictPayment(PaymentService):
    def pay(self, amount: int):
        assert amount > 100, "金额需>100"  # 强化条件 → 违反LSP
        super().pay(amount)
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2. 后置条件 (Postconditions)

子类方法不能弱化输出保证(返回值/状态变更)


class Database:
    def save(self, data) -> bool:
        # 契约:返回True表示保存成功
        ...

# ✅ 合法子类:后置条件更强(增加日志但仍返回bool)
class LoggingDatabase(Database):
    def save(self, data) -> bool:
        result = super().save(data)
        log_result(result)  # 增强行为
        return result       # 仍满足返回bool

# ❌ 非法子类:弱化后置条件
class UnreliableDatabase(Database):
    def save(self, data) -> bool:
        if random.random() > 0.5:
            return False   # 随机失败 → 破坏"保存成功"的契约
        return super().save(data)
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3. 不变量 (Invariants)

子类必须保持父类定义的约束条件


class BankAccount:
    def __init__(self):
        self.balance = 0  # 不变量:balance >= 0
    
    def withdraw(self, amount):
        if self.balance - amount < 0:
            raise ValueError("余额不足")
        self.balance -= amount

# ✅ 合法子类:维持余额>=0
class SavingsAccount(BankAccount):
    def withdraw(self, amount):
        if self.balance - amount < 100:  # 增加限制但仍满足>=0
            raise ValueError("需保留至少100元")
        super().withdraw(amount)

# ❌ 非法子类:破坏不变量
class OverdraftAccount(BankAccount):
    def withdraw(self, amount):
        self.balance -= amount  # 允许负余额 → 破坏balance>=0!
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 四、正确实践:飞行器控制示例


class Aircraft:
    def take_off(self):
        """契约:1. 启动引擎 2. 速度>200km/h 3. 返回爬升高度"""
        self._start_engines()
        self._accelerate_to(250)
        return self._climb()
    
    def _start_engines(self): ...
    def _accelerate_to(self, speed): ...
    def _climb(self) -> int: ...

# ✅ 合法子类:增强功能但不破坏契约
class Jet(Aircraft):
    def _climb(self) -> int:
        # 更快爬升 → 满足返回高度的后置条件
        return super()._climb() * 2  

# ✅ 合法子类:放宽加速限制(前置条件更弱)
class Glider(Aircraft):
    def _accelerate_to(self, speed):
        # 滑翔机只需150km/h → 仍满足父类速度>200的要求
        if speed > 150:  
            super()._accelerate_to(speed)

# ❌ 非法子类示例(假设存在)
class BrokenAircraft(Aircraft):
    def take_off(self):
        self._start_engines()
        # 忘记加速 → 违反"速度>200"的隐含契约
        return 0  # 返回高度0 → 违反返回爬升高度的后置条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 五、LSP 的终极理解:可替换性是行为契约

提问 答案
子类 run()必须和父类相同? 否!子类可优化算法(如更快的排序实现)
何时允许修改方法逻辑? 当满足:1. 接受更宽泛的输入
2. 返回更精确的结果
3. 不破坏父类约束
如何验证 LSP? 编写适用于父类的单元测试 → 所有子类必须能通过该测试

# 六、LSP 与设计模式的关联

设计模式 LSP 应用场景 收益
模板方法 子类重写钩子方法但不改变算法骨架 保证流程契约一致性
策略模式 不同策略实现同一接口 → 互为 LSP 安全替换算法策略
组合模式 叶子节点与复合节点实现相同接口 统一处理树形结构

# 总结:LSP 的实践精髓

  1. “可替换” ≠ “完全一致”
    子类可以:

    • 优化性能(如更快的run()实现)

    • 增加功能(如添加日志记录)

    • 放宽输入限制(如接受更多数据类型)

  2. 禁止行为:

    • 修改父类禁止的状态(如银行账户负余额)

    • 抛出父类未声明的异常类型

    • 返回不符合父类预期的结果类型

  3. 黄金准则:

    当你在子类中重写方法时,假装自己是父类
    ——你的行为必须让调用者无法分辨它面对的是父类还是子类

调用方只需依赖父类契约,任何子类实例都应如父类般工作。这才是“完全替换”的真谛!

# 4. ISP:接口隔离原则

误区:认为"接口越小越好"
正解:按客户端需求拆分臃肿接口

# 违反ISP:多功能接口强迫实现不需要的方法
class Worker(ABC):
    @abstractmethod
    def work(self): ...
    @abstractmethod
    def eat(self): ...  # 机器人不需要此方法

class HumanWorker(Worker):
    def work(self): ...
    def eat(self): ...

class RobotWorker(Worker):
    def work(self): ...
    def eat(self): ...  # 被迫实现无用方法

# 遵循ISP:拆分接口
class Workable(ABC):
    @abstractmethod
    def work(self): ...

class Eatable(ABC):
    @abstractmethod
    def eat(self): ...

class HumanWorker(Workable, Eatable): ...
class RobotWorker(Workable): ...  # 无需实现eat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 5. DIP:依赖倒置原则

误区:认为"DI(依赖注入)就是 DIP"
正解:高层模块应依赖抽象,而非具体实现

# 违反DIP:高层模块直接依赖低层细节
class LightBulb:
    def turn_on(self): ...
    def turn_off(self): ...

class Switch:
    def __init__(self, bulb: LightBulb):  # 依赖具体类
        self.bulb = bulb
    def operate(self): ...

# 遵循DIP:通过抽象解耦
class Switchable(ABC):  # 抽象接口
    @abstractmethod
    def turn_on(self): ...
    @abstractmethod
    def turn_off(self): ...

class LightBulb(Switchable): ...  # 低层实现抽象
class Fan(Switchable): ...        # 新增设备不影响Switch

class Switch:
    def __init__(self, device: Switchable):  # 依赖抽象
        self.device = device
    def operate(self): ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 一、传统分层架构:高层依赖低层

代码实现:


# 低层模块:具体实现
class MySQLDatabase:
    def save(self, data):
        print("保存到MySQL数据库")

class FileLogger:
    def log(self, message):
        print("日志写入文件")

class SmtpEmailSender:
    def send(self, email):
        print("通过SMTP发送邮件")

# 高层模块:直接依赖具体实现
class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()      # 直接依赖MySQL
        self.logger = FileLogger()     # 直接依赖文件日志
        self.email = SmtpEmailSender() # 直接依赖SMTP
    
    def place_order(self, order):
        self.db.save(order)
        self.logger.log("订单创建")
        self.email.send(order.confirmation)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

问题分析:

  1. 数据库更换灾难:改用 MongoDB?需修改所有OrderService代码

  2. 日志系统升级:改用 ELK?需修改业务逻辑

  3. 邮件服务切换:改用 SendGrid?业务类必须调整

  4. 测试困难:无法 Mock 数据库和邮件服务


# 二、依赖倒置架构:共同依赖抽象

代码重构:

# 步骤 1:定义抽象接口


from abc import ABC, abstractmethod

# 抽象存储接口
class DataRepository(ABC):
    @abstractmethod
    def save(self, data): pass

# 抽象日志接口
class Logger(ABC):
    @abstractmethod
    def log(self, message): pass

# 抽象通知接口
class Notifier(ABC):
    @abstractmethod
    def send(self, message): pass
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 步骤 2:实现具体低层模块


# 数据库实现
class MySQLRepository(DataRepository):
    def save(self, data):
        print("保存到MySQL数据库")

class MongoDBRepository(DataRepository):
    def save(self, data):
        print("保存到MongoDB数据库")

# 日志实现
class FileLogger(Logger):
    def log(self, message):
        print("文件日志:", message)

class CloudLogger(Logger):
    def log(self, message):
        print("云日志:", message)

# 通知实现
class SmtpNotifier(Notifier):
    def send(self, message):
        print("SMTP发送:", message)

class ApiNotifier(Notifier):
    def send(self, message):
        print("API发送:", message)
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 步骤 3:高层模块依赖抽象


class OrderService:
    # 通过构造函数注入抽象依赖
    def __init__(self, 
                 repository: DataRepository, 
                 logger: Logger, 
                 notifier: Notifier):
        self.repository = repository
        self.logger = logger
        self.notifier = notifier
    
    def place_order(self, order):
        self.repository.save(order)    # 依赖抽象
        self.logger.log("订单创建")    # 依赖抽象
        self.notifier.send("确认邮件") # 依赖抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 三、依赖倒置的威力展示

# 场景 1:无缝切换数据库

# 使用MySQL
mysql_service = OrderService(
    MySQLRepository(),
    FileLogger(),
    SmtpNotifier()
)

# 切换为MongoDB(业务代码零修改)
mongo_service = OrderService(
    MongoDBRepository(),  # 仅更换实现
    FileLogger(),
    SmtpNotifier()
)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 场景 2:动态组合服务


# 生产环境:MySQL + 云日志 + API邮件
prod_service = OrderService(
    MySQLRepository(),
    CloudLogger(),
    ApiNotifier()
)

# 测试环境:模拟存储 + 控制台日志
class MockRepository(DataRepository):
    def save(self, data): print("测试存储")

class ConsoleLogger(Logger):
    def log(self, msg): print("控制台:", msg)

test_service = OrderService(
    MockRepository(),
  
* * *  ConsoleLogger(),
    None  # 测试不需要邮件
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 场景 3:扩展新功能


# 新增Redis缓存实现
class RedisRepository(DataRepository):
    def save(self, data): 
        print("保存到Redis")

# 业务层无需任何修改
redis_service = OrderService(
    RedisRepository(),
    CloudLogger(),
    ApiNotifier()
)
1
2
3
4
5
6
7
8
9
10
11
12

# 四、DIP 的三大实现机制

# 1. 依赖注入 (Dependency Injection)

# 通过构造函数注入
service = OrderService(repo, logger, notifier)

# 通过属性注入
service.repository = MongoDBRepository()
1
2
3
4
5

# 2. 依赖查找 (Dependency Lookup)

class ServiceFactory:
    @staticmethod
    def create_repository():
        return MongoDBRepository() if config.use_mongo else MySQLRepository()

# 高层模块获取依赖
repo = ServiceFactory.create_repository()
1
2
3
4
5
6
7

# 3. 服务定位器 (Service Locator)


class ServiceLocator:
    _services = {}
    
    @classmethod
    def register(cls, interface, impl):
        cls._services[interface] = impl
        
    @classmethod
    def resolve(cls, interface):
        return cls._services[interface]

# 配置依赖
ServiceLocator.register(DataRepository, MongoDBRepository)
ServiceLocator.register(Logger, CloudLogger)

# 高层模块使用
class OrderService:
    def __init__(self):
        self.repo = ServiceLocator.resolve(DataRepository)
        self.logger = ServiceLocator.resolve(Logger)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 五、DIP 的实践收益

优势 具体表现
技术无关性 业务逻辑不依赖具体技术栈(可随时更换数据库)
并行开发 团队可同时开发高层模块和低层实现
测试友好 轻松注入 Mock 对象进行单元测试
系统扩展性 新增功能只需扩展抽象接口的实现类
部署灵活性 不同环境使用不同实现(开发/测试/生产)

# 六、依赖倒置 vs 依赖注入

概念 描述 关系
依赖倒置 (DIP) 设计原则:高层/低层都依赖抽象 目标
依赖注入 (DI) 实现技术:将依赖项从外部传入 手段
控制反转 (IoC) 设计模式:将控制权交给容器管理 框架支持

# 七、Python 特有实践技巧

# 1. 鸭子类型实现抽象


# 不显式定义接口,依靠鸭子类型
class OrderService:
    def __init__(self, repository, logger, notifier):
        self.repository = repository  # 只要实现save()
        self.logger = logger          # 只要实现log()
        self.notifier = notifier      # 只要实现send()

# 任意实现类
class CustomStorage:
    def save(self, data): ...  # 不需要继承特定接口
    
1
2
3
4
5
6
7
8
9
10
11
12

# 2. 使用 Protocol 定义隐式接口

from typing import Protocol

class DataRepository(Protocol):
    def save(self, data) -> None: ...

# 类型检查会验证实现类
def validate_repo(repo: DataRepository): ...
1
2
3
4
5
6
7

# 3. 依赖注入框架示例

# 使用injector库
from injector import inject, Module, provider

class DatabaseModule(Module):
    @provider
    def provide_repository(self) -> DataRepository:
        return MongoDBRepository()

class OrderService:
    @inject
    def __init__(self, repo: DataRepository):
        self.repo = repo
1
2
3
4
5
6
7
8
9
10
11
12

# 终极总结:依赖倒置的本质

实践箴言:

当你的业务类中出现 import pymysql 或 from redis import Redis 时,
这就是违反 DIP 的红色警报!
应该依赖 from .interfaces import DataRepository 这样的抽象。

通过依赖倒置,您的核心业务代码将成为永恒不变的资产,而技术实现层则是可随时更换的插件。

# 面试常见问题

  1. SRP vs 内聚性
    SRP 强调职责分离,高内聚强调类内元素相关性

  2. OCP 实现方式
    策略模式/模板方法/依赖注入

  3. LSP 关键点
    子类不强化前置条件、不弱化后置条件、保持不变量

  4. ISP 与胖接口
    避免"接口污染",减少客户端依赖冗余

  5. DIP vs DI
    DI 是 DIP 的实现手段之一,核心是抽象解耦

💡 面试技巧:结合项目经验说明原则应用,如:"在 XX 项目中,通过 DIP+DI 解耦了支付模块,使新增支付方式无需修改核心代码"

编辑 (opens new window)
#OOP#SOLID#面向对象
上次更新: 2025-06-06, 02:48:08
面向对象之封装、继承、多态
关于 python 中的 setup.py

← 面向对象之封装、继承、多态 关于 python 中的 setup.py→

最近更新
01
Flask 运行周期及工作原理
06-05
02
支付系统策略模式实现代码
06-04
03
Python 中 OOM(内存泄漏)问题的定位与分析
05-30
更多文章>
Theme by Vdoing | Copyright © 2019-2025 IMOYAO | 别院牧志
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式