面向对象设计原则(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 原则知识体系
下面这张图帮你理清五个原则的关系和核心要点:
五个原则的关系
这五个原则不是孤立的。它们互相配合,共同构建出健壮的面向对象设计:
- SRP 是基础——类职责单一了,才容易扩展(OCP)
- LSP 保证继承的正确性,让 OCP 的扩展不会出问题
- ISP 让接口更干净,避免实现类承担不必要的负担
- DIP 是顶层指导,让整个系统依赖抽象,而不是具体实现
我的建议:刚开始学 SOLID 时,别想着全用上。先从 SRP 和 DIP 入手,这两个最容易见效。等熟练了,再慢慢把其他原则融入日常编码中。
好了,SOLID 原则就聊到这儿。记住,这些原则是指导方针,不是法律条文。实际项目中,有时候为了性能或简化设计,适当违反某个原则也是可以接受的。关键是——你得知道自己为什么这么做。