439_SOLID原则详解¶
一句话说明¶
SOLID 是面向对象设计的五大原则,记住这五个字母,写出来的代码更容易修改、扩展和测试,是面试必考设计题的答题框架。
核心知识点¶
白话理解¶
SOLID 就像建房子的五条规矩: - S 一个工人只干一种活(单一职责) - O 加新功能不改旧代码(开闭) - L 子类能完全替换父类(里氏替换) - I 不强迫别人实现用不上的接口(接口隔离) - D 依赖抽象不依赖具体(依赖倒置)
五大原则¶
| 字母 | 原则 | 核心思想 |
|---|---|---|
| S | Single Responsibility | 一个类只有一个改变的理由 |
| O | Open/Closed | 对扩展开放,对修改关闭 |
| L | Liskov Substitution | 子类可以替换父类而不破坏程序 |
| I | Interface Segregation | 接口要小而精,不塞用不到的方法 |
| D | Dependency Inversion | 高层依赖抽象,不依赖低层实现 |
经典题目与解法¶
题目1:违反 SRP 的代码识别与修复¶
题意:以下 User 类同时处理用户数据、邮件发送、数据存储,违反 SRP,请重构。
# ===== 违反SRP =====
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def get_user_info(self):
return f"{self.name}: {self.email}" # 数据展示逻辑
def send_email(self, message):
print(f"发送邮件到 {self.email}: {message}") # 邮件逻辑
def save_to_db(self):
print(f"保存 {self.name} 到数据库") # 持久化逻辑
# ===== 遵守SRP:每个类只负责一件事 =====
class User:
"""只负责用户数据的承载"""
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def __repr__(self):
return f"User({self.name}, {self.email})" # 数据展示
class EmailService:
"""只负责邮件发送"""
def send(self, user: User, message: str):
print(f"发送邮件到 {user.email}: {message}")
class UserRepository:
"""只负责用户数据持久化"""
def save(self, user: User):
print(f"保存 {user.name} 到数据库")
def find_by_name(self, name: str):
print(f"从数据库查询 {name}")
# 使用:各司其职
user = User("张三", "zhangsan@example.com")
email_service = EmailService()
user_repo = UserRepository()
email_service.send(user, "欢迎注册!") # 发邮件
user_repo.save(user) # 存库
题目2:用依赖倒置原则(DIP)解耦数据库¶
题意:业务逻辑直接依赖 MySQL 实现,切换数据库很麻烦,用 DIP 改造。
from abc import ABC, abstractmethod
# ===== 抽象层(高层和低层都依赖它)=====
class UserRepositoryInterface(ABC):
"""用户仓储抽象接口——这是"约定",不是实现"""
@abstractmethod
def save(self, user_id: int, name: str) -> bool:
pass
@abstractmethod
def find(self, user_id: int) -> dict:
pass
# ===== 低层:具体实现(可替换)=====
class MySQLUserRepository(UserRepositoryInterface):
"""MySQL具体实现"""
def save(self, user_id: int, name: str) -> bool:
print(f"[MySQL] 保存用户 {user_id}: {name}")
return True
def find(self, user_id: int) -> dict:
print(f"[MySQL] 查询用户 {user_id}")
return {'id': user_id, 'name': '张三'}
class RedisUserRepository(UserRepositoryInterface):
"""Redis缓存实现(切换数据库只需换这个类)"""
def save(self, user_id: int, name: str) -> bool:
print(f"[Redis] 缓存用户 {user_id}: {name}")
return True
def find(self, user_id: int) -> dict:
print(f"[Redis] 从缓存读取用户 {user_id}")
return {'id': user_id, 'name': '李四(缓存)'}
# ===== 高层:业务逻辑(依赖抽象,不管底层是什么)=====
class UserService:
def __init__(self, repo: UserRepositoryInterface):
"""依赖注入:接收抽象接口,不关心具体实现"""
self.repo = repo
def register_user(self, user_id: int, name: str):
"""注册用户:不知道也不关心底层用什么数据库"""
if self.repo.save(user_id, name):
print(f"用户 {name} 注册成功")
def get_user(self, user_id: int):
return self.repo.find(user_id)
# 测试:切换实现无需改业务代码
mysql_repo = MySQLUserRepository()
redis_repo = RedisUserRepository()
service1 = UserService(mysql_repo) # 使用MySQL
service1.register_user(1, "张三")
service2 = UserService(redis_repo) # 切换Redis,业务代码不变
service2.register_user(2, "李四")
时间复杂度:O(1) 接口查找
设计收益:业务代码与存储实现完全解耦,单元测试可以注入 Mock
面试技巧¶
- 举例胜过背定义:说"SRP就是一个类只有一个改变的理由"不够,要举出具体例子
- OCP 和策略模式绑定:开闭原则通常用策略模式/注册表实现
- LSP 常见违反:子类重写父类方法后抛出父类不抛的异常,就违反了 LSP
- DIP = 依赖注入的理论基础:面试问依赖注入时,先聊 DIP 原则
- 不要生搬硬套:SOLID 是指导原则,不是铁律,过度设计比不设计更糟
速查表¶
| 原则 | 违反迹象 | 解决方案 |
|---|---|---|
| SRP | 类改动原因有多个 | 按职责拆分类 |
| OCP | 加功能要改已有代码 | 策略模式/注册表 |
| LSP | 子类方法比父类限制多 | 重新设计继承关系 |
| ISP | 实现接口只用到部分方法 | 拆分细粒度接口 |
| DIP | 高层模块 import 低层模块 | 依赖注入+抽象接口 |