1. 首先,我们看一个“反面教材”(违反DIP的设计)
假设我们要设计一个简单的系统,让用户可以通过不同的开关(比如一个按钮、一个遥控器)来控制不同的灯。
一个很直觉但糟糕的设计如下:
List item
// 高层模块:开关
class Button {
private Lamp lamp; // Button类直接依赖了一个具体的Lamp类
public Button(Lamp lamp) {
this.lamp = lamp;
}
public void press() {
// ... 一些其他逻辑 ...
if (/* 某种条件 */) {
lamp.turnOn(); // 直接调用Lamp的方法
} else {
lamp.turnOff();
}
}
}
// 低层模块:灯
class Lamp {
public void turnOn() {
System.out.println("灯亮了");
}
public void turnOff() {
System.out.println("灯灭了");
}
}
这个设计有什么问题?
- 紧耦合(Tight Coupling):
Button类直接依赖于具体的Lamp类。Button的代码里写死了“我只能控制灯”。 - 难以扩展:如果有一天,我们想让这个按钮去控制电风扇或者空调,怎么办?我们只能去修改
Button类的源代码,把Lamp lamp改成Fan fan或AirConditioner ac。这违反了“对修改关闭,对扩展开放”的开闭原则。 - 高层依赖低层:在这个设计中,高层策略模块(
Button,它定义了“按下开关”这个抽象策略)反而依赖于低层实现细节模块(Lamp)。这就像是将军(高层)的作战计划要根据一个士兵(低层)的装备来制定,非常不合理。
2. 现在,我们用“依赖倒置”原则来重构
依赖倒置原则的两条核心定义:
- 高层模块不应该依赖低层模块,两者都应该依赖于抽象。
- 抽象不应该依赖细节,细节应该依赖抽象。
如何应用?
第1步:创建一个抽象接口(两者都依赖它)
这个接口应该抽象出高层模块想要控制低层模块的方式。对于 Button 来说,它不关心你到底是灯还是风扇,它只关心“你能不能被我打开和关闭”。
所以我们创建一个 SwitchableDevice 接口:
// 抽象接口
public interface SwitchableDevice {
void turnOn();
void turnOff();
}
第2步:让低层模块实现这个抽象接口
现在,Lamp 不再是一个简单的类,它成为了抽象接口的一个“实现细节”。
// 低层模块:实现抽象接口
class Lamp implements SwitchableDevice {
@Override
public void turnOn() {
System.out.println("灯亮了");
}
@Override
public void turnOff() {
System.out.println("灯灭了");
}
}
// 我们可以轻松地添加新的低层模块,而无需修改任何现有高层代码
class Fan implements SwitchableDevice {
@Override
public void turnOn() {
System.out.println("风扇开始转动");
}
@Override
public void turnOff() {
System.out.println("风扇停止了");
}
}
第3步:让高层模块依赖抽象接口,而不是具体实现
修改 Button 类,让它不再依赖 Lamp,而是依赖 SwitchableDevice 这个接口。
// 高层模块:依赖抽象接口
class Button {
// 现在它依赖的是一个抽象,而不是一个具体的实现
private SwitchableDevice device;
// 通过构造器注入依赖(这就是依赖注入,是实现依赖倒置的常用方法)
public Button(SwitchableDevice device) {
this.device = device;
}
public void press() {
// ... 一些其他逻辑 ...
if (/* 某种条件 */) {
device.turnOn(); // 它调用的是接口的方法,根本不知道后面是灯还是风扇
} else {
device.turnOff();
}
}
}
3. 看看现在的效果(“倒置”在哪里?)
现在我们来组装和使用这个系统:
public class Main {
public static void main(String[] args) {
// 控制灯
SwitchableDevice myLamp = new Lamp();
Button button1 = new Button(myLamp);
button1.press(); // 输出“灯亮了”
// 控制风扇:我们不需要修改Button的任何代码!
SwitchableDevice myFan = new Fan();
Button button2 = new Button(myFan);
button2.press(); // 输出“风扇开始转动”
// 未来如果想控制空调,只需要创建一个AirConditioner类实现SwitchableDevice接口即可。
// Button类永远不需要再被修改。
}
}
“倒置”体现在哪里?
- 传统依赖关系:高层
Button-> 低层Lamp(从上到下) - 倒置后的依赖关系:
- 高层
Button-> 抽象SwitchableDevice<- 低层Lamp - 依赖关系的箭头不再是从上到下,而是同时指向了中间的抽象层。高層和低層的依赖方向被“倒置”了,它们现在处于同一水平位置,都依赖于一个稳定的抽象。
- 高层
总结
你可以把依赖倒置理解为一种“中介”或“合同”模式。
- 抽象接口(
SwitchableDevice)就是一份合同。它规定了你必须提供“打开”和“关闭”的服务。 - 高层模块(
Button)是合同的甲方。它说:“我不管你是谁,只要你签了这份合同(实现了接口),我就可以按合同要求你做事。” - 低层模块(
Lamp,Fan)是合同的乙方。它们签署合同(实现接口),并具体完成合同规定的任务。
这样做的好处巨大:
- 解耦:
Button和Lamp完全解耦,它们之间唯一的联系就是合同。 - 可扩展性:可以无限添加新的“乙方”(新的设备),而完全不用修改“甲方”(
Button)的代码。 - 可测试性:在测试
Button时,你可以很容易地创建一个“ Mock Device”(模拟设备)来实现接口,从而孤立地测试Button的逻辑。
DDD架构中的依赖倒置
在DDD架构中要实现业务逻辑,我们没有专门的业务层去调用各种接口(外部接口,我把他当成勒我们所需要的依赖),当功能越来越完善的时候我们就会发现业务层写得很乱,所以我们就根据依赖倒置的原则,外部接口的实现我们下掉到infrastructurec层,就是把依赖提取出来噻。然后再业务中调用接口,关注业务即可
753

被折叠的 条评论
为什么被折叠?



