Python OOP 设计原则与反模式
约 3038 字大约 10 分钟
2026-05-10
学会类、继承、封装、多态之后,并不等于真正会写面向对象代码。语法只是工具,设计才是关键。
- 好的 OOP 设计是什么
- 单一职责原则
- 开闭原则
- 依赖倒置原则
- 1拿一个 100 行的“上帝类”,按 SRP 拆 3-4 个职责单一的小类,体会“改一处不影响别处”。
- 2把 if user_type == 'vip' elif 'svip'... 改写成 DiscountPolicy 策略类家族,应用 OCP。
- 3对“UserService 直接 new EmailSender” 改成构造函数注入 notifier,体会依赖倒置带来的可测试性。
- 4试着把继承关系换成组合(self.logger = FileLogger())—— 大多数“为了复用方法”的继承都更适合组合。
- 5判断要不要再加一层抽象:问自己“是否已有 ≥2 个实现?未来真的会扩?”,答案否就别抽象。
学会类、继承、封装、多态之后,并不等于真正会写面向对象代码。语法只是工具,设计才是关键。
很多初学者刚学会 OOP 后,会出现一个常见问题:什么都想写成类,什么都想继承,最后代码层级越来越深,逻辑却越来越难懂。
这一章不讲新的语法,而是讨论 Python OOP 中常见的设计原则和反模式。
好的 OOP 设计是什么
好的面向对象设计不是“类越多越好”,也不是“继承越复杂越高级”。
好的设计通常有这些特点:
- 每个类有清晰职责
- 对外接口简单稳定
- 内部实现可以独立变化
- 类之间依赖关系清楚
- 修改一个功能时,不需要牵一发动全身
- 读代码时能快速理解对象之间的协作关系
面向对象的目标是管理复杂性,而不是制造复杂性。
单一职责原则
单一职责原则(Single Responsibility Principle)指的是:一个类应该只有一个引起它变化的原因。
通俗地说,一个类不要什么都管。
反例:
class UserService:
def create_user(self, name, email):
print('校验用户数据')
print('写入数据库')
print('发送欢迎邮件')
print('记录审计日志')这个 UserService 同时负责:
- 用户数据校验
- 数据持久化
- 邮件发送
- 审计日志
它的问题是:任何一个环节变化,都可能导致这个类变化。
可以拆分职责:
class UserValidator:
def validate(self, name, email):
if not name:
raise ValueError('用户名不能为空')
if '@' not in email:
raise ValueError('邮箱格式错误')
class UserRepository:
def save(self, user):
print(f'保存用户:{user}')
class EmailSender:
def send_welcome(self, email):
print(f'发送欢迎邮件到:{email}')
class UserService:
def __init__(self, validator, repository, email_sender):
self.validator = validator
self.repository = repository
self.email_sender = email_sender
def create_user(self, name, email):
self.validator.validate(name, email)
user = {'name': name, 'email': email}
self.repository.save(user)
self.email_sender.send_welcome(email)
return user拆分后,每个类的职责更清晰。UserService 负责协调流程,具体细节交给专门的对象。
开闭原则
开闭原则(Open-Closed Principle)指的是:软件实体应该对扩展开放,对修改关闭。
也就是说,新增功能时,尽量通过增加新代码来扩展,而不是频繁修改旧代码。
反例:
class DiscountCalculator:
def calculate(self, user_type, amount):
if user_type == 'normal':
return amount
if user_type == 'vip':
return amount * 0.9
if user_type == 'svip':
return amount * 0.8
return amount如果新增 student、employee、partner 等类型,就要不断修改 calculate()。
可以用多态改造:
class DiscountPolicy:
def calculate(self, amount):
return amount
class VipDiscountPolicy(DiscountPolicy):
def calculate(self, amount):
return amount * 0.9
class SVipDiscountPolicy(DiscountPolicy):
def calculate(self, amount):
return amount * 0.8
class Order:
def __init__(self, amount, discount_policy):
self.amount = amount
self.discount_policy = discount_policy
def final_amount(self):
return self.discount_policy.calculate(self.amount)新增折扣时,只需要增加新的策略类,而不必修改 Order 的计算逻辑。
class StudentDiscountPolicy(DiscountPolicy):
def calculate(self, amount):
return amount * 0.85依赖倒置原则
依赖倒置原则(Dependency Inversion Principle)指的是:高层模块不应该依赖低层模块,二者都应该依赖抽象。
简单来说,业务逻辑不要直接绑定具体实现。
反例:
class EmailSender:
def send(self, message):
print(f'发送邮件:{message}')
class OrderService:
def __init__(self):
self.sender = EmailSender()
def create_order(self):
print('创建订单')
self.sender.send('订单创建成功')OrderService 直接创建了 EmailSender,如果以后要改成短信、企业微信或站内信,就必须修改 OrderService。
更好的做法是把依赖从外部传进来:
class OrderService:
def __init__(self, notifier):
self.notifier = notifier
def create_order(self):
print('创建订单')
self.notifier.send('订单创建成功')
class EmailNotifier:
def send(self, message):
print(f'发送邮件:{message}')
class SmsNotifier:
def send(self, message):
print(f'发送短信:{message}')
order_service = OrderService(EmailNotifier())
order_service.create_order()这样 OrderService 只依赖“通知器”这个能力,不依赖具体的邮件实现。
在 Python 中,这种抽象既可以用 ABC 表达,也可以用 Protocol 表达,也可以在简单场景下只依赖鸭子类型。
多用组合,少用继承
继承表达的是 “is-a” 关系:狗是一种动物,管理员是一种用户。
组合表达的是 “has-a” 关系:订单有一个支付器,用户有一个地址,服务有一个仓储对象。
很多时候,组合比继承更灵活。
反例:
class FileLogger:
def log(self, message):
print(f'写入文件:{message}')
class UserService(FileLogger):
def create_user(self):
self.log('创建用户')UserService 继承 FileLogger 并不自然。用户服务“是一种”文件日志器吗?显然不是。它只是“需要一个”日志器。
更好的写法是组合:
class FileLogger:
def log(self, message):
print(f'写入文件:{message}')
class UserService:
def __init__(self, logger):
self.logger = logger
def create_user(self):
self.logger.log('创建用户')组合的好处是可以轻松替换依赖:
class ConsoleLogger:
def log(self, message):
print(f'控制台日志:{message}')
service = UserService(ConsoleLogger())不要过度抽象
抽象可以减少重复、隔离变化,但过度抽象会让代码变复杂。
反例:
class AbstractUserCreator:
def create(self):
pass
class BaseUserCreator(AbstractUserCreator):
def create(self):
pass
class DefaultUserCreator(BaseUserCreator):
def create(self):
print('创建用户')如果系统只有一种创建用户的方式,这些抽象层就没有意义。
更简单的代码可能更好:
class UserService:
def create_user(self):
print('创建用户')判断是否需要抽象,可以问自己几个问题:
- 是否已经有多个实现?
- 未来是否真的很可能新增实现?
- 调用方是否需要屏蔽具体实现?
- 抽象后是否让代码更容易理解?
如果答案都是否定的,就先不要抽象。
贫血模型与充血模型
贫血模型指的是对象只有数据,没有行为。所有业务逻辑都写在 Service 里。
class Account:
def __init__(self, balance):
self.balance = balance
class AccountService:
def withdraw(self, account, amount):
if amount <= 0:
raise ValueError('提现金额必须大于 0')
if account.balance < amount:
raise ValueError('余额不足')
account.balance -= amount这个写法不是一定错误,但如果所有规则都堆在 Service 里,模型对象就会变成单纯的数据袋。
可以把和账户强相关的行为放回对象里:
class Account:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError('提现金额必须大于 0')
if self.balance < amount:
raise ValueError('余额不足')
self.balance -= amount这样 Account 不只是保存余额,还负责维护自己的业务规则。
但也不要走向另一个极端:把数据库访问、网络请求、文件操作全部塞进模型对象。对象应该承担和自身状态密切相关的行为,而不是包揽所有外部协作。
类爆炸问题
类爆炸指的是为了追求“面向对象”,把简单逻辑拆成大量细碎的类。
例如一个简单的字符串清洗流程:
def clean_text(text):
return text.strip().lower().replace(' ', '-')如果强行设计成这样:
class StripProcessor:
def process(self, text):
return text.strip()
class LowerProcessor:
def process(self, text):
return text.lower()
class ReplaceProcessor:
def process(self, text):
return text.replace(' ', '-')并不一定更好。除非这些处理器确实需要独立配置、组合、扩展,否则一个函数就够了。
Python 是多范式语言。函数、模块、类都可以用,不必把所有问题都塞进 OOP。
什么时候不要用 OOP
以下场景不一定需要类:
- 只是一次性脚本
- 逻辑很短,没有复杂状态
- 只是纯数据转换
- 函数已经能清晰表达意图
- 类只是为了包装一个函数
例如:
def calculate_total(items):
return sum(item['price'] * item['quantity'] for item in items)这比下面这种写法更直接:
class TotalCalculator:
def calculate(self, items):
return sum(item['price'] * item['quantity'] for item in items)除非后续确实需要多个计算策略、依赖注入或复杂状态,否则函数更简单。
常见反模式
1. 上帝类
一个类什么都管:用户、订单、支付、日志、缓存、通知都在里面。
后果是难测试、难修改、难复用。
解决方式:按职责拆分。
2. 继承滥用
为了复用几个方法就使用继承,导致类层级越来越深。
解决方式:优先考虑组合。
3. 过早设计接口
系统只有一个实现,却先定义一堆抽象类、工厂类、管理器类。
解决方式:先写简单代码,等变化真的出现后再抽象。
4. 数据和行为完全分离
所有类只有属性,所有业务逻辑都堆在 Service 中。
解决方式:把和对象自身状态强相关的规则放回对象。
5. 滥用魔法方法
为了炫技重写大量魔法方法,让对象行为变得不可预测。
解决方式:只有当对象确实需要像容器、数字、上下文管理器那样使用时,才实现对应魔法方法。
一个小型重构示例
反例:
class OrderService:
def create_order(self, user, items, pay_type):
total = sum(item['price'] * item['quantity'] for item in items)
if pay_type == 'wechat':
print(f'微信支付 {total} 元')
elif pay_type == 'alipay':
print(f'支付宝支付 {total} 元')
else:
raise ValueError('不支持的支付方式')
print('保存订单')
print('发送通知')问题:
- 订单金额计算、支付、保存、通知混在一起
- 新增支付方式要修改
OrderService - 难以单独测试支付逻辑
改造:
class Order:
def __init__(self, user, items):
self.user = user
self.items = items
@property
def total(self):
return sum(item['price'] * item['quantity'] for item in self.items)
class WeChatPayment:
def pay(self, amount):
print(f'微信支付 {amount} 元')
class AliPayPayment:
def pay(self, amount):
print(f'支付宝支付 {amount} 元')
class OrderRepository:
def save(self, order):
print('保存订单')
class Notifier:
def send(self, user, message):
print(f'通知 {user}:{message}')
class OrderService:
def __init__(self, repository, notifier):
self.repository = repository
self.notifier = notifier
def create_order(self, user, items, payment):
order = Order(user, items)
payment.pay(order.total)
self.repository.save(order)
self.notifier.send(user, '订单创建成功')
return order改造后:
Order负责订单自身数据和金额计算- 支付类负责支付
OrderRepository负责保存Notifier负责通知OrderService负责协调流程
这就是“职责清晰 + 组合协作 + 多态扩展”。
总结
Python OOP 的重点不是把所有东西都写成类,而是合理地组织变化。
可以记住几条经验:
- 类应该有清晰职责。
- 优先组合,谨慎继承。
- 不要过早抽象。
- 简单函数能解决的问题,不必强行写类。
- 业务规则可以放进对象,但外部协作不要全塞进对象。
- 多态适合替代不断膨胀的
if-elif。 - 抽象应该来自真实重复和真实变化,而不是来自想象。
写 Python 面向对象代码时,最重要的问题不是“这个类怎么写”,而是“这个对象应该负责什么”。
- 类应有单一职责:一句话能说清这个类负责什么,否则就该拆。
- 对扩展开放、对修改关闭(OCP):新增分支就加新类,老代码不动 —— 多态替代 if/elif 是首选手段。
- 依赖倒置:业务对象依赖抽象(ABC/Protocol/鸭子类型),具体实现从外面注入,便于替换和测试。
- 多用组合(has-a),少用继承(is-a):继承超过两层就该警惕过度耦合。
- Python 是多范式语言:函数能解决的别强行写类;抽象层来自真实重复,而非想象中的扩展性。
版权所有
版权归属:Shuo Liu
