面向对象设计原则(SOLID)

聊到设计模式,就绕不开 SOLID 这五个原则。说实话,我早年写代码时根本不管这些,觉得能跑就行。直到有一次接手一个遗留系统,改一个功能要动十几个文件,改完还总出 bug…… 那之后我才真正理解,这些原则不是纸上谈兵,而是实打实的血泪教训。

SOLID 是五个原则的缩写,咱们一个一个来看。

单一职责原则(SRP)

一个类,只应该有一个引起它变化的原因。

说白了,就是一个类只干一件事。你想想看,如果一个类既负责数据存取,又负责业务逻辑,还负责界面展示——那改任何一个功能,你都得动这个类。风险太大了。

核心思想:高内聚,低耦合。每个类只关注一个职责,变化时只影响一个点。

我在项目中遇到过这样一个例子:一个 OrderService 类,里面既有订单计算逻辑,又有数据库操作,还发了邮件通知。后来需求变了,邮件模板要改,结果不小心把价格计算逻辑改坏了。嗯,这就是典型的 SRP 被违反。

正确的做法是拆开:

// 违反 SRP
public class OrderService {
    public void processOrder(Order order) {
        // 计算价格
        // 保存到数据库
        // 发送邮件
    }
}

// 符合 SRP
public class OrderCalculator {
    public double calculate(Order order) { ... }
}

public class OrderRepository {
    public void save(Order order) { ... }
}

public class EmailService {
    public void sendConfirmation(Order order) { ... }
}

我的习惯:写类之前先问自己——这个类以后会因为什么原因被修改?如果答案超过一个,就该拆了。

开闭原则(OCP)

对扩展开放,对修改关闭。

这句话听起来有点绕。我换个说法:你写好的代码,尽量不要去改它;但如果有新需求,你得能轻松地加新功能。

怎么做到?靠抽象。用接口或抽象类把变和不变的部分隔离开。

// 违反 OCP
public class AreaCalculator {
    public double calculate(Object shape) {
        if (shape instanceof Circle) {
            Circle c = (Circle) shape;
            return Math.PI * c.radius * c.radius;
        } else if (shape instanceof Rectangle) {
            Rectangle r = (Rectangle) shape;
            return r.width * r.height;
        }
        // 每加一种形状,都要改这里
    }
}

// 符合 OCP
public interface Shape {
    double area();
}

public class Circle implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

public class Rectangle implements Shape {
    public double area() { return width * height; }
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.area();  // 不用改!
    }
}

我曾经踩过的坑:为了追求 OCP,过度设计抽象层,结果代码变得极其复杂。记住,OCP 是针对频繁变化的部分,不是所有地方都要搞抽象。

里氏替换原则(LSP)

子类必须能够替换掉父类,并且程序行为不变。

这个原则其实在说:继承要谨慎。如果你写了一个子类,把它当父类用的时候出了问题,那你的继承关系就有问题。

我见过最经典的例子是「正方形继承矩形」:

public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w);  // 保持正方形
    }
}

// 客户端代码
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
// 用户期望宽5高10,但实际是10x10 —— 违反 LSP!

为什么会这样?因为正方形和矩形虽然在数学上是「is-a」关系,但在行为上并不兼容。我个人习惯是:优先用组合,而不是继承。组合更灵活,也不容易踩 LSP 的坑。

判断方法:如果子类重写父类方法后,需要客户端做特殊判断,那就违反了 LSP。

接口隔离原则(ISP)

客户端不应该被迫依赖它不需要的接口。

说白了,接口要小而专。不要搞一个大而全的接口,让实现类去实现一堆用不到的方法。

我记得有一次,团队里有人设计了一个 Worker 接口,里面有 work()eat()sleep() 三个方法。结果机器人实现类也得实现 eat()sleep(),只能抛异常。这就是典型的接口太胖了。

// 违反 ISP
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// 符合 ISP
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class HumanWorker implements Workable, Eatable, Sleepable { ... }
public class RobotWorker implements Workable { ... }

我的经验:接口的方法数控制在 3-5 个以内。如果超过 5 个,大概率需要拆分了。

依赖倒置原则(DIP)

高层模块不应该依赖低层模块,两者都应该依赖抽象。

这句话的核心是:面向接口编程,而不是面向实现编程

你想想看,如果业务层直接依赖数据库层,那换数据库的时候,业务层也得跟着改。这合理吗?不合理。正确的做法是让业务层依赖一个抽象接口,数据库层去实现这个接口。

// 违反 DIP
public class UserService {
    private MySQLDatabase db = new MySQLDatabase();  // 直接依赖具体实现
}

// 符合 DIP
public interface UserRepository {
    User findById(int id);
}

public class MySQLUserRepository implements UserRepository { ... }

public class UserService {
    private UserRepository repo;  // 依赖抽象
    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}

注意:DIP 不是让你把所有地方都抽象化。我见过有人为了 DIP,连工具类都搞接口,那就过度了。判断标准是:这个依赖是否可能变化?如果不会变,直接依赖也没问题。

SOLID 原则知识体系

下面这张图帮你理清五个原则的关系和核心要点:

SOLID 原则知识体系 SOLID 原则 SRP 单一职责 OCP 开闭原则 LSP 里氏替换 ISP 接口隔离 DIP 依赖倒置 一个类只做一件事 高内聚,低耦合 对扩展开放 对修改关闭 子类可替换父类 行为不变 接口小而专 不依赖不需要的方法 依赖抽象 不依赖具体实现 核心目标:降低耦合,提高可维护性 每个原则解决一类特定的设计问题 实践建议:先理解问题,再套用原则。不要为了原则而原则。

五个原则的关系

这五个原则不是孤立的。它们互相配合,共同构建出健壮的面向对象设计:

  • SRP 是基础——类职责单一了,才容易扩展(OCP)
  • LSP 保证继承的正确性,让 OCP 的扩展不会出问题
  • ISP 让接口更干净,避免实现类承担不必要的负担
  • DIP 是顶层指导,让整个系统依赖抽象,而不是具体实现

我的建议:刚开始学 SOLID 时,别想着全用上。先从 SRP 和 DIP 入手,这两个最容易见效。等熟练了,再慢慢把其他原则融入日常编码中。

好了,SOLID 原则就聊到这儿。记住,这些原则是指导方针,不是法律条文。实际项目中,有时候为了性能或简化设计,适当违反某个原则也是可以接受的。关键是——你得知道自己为什么这么做。