面向对象设计并不是把数据和行为随便塞进类里就完事。真正健康的系统,应该让每个模块各司其职,模块之间尽量互不牵扯,也就是所谓的高内聚、低耦合。SOLID是五位学者从不同角度总结出的五条设计准则,它们恰好对应了实现这种目标的核心抓手。理解并运用这五条原则,可以让代码在需求频繁变动时依然保持清晰结构。

单一职责原则与高内聚的底层逻辑
单一职责原则要求一个类或者一个模块只因为一个理由发生变更。很多初学者会误以为“职责”是指功能数量少,其实它强调的是变更原因的单一性。当一个类既负责用户信息校验,又负责把数据写入数据库,还负责发送邮件通知,那么数据库表结构变动、邮件服务替换、校验规则调整都会引发它的修改,这种多因一果正是低内聚的典型症状。
从依赖关系看,这样的类会被表现层、持久层、消息层同时依赖,任何一层的变动都可能波及其他层,耦合度直线上升。把校验逻辑拆到UserValidator,持久化放到UserRepository,通知交给Notifier,每个类只响应自己那一类的需求变化,内聚自然提高。调用方依赖的是清晰的能力而非混杂的实现,系统局部修改的爆炸半径被有效控制。
下面示例展示违反与遵循该原则的对照。前者把三件不相干的事写在一个类,后者做了基础拆分:
// 违反单一职责:一个类做了校验、存储、通知
class UserManager {
void process(User u) {
if (u.name == null) throw new RuntimeException("name null");
// 存库
System.out.println("save " + u.name);
// 发邮件
System.out.println("send mail to " + u.email);
}
}
// 遵循单一职责:拆分职责
class UserValidator {
void validate(User u) { if (u.name == null) throw new RuntimeException("name null"); }
}
class UserRepository {
void save(User u) { System.out.println("save " + u.name); }
}
class EmailNotifier {
void notify(User u) { System.out.println("send mail to " + u.email); }
}
开闭原则与里氏替换如何隔离变更
开闭原则主张软件实体对扩展开放、对修改关闭。表面看是要求少改旧代码,实质是借助抽象层来容纳新行为。比如计算折扣,如果直接在Order里写一堆if判断会员等级,每次加新等级都要动原有方法,这既违反开闭也引入回归风险。更好的做法是抽象出DiscountStrategy接口,新等级只需新增实现类。
里氏替换原则规定子类必须能替换父类且不改变程序正确性。它是开闭能落地的保障:只有子类真正遵循父类的契约(比如不抛出父类未声明的异常、前置条件不收紧),上层才能放心依赖抽象而不关心具体子类。若子类偷偷改了平方根函数的语义,或者集合子类add方法变为忽略重复项却未声明,调用方基于父类写的逻辑就会出错,抽象也就失去了稳定支点。
以下代码演示通过策略接口实现开闭,且子类可安全替换:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calc(self, price: float) -> float:
pass
class NormalDiscount(DiscountStrategy):
def calc(self, price: float) -> float:
return price
class VipDiscount(DiscountStrategy):
def calc(self, price: float) -> float:
return price * 0.8
def checkout(strategy: DiscountStrategy, price: float) -> float:
# 里氏替换:任何DiscountStrategy子类都可传入
return strategy.calc(price)
接口隔离与依赖倒置对耦合的削减
接口隔离原则反对强迫调用方依赖用不到的方法。一个庞大的Worker接口若同时定义code()、test()、deploy(),那么只做开发的类也得空实现测试和部署,造成无谓的绑定。按角色拆成Coder、Tester、Deployer后,各类只实现自己关心的契约,依赖图更稀疏,修改其中一个接口不会影响无关实现。
依赖倒置原则进一步要求高层模块不依赖低层细节,二者都依赖抽象。传统写法中业务逻辑直接new MySQLOrderDao(),数据库换成PostgreSQL就要改业务类。引入OrderDao接口并由外部注入具体实现,业务层只认接口,底层更换或打桩测试都无需触碰核心代码。这两个原则配合,让模块间通过窄而稳定的抽象通信,而不是宽而易碎的具体类,耦合度显著下降。
下面用Java片段展示依赖倒置的注入方式:
interface OrderDao {
void insert(String order);
}
class MySQLOrderDao implements OrderDao {
public void insert(String order) { System.out.println("mysql " + order); }
}
class OrderService {
private OrderDao dao;
// 构造注入,依赖抽象而非具体
OrderService(OrderDao dao) { this.dao = dao; }
void create(String order) { dao.insert(order); }
}
把SOLID五原则串起来看,它们并不是孤立的教条。单一职责划清边界,接口隔离收窄边界间的通道,开闭与里氏替换为边界提供可替换的扩展点,依赖倒置把边界之间的联系固定为抽象契约。当这些准则在代码层面持续生效,系统自然会朝高内聚、低耦合的方向演化,后续维护与重构的代价也会随之降低。