01-设计原则¶
学习时间: 约3-4小时 难度级别: ⭐⭐ 初中级 前置知识: 面向对象编程基础(类、继承、多态、封装) 学习目标: 深入理解SOLID五大原则和其他核心设计原则,为学习设计模式打下坚实基础
🎯 学习目标¶
- 理解并能举例说明SOLID五大原则
- 掌握DRY、KISS、YAGNI、LoD等实用原则
- 理解"组合优于继承"的设计思想
- 理解"针对接口编程"的含义
- 能识别代码中违反设计原则的"坏味道"
目录¶
- 1. SOLID原则概述
- 2. 单一职责原则 (SRP)
- 3. 开闭原则 (OCP)
- 4. 里氏替换原则 (LSP)
- 5. 接口隔离原则 (ISP)
- 6. 依赖倒置原则 (DIP)
- 7. 其他重要原则
- 8. 组合优于继承
- 9. 针对接口编程
- 10. 练习与自我检查
1. SOLID原则概述¶
SOLID是由Robert C. Martin(Uncle Bob)提出的五个面向对象设计原则的首字母缩写:
| 字母 | 原则 | 核心思想 |
|---|---|---|
| S | Single Responsibility Principle | 一个类只做一件事 |
| O | Open/Closed Principle | 对扩展开放,对修改关闭 |
| L | Liskov Substitution Principle | 子类可以替换父类 |
| I | Interface Segregation Principle | 接口要精简专一 |
| D | Dependency Inversion Principle | 依赖抽象,不依赖具体 |
这五个原则不是孤立的,它们相互支持、互为补充。遵循SOLID原则的代码通常具有高内聚、低耦合的特点,易于维护和扩展。
2. 单一职责原则 (SRP)¶
定义¶
一个类应该只有一个引起它变化的原因。
换句话说,每个类应该只负责一个功能领域中的工作。
❌ 坏例子¶
class Employee:
"""一个类承担了太多职责"""
def __init__(self, name, salary):
self.name = name
self.salary = salary
def calculate_pay(self):
"""计算薪资 — 财务部门的职责"""
return self.salary * 1.1
def save_to_database(self):
"""保存到数据库 — IT部门的职责"""
db.execute(f"INSERT INTO employees VALUES ('{self.name}', {self.salary})")
def generate_report(self):
"""生成报告 — HR部门的职责"""
return f"Employee Report: {self.name}, Salary: {self.salary}"
问题:如果数据库架构变了,要修改 Employee 类;如果报告格式变了,还要修改这个类。三个不同的变化原因影响同一个类。
✅ 好例子¶
class Employee:
"""只负责员工数据"""
def __init__(self, name, salary):
self.name = name
self.salary = salary
class PayCalculator:
"""只负责薪资计算"""
def calculate_pay(self, employee: Employee):
return employee.salary * 1.1
class EmployeeRepository:
"""只负责数据持久化"""
def save(self, employee: Employee):
db.execute(f"INSERT INTO employees VALUES ('{employee.name}', {employee.salary})")
class ReportGenerator:
"""只负责报告生成"""
def generate(self, employee: Employee):
return f"Employee Report: {employee.name}, Salary: {employee.salary}"
改进:每个类只有一个变化原因,修改一个不会影响其他。
Java示例¶
// 好的设计:每个类单一职责
public class Employee {
private String name;
private double salary;
// getter/setter
}
public class PayCalculator {
public double calculatePay(Employee employee) {
return employee.getSalary() * 1.1;
}
}
public class EmployeeRepository {
public void save(Employee employee) {
// 数据库操作
}
}
3. 开闭原则 (OCP)¶
定义¶
软件实体应该对扩展开放,对修改关闭。
添加新功能时,应该通过增加新代码来实现,而不是修改已有代码。
❌ 坏例子¶
class DiscountCalculator:
def calculate(self, customer_type, amount):
"""每增加一种客户类型,就要修改这个方法"""
if customer_type == "regular":
return amount * 0.95
elif customer_type == "vip":
return amount * 0.8
elif customer_type == "super_vip":
return amount * 0.7
# 新增类型?继续加 elif...
else:
return amount
✅ 好例子¶
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, amount: float) -> float:
pass
class RegularDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.95
class VIPDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.80
class SuperVIPDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.70
# 新增客户类型?添加新类即可,不修改已有代码
class EmployeeDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.60
class DiscountCalculator:
def __init__(self, strategy: DiscountStrategy):
self.strategy = strategy
def calculate(self, amount):
return self.strategy.calculate(amount)
改进:新增折扣类型只需创建新类,无需修改 DiscountCalculator。这实际上就是策略模式的应用。
4. 里氏替换原则 (LSP)¶
定义¶
子类对象必须能替换父类对象,而程序的行为不受影响。
子类可以扩展父类功能,但不能改变父类原有的行为契约。
❌ 坏例子:经典的正方形/矩形问题¶
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property # @property将方法变为属性访问
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
def area(self):
return self._width * self._height
class Square(Rectangle):
"""正方形是矩形?数学上是,但OOD中不是!"""
@Rectangle.width.setter
def width(self, value):
self._width = value
self._height = value # 违反了矩形"宽高独立"的隐含契约
@Rectangle.height.setter
def height(self, value):
self._width = value
self._height = value
# 使用者期望矩形行为
def test_rectangle(rect: Rectangle):
rect.width = 5
rect.height = 4
assert rect.area() == 20 # 如果传入Square,面积是16!❌
✅ 好例子¶
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
# 任何Shape子类都可以安全替换
def print_area(shape: Shape):
print(f"Area: {shape.area()}") # 总是正确的 ✅
5. 接口隔离原则 (ISP)¶
定义¶
客户端不应该被迫依赖它不使用的方法。
大而全的接口应该拆分为多个小而专的接口。
❌ 坏例子¶
from abc import ABC, abstractmethod
class Worker(ABC):
"""一个臃肿的接口"""
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep_in_office(self): pass
class HumanWorker(Worker):
def work(self): print("Working...")
def eat(self): print("Eating lunch...")
def sleep_in_office(self): print("Taking a nap...")
class RobotWorker(Worker):
def work(self): print("Working efficiently...")
def eat(self): raise NotImplementedError("Robots don't eat!") # ❌ 被迫实现不需要的方法
def sleep_in_office(self): raise NotImplementedError("Robots don't sleep!")
✅ 好例子¶
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Sleepable(ABC):
@abstractmethod
def sleep_in_office(self): pass
class HumanWorker(Workable, Eatable, Sleepable):
def work(self): print("Working...")
def eat(self): print("Eating lunch...")
def sleep_in_office(self): print("Taking a nap...")
class RobotWorker(Workable): # 只实现需要的接口 ✅
def work(self): print("Working efficiently...")
Java示例¶
// 拆分接口
interface Workable {
void work();
}
interface Eatable {
void eat();
}
public class HumanWorker implements Workable, Eatable {
public void work() { System.out.println("Working..."); }
public void eat() { System.out.println("Eating..."); }
}
public class RobotWorker implements Workable {
public void work() { System.out.println("Working efficiently..."); }
}
6. 依赖倒置原则 (DIP)¶
定义¶
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
❌ 坏例子¶
class MySQLDatabase:
def query(self, sql):
return f"MySQL executing: {sql}"
class UserService:
def __init__(self):
self.db = MySQLDatabase() # ❌ 直接依赖具体实现
def get_user(self, user_id):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# 如果要换成PostgreSQL?必须修改UserService内部代码
✅ 好例子¶
from abc import ABC, abstractmethod
class Database(ABC):
"""抽象层:定义接口"""
@abstractmethod
def query(self, sql: str) -> str:
pass
class MySQLDatabase(Database):
def query(self, sql):
return f"MySQL: {sql}"
class PostgreSQLDatabase(Database):
def query(self, sql):
return f"PostgreSQL: {sql}"
class MongoDatabase(Database):
def query(self, sql):
return f"MongoDB: {sql}"
class UserService:
def __init__(self, db: Database): # ✅ 依赖抽象,通过构造函数注入
self.db = db
def get_user(self, user_id):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# 使用时注入具体实现
service = UserService(PostgreSQLDatabase()) # 切换数据库无需修改UserService
关键:控制反转(IoC)和依赖注入(DI)是实现DIP的常用手段。
Java示例¶
// 依赖抽象接口
public interface Database { // interface定义类型契约
String query(String sql);
}
public class MySQLDatabase implements Database { // extends继承;implements实现接口
public String query(String sql) {
return "MySQL: " + sql;
}
}
public class UserService {
private final Database db;
public UserService(Database db) { // 构造器注入
this.db = db;
}
public String getUser(int userId) {
return db.query("SELECT * FROM users WHERE id = " + userId);
}
}
7. 其他重要原则¶
7.1 DRY(Don't Repeat Yourself)¶
不要重复自己 — 每一个知识点在系统中只应有一个、明确的、权威的表示。
# ❌ 违反DRY:验证逻辑重复
def create_user(email):
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email):
raise ValueError("Invalid email")
# ...
def update_user(email):
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email): # 重复!
raise ValueError("Invalid email")
# ...
# ✅ 遵循DRY:提取通用函数
def validate_email(email):
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email):
raise ValueError("Invalid email")
def create_user(email):
validate_email(email)
# ...
7.2 KISS(Keep It Simple, Stupid)¶
保持简单 — 简单的设计比复杂的设计更容易理解、测试和维护。
# ❌ 过度设计:为了"灵活"写了一堆抽象
class AbstractStrategyFactoryProviderSingleton:
...
# ✅ KISS:简单直接地解决问题
def calculate_discount(price, discount_rate):
return price * (1 - discount_rate)
7.3 YAGNI(You Aren't Gonna Need It)¶
你不会需要它 — 不要为"将来可能"的需求提前编写代码。
# ❌ YAGNI违反:项目只需要JSON,但"以防万一"支持了5种格式
class DataExporter:
def export_json(self, data): ...
def export_xml(self, data): ... # 没人用
def export_csv(self, data): ... # 没人用
def export_yaml(self, data): ... # 没人用
def export_protobuf(self, data): ...# 没人用
# ✅ YAGNI:只实现当前需要的
class DataExporter:
def export_json(self, data): ...
# 将来需要其他格式时再加
7.4 迪米特法则 / 最少知识原则(Law of Demeter, LoD)¶
一个对象应该对其他对象有最少的了解。 只与直接朋友通信。
# ❌ 违反LoD:链式调用暴露了内部结构
customer.get_wallet().get_credit_card().charge(100)
# ✅ 遵循LoD:封装内部细节
customer.charge(100)
# Customer内部:
# def charge(self, amount):
# self.wallet.charge(amount)
8. 组合优于继承¶
8.1 为什么继承有问题¶
- 强耦合:子类与父类紧密绑定,父类改变可能破坏所有子类
- 脆弱基类:修改基类可能导致子类行为异常
- 单继承限制:大多数语言只支持单继承(Java),限制了灵活性
- 层次膨胀:功能组合导致继承层次爆炸
8.2 组合的优势¶
# ❌ 继承方式:功能组合导致类爆炸
class Dog: pass
class SwimmingDog(Dog): pass
class FlyingDog(Dog): pass # 真的有
class SwimmingFlyingDog(Dog): pass # 组合爆炸!
# ✅ 组合方式:通过组装能力来构建
class SwimAbility:
def swim(self):
print("Swimming!")
class FlyAbility:
def fly(self):
print("Flying!")
class BarkAbility:
def bark(self):
print("Woof!")
class Dog:
def __init__(self):
self.bark = BarkAbility()
self.swim = None
self.fly = None
def add_ability(self, ability_name, ability):
setattr(self, ability_name, ability) # hasattr/getattr/setattr动态操作对象属性
# 灵活组合
super_dog = Dog()
super_dog.add_ability('swim', SwimAbility())
super_dog.add_ability('fly', FlyAbility())
8.3 何时使用继承¶
继承并非完全不能用,以下场景仍适合: - "is-a"关系明确且稳定:Cat is an Animal - 模板方法模式:定义算法骨架 - 抽象基类:定义接口契约
经验法则:优先考虑组合,只在"is-a"关系明确时使用继承。
9. 针对接口编程¶
9.1 核心思想¶
面向接口(抽象)编程,而非面向实现编程。
from abc import ABC, abstractmethod # ABC抽象基类;abstractmethod强制子类实现
from typing import List
class Notifier(ABC):
"""接口:通知能力"""
@abstractmethod
def send(self, message: str): pass
class EmailNotifier(Notifier):
def send(self, message):
print(f"📧 Email: {message}")
class SMSNotifier(Notifier):
def send(self, message):
print(f"📱 SMS: {message}")
class SlackNotifier(Notifier):
def send(self, message):
print(f"💬 Slack: {message}")
class AlertService:
"""依赖接口,不依赖具体实现"""
def __init__(self, notifiers: List[Notifier]):
self.notifiers = notifiers
def alert(self, message):
for notifier in self.notifiers:
notifier.send(message)
# 灵活配置通知渠道
service = AlertService([EmailNotifier(), SlackNotifier()])
service.alert("Server is down!")
9.2 Python中的"接口"¶
Python没有Java那样的 interface 关键字,但有多种实现方式:
- 抽象基类(ABC):最正式的方式
- Protocol(Python 3.8+):结构化子类型(鸭子类型+类型检查)
- 鸭子类型:只要有需要的方法就行
from typing import Protocol
class Renderable(Protocol):
"""Protocol: 结构化子类型检查"""
def render(self) -> str: ...
class HTMLRenderer:
def render(self) -> str:
return "<html>...</html>"
class JSONRenderer:
def render(self) -> str:
return '{"key": "value"}'
def display(renderer: Renderable):
"""不需要任何继承关系,只要有render方法即可"""
print(renderer.render())
display(HTMLRenderer()) # ✅
display(JSONRenderer()) # ✅
10. 练习与自我检查¶
✏️ 练习题¶
- 识别违反:以下代码违反了哪些SOLID原则?如何重构?
class OrderService:
def create_order(self, items, user):
# 验证库存
for item in items:
if item.stock <= 0:
raise ValueError(f"{item.name} out of stock")
# 计算价格
total = sum(item.price * item.qty for item in items)
# 应用折扣
if user.is_vip:
total *= 0.8
# 保存到数据库
db.save_order(user.id, items, total)
# 发送邮件
smtp.send_email(user.email, f"Order confirmed: ${total}")
# 发送短信
sms.send(user.phone, f"Order confirmed: ${total}")
-
改造代码:将上面的代码重构为符合SOLID原则的设计(至少拆分为3个类)。
-
LSP判断:下面的继承关系合理吗?为什么?
Penguin extends Bird(企鹅是鸟)—— 如果Bird有fly()方法? > 不合理。企鹅不能飞,违反LSP——子类无法替代父类使用。解决:将fly()提取到Flyable接口,Bird不强制包含fly()。Stack extends ArrayList—— 栈"是一个"列表吗? > 不合理。栈只允许LIFO操作,但继承ArrayList会暴露get(i)/add(i,e)等随机访问方法,违反LSP。应该用组合:Stack内部持有List,只暴露push/pop/peek。-
FileLogger extends Logger——Logger接口有log(message)方法 > 合理。FileLogger可以完全实现Logger的log()契约,调用方无需知道具体实现,符合LSP。 -
组合重构:将以下继承层次重构为组合方式:
Vehicle
├── ElectricCar
├── GasCar
├── ElectricSelfDrivingCar
├── GasSelfDrivingCar
└── HybridSelfDrivingCar (混合动力+自动驾驶)
- SOLID选择题:对于以下场景,应该应用哪条原则?
- 一个类同时处理用户认证和日志记录 → SRP(单一职责原则)——拆分为AuthService和LogService
- 更换短信服务商需要修改业务逻辑代码 → DIP(依赖倒置原则)——业务代码依赖SMSService接口,而非具体供应商实现
- 某个接口有15个方法,但大多数实现者只用其中3个 → ISP(接口隔离原则)——拆分为多个小接口,实现者只需实现需要的
面试要点¶
Q1: 解释SOLID原则中的"O"? A: 开闭原则——对扩展开放(可以添加新功能),对修改关闭(不需要改已有代码)。通过抽象和多态实现,如策略模式用接口定义行为,新增策略只需添加新类。
Q2: 组合优于继承是什么意思? A: 优先使用对象组合(has-a)而非类继承(is-a)来实现代码复用。继承会导致强耦合和类层次膨胀,组合更灵活——可以在运行时动态组装功能。
Q3: 依赖倒置和依赖注入的关系? A: 依赖倒置是原则(高层不应依赖低层,两者都依赖抽象),依赖注入是实现手段(通过构造函数/setter/接口将依赖从外部传入,而非内部创建)。
Q4: DRY原则的适用范围? A: DRY不仅指不要复制粘贴代码,更重要的是"知识不要重复"——同一个业务规则只在一处定义。但不要为了DRY而强行合并相似但实际无关的逻辑。
自我检查清单¶
- 能解释SOLID五个原则各自的含义
- 能识别代码中违反SOLID原则的问题
- 理解DRY、KISS、YAGNI的实际应用
- 理解为什么"组合优于继承"
- 知道Python中实现"接口"的多种方式
- 能区分"针对接口编程"和"针对实现编程"
下一章: 02-创建型模式 — 学习如何优雅地创建对象