设计原则(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 原则就是你在重构路上的指南针。