24、面向对象设计原则:高内聚低耦合,组合优于继承,迪米特法则,接口隔离
面向对象设计原则,说白了就是一套「写代码的规矩」。我刚开始写程序那会儿,觉得能跑就行。后来项目越做越大,改一个功能要动十几个文件,改完还出 bug……嗯,那时候我才明白,规矩不是用来限制你的,是用来救你的。
今天聊四个核心原则。它们不是孤立的,你想想看,它们其实都在回答同一个问题:怎么让代码好改、好测、好复用?
一、高内聚与低耦合
这两个词经常一起出现。我个人的理解很简单:高内聚,就是把相关的东西放一起;低耦合,就是让不相关的东西少说话。
1. 高内聚
一个类只做一件事,并且把它做好。比如一个 OrderService 只处理订单逻辑,别把发送邮件、生成报表也塞进去。
- 代码容易理解——打开一个类就知道它干嘛的
- 修改影响范围小——改订单逻辑不会炸了邮件模块
- 复用性高——拿出去就能用
2. 低耦合
模块之间依赖越少越好。我在项目中遇到过最头疼的事:改了一个工具类的参数名,结果十几个模块报错。这就是耦合太紧的代价。
// 高耦合(不推荐)
public class OrderService {
private EmailService email = new EmailService();
private SmsService sms = new SmsService();
public void createOrder(Order order) {
// 创建订单...
email.send("订单创建成功");
sms.send("订单创建成功");
}
}
// 低耦合(推荐)
public class OrderService {
private List<NotificationService> notifiers;
public OrderService(List<NotificationService> notifiers) {
this.notifiers = notifiers;
}
public void createOrder(Order order) {
// 创建订单...
for (var n : notifiers) {
n.notify("订单创建成功");
}
}
}
看到了吗?低耦合版本里,OrderService 根本不关心具体用什么方式通知。加个微信通知?加个钉钉通知?完全不用改 OrderService 的代码。
二、组合优于继承
这个原则我年轻时吃过亏。那时候觉得继承很「面向对象」,恨不得所有类都继承来继承去。结果呢?三层继承之后,父类改一个方法,子类全崩了。
组合的核心思想:用「有什么」代替「是什么」。
// 继承方式(不灵活)
public class Bird {
public void fly() { System.out.println("飞"); }
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("企鹅不会飞");
}
}
// 组合方式(灵活)
public class Bird {
private FlyBehavior flyBehavior;
public Bird(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void performFly() {
flyBehavior.fly();
}
}
public class Penguin extends Bird {
public Penguin() {
super(new NoFly()); // 不会飞的行为
}
}
组合的优势很明显:
- 运行时可以动态改变行为
- 不会破坏封装
- 类层次扁平,容易理解
三、迪米特法则(最少知识原则)
这个原则的名字听着挺唬人,其实意思很简单:一个对象应该尽量少地知道其他对象。
说白了就是:别跟陌生人说话。你只跟你的直接朋友打交道,别去调用朋友的朋友的方法。
// 违反迪米特法则
public class Teacher {
public void checkHomework(Student student) {
// 老师直接调用学生的书包里的作业本
student.getBag().getHomework().check();
}
}
// 符合迪米特法则
public class Teacher {
public void checkHomework(Student student) {
// 老师只跟学生说话
student.submitHomework();
}
}
public class Student {
public void submitHomework() {
// 学生自己处理书包和作业本
bag.getHomework().check();
}
}
- 一个对象的方法只能调用:自身的方法、参数对象的方法、成员对象的方法、创建的对象的方法
- 不要通过 getter 拿到内部对象后再调用它的方法
- 如果发现代码里出现连续的点号(a.getB().getC().doSomething()),大概率违反了这个原则
我记得有一次重构一个老项目,看到一个方法里写了 order.getUser().getAddress().getCity()。我当时就想:这个方法的调用者到底需要知道多少东西?后来改成 order.getCity(),清爽多了。
四、接口隔离原则
这个原则说的是:客户端不应该被迫依赖它不使用的接口。
你想想看,如果一个接口里塞了十几个方法,实现类里有一半方法都只能抛出 UnsupportedOperationException,那这个接口设计就有问题。
// 胖接口(不推荐)
public interface Worker {
void work();
void eat();
void sleep();
void code();
void design();
void test();
}
// 接口隔离(推荐)
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Coder {
void code();
}
public interface Designer {
void design();
}
public interface Tester {
void test();
}
// 程序员只需要实现需要的接口
public class Programmer implements Workable, Eatable, Sleepable, Coder {
// 只实现自己需要的
}
知识体系总览
下面这张图把这四个原则的关系梳理了一下。你会发现它们其实都在解决同一个问题:让代码更灵活、更稳定。
总结一下
这四个原则,我个人的使用频率排序是这样的:
| 原则 | 核心思想 | 我常用的场景 |
|---|---|---|
| 高内聚低耦合 | 相关放一起,不相关少联系 | 模块拆分、类设计 |
| 组合优于继承 | 用组合代替继承层次 | 策略模式、装饰器模式 |
| 迪米特法则 | 减少对象间的知识 | 接口设计、方法参数设计 |
| 接口隔离 | 接口小而专一 | 微服务接口、类库设计 |
公众号:蓝海资料掘金营,微信deep3321