设计原则(SOLID):让代码经得起时间考验
聊到架构设计,SOLID 原则是绕不开的话题。我刚开始带团队那会儿,总觉得这些原则太抽象,不就是几个英文单词嘛。直到有一次,一个项目迭代到第三年,改一个需求要动十几个文件,我才真正明白——这些原则不是理论,是血泪教训换来的经验。
说白了,SOLID 就是五个让代码更好维护、更容易扩展的指导方针。咱们一个一个来看。
1. 单一职责原则(SRP)
一个类,只做一件事。听起来简单,对吧?
我在项目中遇到过这样的场景:一个 OrderService 类,里面既有订单计算逻辑,又有数据库操作,还夹杂着发送邮件通知。你想想看,这要是哪天邮件服务换了,改代码的人得多崩溃?
核心思想:一个类应该只有一个引起它变化的原因。
举个例子,咱们写个用户注册功能:
// ❌ 不好的写法:一个类干了三件事
class UserService {
public function register($data) {
// 验证数据
// 保存到数据库
// 发送欢迎邮件
}
}
// ✅ 好的写法:各司其职
class UserValidator {
public function validate($data) { /* ... */ }
}
class UserRepository {
public function save($user) { /* ... */ }
}
class EmailService {
public function sendWelcome($user) { /* ... */ }
}
嗯,这里要注意:判断职责是否单一,就看这个类有没有多个「修改理由」。如果改邮件逻辑要动 UserService,改数据库也要动它,那肯定有问题。
2. 开闭原则(OCP)
对扩展开放,对修改关闭。我个人的习惯是:写代码时多想想「以后要加新功能怎么办」。
举个例子,假设有个支付系统:
// ❌ 每次加新支付方式都要改这个类
class PaymentProcessor {
public function pay($type, $amount) {
if ($type == 'alipay') {
// 支付宝逻辑
} elseif ($type == 'wechat') {
// 微信逻辑
}
}
}
// ✅ 用接口+策略模式,扩展不用改
interface PaymentMethod {
public function pay($amount);
}
class Alipay implements PaymentMethod {
public function pay($amount) { /* ... */ }
}
class WechatPay implements PaymentMethod {
public function pay($amount) { /* ... */ }
}
class PaymentProcessor {
public function process(PaymentMethod $method, $amount) {
$method->pay($amount);
}
}
我曾经在一个电商项目中,就因为没遵守这个原则,每次加支付渠道都要改核心类,测试回归一遍,累得够呛。后来重构后,加新渠道只需要新建一个类,爽多了。
小技巧:多用接口和抽象类,把「变化的部分」抽象出来。这样新功能来了,写新代码就行,不用动老代码。
3. 里氏替换原则(LSP)
这个原则名字听着吓人,其实意思很简单:子类应该能替换父类,而且程序行为不变。
我记得有一次,团队里有人写了个 Square(正方形)继承 Rectangle(矩形),结果出问题了:
class Rectangle {
protected $width;
protected $height;
public function setWidth($w) { $this->width = $w; }
public function setHeight($h) { $this->height = $h; }
public function getArea() { return $this->width * $this->height; }
}
class Square extends Rectangle {
public function setWidth($w) {
$this->width = $w;
$this->height = $w; // 正方形宽高相等
}
public function setHeight($h) {
$this->height = $h;
$this->width = $h;
}
}
// 调用方以为自己在操作矩形
function resize(Rectangle $rect) {
$rect->setWidth(5);
$rect->setHeight(10);
echo $rect->getArea(); // 期望50,但Square返回100!
}
为什么会这样?因为正方形和矩形虽然数学上有关联,但行为上不能互相替换。继承不是用来复用代码的,是用来表达「is-a」关系的。
避坑指南:我曾经在项目中看到有人让 AdminUser 继承 User,然后重写了权限验证方法,结果到处出 bug。记住:子类不能削弱父类的能力,也不能违反父类的约定。
4. 接口隔离原则(ISP)
接口要小而专,不要大而全。说白了就是:别强迫调用方依赖他们不需要的方法。
我见过这样的「上帝接口」:
// ❌ 一个接口塞了太多方法
interface Worker {
public function work();
public function eat();
public function sleep();
}
// 机器人只需要work,但被迫实现了eat和sleep
class Robot implements Worker {
public function work() { /* 干活 */ }
public function eat() { throw new Exception('我不吃饭'); }
public function sleep() { throw new Exception('我不睡觉'); }
}
// ✅ 拆分成小接口
interface Workable {
public function work();
}
interface Eatable {
public function eat();
}
interface Sleepable {
public function sleep();
}
class Robot implements Workable {
public function work() { /* 干活 */ }
}
class Human implements Workable, Eatable, Sleepable {
public function work() { /* 干活 */ }
public function eat() { /* 吃饭 */ }
public function sleep() { /* 睡觉 */ }
}
嗯,这里要注意:接口越小,复用性越强。但也不是越小越好,得有个度。我一般遵循「调用方需要什么,接口就提供什么」的原则。
5. 依赖倒置原则(DIP)
这个原则是 SOLID 里最核心的,也是很多人容易搞混的。它说的是:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
说白了,就是面向接口编程,不要面向实现编程。
举个例子:
// ❌ 高层直接依赖低层
class OrderService {
private $db;
public function __construct() {
$this->db = new MySQLConnection(); // 硬编码!
}
}
// ✅ 依赖抽象
interface DatabaseConnection {
public function connect();
}
class MySQLConnection implements DatabaseConnection {
public function connect() { /* ... */ }
}
class PostgreSQLConnection implements DatabaseConnection {
public function connect() { /* ... */ }
}
class OrderService {
private $db;
public function __construct(DatabaseConnection $db) {
$this->db = $db; // 依赖接口,不依赖具体实现
}
}
我在项目中遇到过这样的坑:一个系统用了 MySQL,后来要迁移到 PostgreSQL,结果因为到处都是 new MySQLConnection(),改了一个星期。如果一开始就依赖接口,改一行配置就行。
核心总结:依赖倒置不是「依赖注入」,依赖注入是实现依赖倒置的一种手段。真正的目的是让高层模块不依赖低层模块的具体实现。
写在最后
SOLID 原则不是教条,是工具。你不需要在每个项目里都严格遵守,但当代码开始变得难以维护时,回头看看这五个原则,往往能找到答案。
我个人习惯是:写代码时先想清楚「这个类会不会经常变」「调用方需要什么」「怎么扩展更方便」。想清楚了再动手,比后期重构省事多了。
记住一句话:好的架构设计,不是一次写对的,是不断重构出来的。SOLID 原则就是你在重构路上的指南针。