深入理解 OOP 的 SOLID 原则
# SOLID 原则概述
SOLID 是面向对象编程(OOP)的五大设计原则,由 Robert C. Martin 提出,旨在提高代码的可维护性、扩展性和复用性:
SRP:单一职责原则
一个类只应有一个引起变化的原因OCP:开闭原则
软件实体应对扩展开放,对修改关闭LSP:里氏替换原则
子类必须能够替换其基类ISP:接口隔离原则
不应强迫客户端依赖它们不用的接口客户端程序不应该依赖它不需要的接口方法,一个接口所提供的方法,应该就是调用方所需要的方法
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
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 的关键测试
用这两个问题检验类的设计:
变更触发测试
“修改 XXX 功能时,是否必须修改这个类?”
✅ 合法:修改用户密码策略时只需改User
类
❌ 违规:修改邮件模板时被迫改User
类职责描述测试
能否用一个业务名词描述类的职责?
✅ 合法: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): # 通知职责
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): ... # 通知
2
3
4
5
6
7
8
修复方案:
class OrderValidator: ... # 校验职责
class TaxCalculator: ... # 计算职责
class OrderRepository: ... # 存储职责
class ReceiptPrinter: ... # 打印职责
2
3
4
5
# 场景 2:混合业务与技术细节
# 违反SRP:业务逻辑和技术实现耦合
class ReportGenerator:
def fetch_data(self):
# 直接调用SQL查询 ❌
def format_html(self): ... # 展示逻辑
2
3
4
5
6
7
8
修复方案:
class ReportDataFetcher: ... # 数据获取职责
class HtmlReportRenderer: ... # 渲染职责
2
3
4
# 五、SRP 的黄金实践准则
业务逻辑聚焦
类的所有方法应服务于同一个业务目标(如“订单创建”、“支付处理”)技术隔离
数据库操作、网络请求、日志记录等技术细节应封装到独立类变更隔离
当以下任一变化发生时,不应影响同一个类:业务规则变化 vs 技术实现变化
前端展示变化 vs 后端计算变化
数据来源变化 vs 数据处理逻辑变化
依赖方向
高层业务类依赖抽象(接口),而非具体技术实现类
# 六、为什么 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() # 新增形状不影响此方法
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分支
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
2
3
4
5
6
7
8
# 步骤 2:实现具体支付策略
class AlipayStrategy(PaymentStrategy):
def execute(self):
print("支付宝支付逻辑")
class WechatPayStrategy(PaymentStrategy):
def execute(self):
print("微信支付逻辑")
2
3
4
5
6
7
8
# 步骤 3:核心处理器依赖抽象
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy): # 依赖抽象而非具体实现
self.strategy = strategy
def process(self):
self.strategy.execute() # 核心逻辑永不修改
2
3
4
5
6
7
8
# 步骤 4:扩展新支付方式
# ✅ 新增银联支付:无需修改PaymentProcessor
class UnionPayStrategy(PaymentStrategy):
def execute(self):
print("银联支付逻辑")
# 使用新支付
processor = PaymentProcessor(UnionPayStrategy())
processor.process() # 安全扩展
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()) # ✅ 无缝扩展
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) # ✅ 函数即策略
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 落地流程
隔离变化点 → 将支付方式抽象为接口
封装行为 → 每种支付作为独立策略类
委托执行 → 上下文类调用策略接口
扩展功能 → 新增策略类而非修改上下文
# 三、对比: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}")
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()) # [缓存] 原始数据
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() # 核心执行逻辑稳定
2
3
4
5
6
7
8
9
10
11
12
# 五、关键结论:OCP 与策略模式的关系
策略模式是 OCP 的“金牌实践者”
- 当变化点是算法或策略时(如支付/排序方式),优先选择策略模式
OCP 的实现不唯一
根据场景选择合适模式:
行为扩展 → 策略模式
事件响应 → 观察者模式
功能叠加 → 装饰器模式
系统扩展 → 插件架构
违反 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
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(行为不一致)
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
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 违规分析:
父类契约:
set_width()
只修改宽度子类行为:
set_width()
同时修改高度 → 违反前置条件导致结果:通用函数
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)
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)
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!
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 → 违反返回爬升高度的后置条件
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 的实践精髓
“可替换” ≠ “完全一致”
子类可以:优化性能(如更快的
run()
实现)增加功能(如添加日志记录)
放宽输入限制(如接受更多数据类型)
禁止行为:
修改父类禁止的状态(如银行账户负余额)
抛出父类未声明的异常类型
返回不符合父类预期的结果类型
黄金准则:
当你在子类中重写方法时,假装自己是父类
——你的行为必须让调用者无法分辨它面对的是父类还是子类
调用方只需依赖父类契约,任何子类实例都应如父类般工作。这才是“完全替换”的真谛!
# 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
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): ...
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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
问题分析:
数据库更换灾难:改用 MongoDB?需修改所有
OrderService
代码日志系统升级:改用 ELK?需修改业务逻辑
邮件服务切换:改用 SendGrid?业务类必须调整
测试困难:无法 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
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)
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("确认邮件") # 依赖抽象
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()
)
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 # 测试不需要邮件
)
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()
)
2
3
4
5
6
7
8
9
10
11
12
# 四、DIP 的三大实现机制
# 1. 依赖注入 (Dependency Injection)
# 通过构造函数注入
service = OrderService(repo, logger, notifier)
# 通过属性注入
service.repository = MongoDBRepository()
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()
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)
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): ... # 不需要继承特定接口
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): ...
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
2
3
4
5
6
7
8
9
10
11
12
# 终极总结:依赖倒置的本质
实践箴言:
当你的业务类中出现
import pymysql
或from redis import Redis
时,
这就是违反 DIP 的红色警报!
应该依赖from .interfaces import DataRepository
这样的抽象。
通过依赖倒置,您的核心业务代码将成为永恒不变的资产,而技术实现层则是可随时更换的插件。
# 面试常见问题
SRP vs 内聚性
SRP 强调职责分离,高内聚强调类内元素相关性OCP 实现方式
策略模式/模板方法/依赖注入LSP 关键点
子类不强化前置条件、不弱化后置条件、保持不变量ISP 与胖接口
避免"接口污染",减少客户端依赖冗余DIP vs DI
DI 是 DIP 的实现手段之一,核心是抽象解耦
💡 面试技巧:结合项目经验说明原则应用,如:"在 XX 项目中,通过 DIP+DI 解耦了支付模块,使新增支付方式无需修改核心代码"