4、面向对象设计原则(下):接口隔离原则、依赖倒置原则、迪米特法则
好,咱们接着聊剩下的三个设计原则。这三个原则,说实话,比前面几个更「抽象」一些,也更考验你对系统架构的把控能力。我在带团队做代码评审时,发现大部分设计问题,其实都跟这几个原则没用好有关。
4.1 接口隔离原则(ISP)
定义:客户端不应该依赖它不需要的接口。一个类对另一个类的依赖,应该建立在最小的接口上。
说白了,就是别把一堆不相干的方法塞到一个接口里。你想想看,如果一个接口里有10个方法,调用方只需要其中2个,那剩下的8个就是累赘。实现类还得硬着头皮去实现那些空方法,多别扭。
核心思想:接口要「小而专」,不要「大而全」。
我在项目中遇到过这样一个场景:早期我们设计了一个「员工管理」接口,里面既有addEmployee()、deleteEmployee(),又有calculateSalary()、generatePayroll()。结果做考勤系统的同事只想调用calculateSalary(),却被迫依赖了整个接口。后来重构时,我们把薪资相关的操作单独抽成了一个PayrollService接口,问题就解决了。
判断标准:如果一个接口的某个方法,在调用方那里永远用不到,就该考虑拆接口了。
来看个代码对比。这是违反ISP的写法:
// 违反接口隔离原则
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() { /* 干活 */ }
public void eat() { /* 机器人不需要吃饭 */ }
public void sleep() { /* 机器人不需要睡觉 */ }
}
这是遵循ISP的写法:
// 遵循接口隔离原则
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Robot implements Workable {
public void work() { /* 干活 */ }
}
public class Human implements Workable, Eatable, Sleepable {
public void work() { /* 干活 */ }
public void eat() { /* 吃饭 */ }
public void sleep() { /* 睡觉 */ }
}
你看,Robot不再需要实现那些无意义的方法了。接口职责清晰,每个实现类只关心自己需要的。
4.2 依赖倒置原则(DIP)
定义:高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
这个原则的名字有点绕,但道理很简单:面向接口编程,不要面向实现编程。
我记得有一次,团队里有个同事写了一个订单处理类,里面直接new了一个MySqlOrderRepository。我说:「如果明天要换成Oracle,你改不改代码?」他愣了一下。这就是典型的违反DIP——高层模块(订单处理)直接依赖了低层模块(MySQL实现)。
注意:依赖倒置不是说「不要用new」,而是说「不要直接new一个具体实现类」。应该通过接口或抽象类来解耦。
正确的做法是这样的:
// 定义抽象接口
public interface OrderRepository {
void save(Order order);
}
// 低层实现
public class MySqlOrderRepository implements OrderRepository {
public void save(Order order) { /* MySQL存储 */ }
}
public class OracleOrderRepository implements OrderRepository {
public void save(Order order) { /* Oracle存储 */ }
}
// 高层模块依赖抽象
public class OrderService {
private OrderRepository repository;
// 通过构造器注入,而不是在内部new
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void processOrder(Order order) {
// 业务逻辑...
repository.save(order);
}
}
这样一来,OrderService不再关心底层是MySQL还是Oracle。换数据库?换个实现类传进来就行。这就是依赖倒置的魅力。
个人习惯:我写代码时,只要发现一个类里出现了多个new关键字,就会停下来想想——这些依赖是不是该抽象一下?
4.3 迪米特法则(LoD)
定义:一个对象应该对其他对象保持最少的了解。又叫「最少知识原则」。
这个原则最接地气的理解就是:不要和陌生人说话。你只跟你直接的朋友通信,不要绕来绕去调用别人的内部方法。
我曾经接手过一个遗留系统,里面有一段代码是这样的:
// 违反迪米特法则
public class Teacher {
public void command(GroupLeader leader) {
// 老师直接指挥小组长,让小组长去统计学生人数
// 但这里老师直接访问了GroupLeader内部的List<Student>
List<Student> students = leader.getStudents();
for (Student student : students) {
System.out.println(student.getName());
}
}
}
这里的问题在于:Teacher直接访问了GroupLeader内部的students列表。如果哪天GroupLeader内部的数据结构变了,Teacher也得跟着改。这耦合度太高了。
正确的做法是让GroupLeader自己去做统计,Teacher只告诉它「去统计人数」:
// 遵循迪米特法则
public class GroupLeader {
private List<Student> students;
public void countStudents() {
System.out.println("学生人数:" + students.size());
}
}
public class Teacher {
public void command(GroupLeader leader) {
// 老师只跟直接的朋友(GroupLeader)通信
// 不关心GroupLeader内部怎么实现
leader.countStudents();
}
}
判断标准:如果你发现代码里出现了「A.B.C.doSomething()」这种链式调用,十有八九是违反迪米特法则了。
嗯,这里要注意:迪米特法则不是让你把所有方法都封装起来,而是让你控制好「暴露的边界」。每个类只暴露必要的接口,内部实现细节藏好。
4.4 三个原则的关系与对比
这三个原则其实是从不同角度在说同一件事:降低耦合,提高内聚。
- 接口隔离原则:从接口设计角度,让接口小而专
- 依赖倒置原则:从依赖关系角度,让高层不依赖低层
- 迪米特法则:从通信角度,减少对象之间的直接交互
我把它们的关系画了一张图,方便你理解:
从图中可以看出,这三个原则虽然名字不同,但最终都指向同一个目标。实际开发中,它们往往是配合使用的。比如你按照ISP拆分了接口,然后按照DIP让高层依赖这些接口,最后按照LoD控制好对象之间的交互边界——一套组合拳下来,代码质量会有质的提升。
避坑指南:我曾经在一个项目中过度追求「原则」,把接口拆得太细,结果导致类数量爆炸,反而增加了维护成本。记住:原则是指导,不是教条。适度就好。
好了,这三个原则就讲到这里。理解它们不难,难的是在实际项目中「恰到好处」地运用。多写、多重构、多复盘,慢慢就有感觉了。
公众号:蓝海资料掘金营,微信deep3321