DIP – 隐藏不相关的细节
在本节中,我们将学习 DIP 如何允许我们将代码拆分为可以相互独立更改的单独组件。然后我们将看到这如何自然地导致 SOLID 中的 OCP 部分。
依赖倒置(DI)意味着我们编写的代码依赖于抽象,而不是细节。与此相反的是有两个代码块,一个依赖于另一个的详细实现。对一个块的更改将导致另一个块的更改。为了在实践中看到这个问题是什么样子,让我们回顾一个反例。以下代码片段从我们在应用 SRP 后离开的 Shapes
类开始:
package shapes;
import java.util.ArrayList;
import java.util.List;
public class Shapes {
private final List<Shape> allShapes = new ArrayList<>();
public void add(Shape s) {
allShapes.add(s);
}
public void draw(Graphics g) {
for (Shape s : allShapes) {
switch (s.getType()) {
case "textbox":
var t = (TextBox) s;
t.draw(g);
break;
case "rectangle":
var r = (Rectangle) s;
r.draw(g);
}
}
}
}
这段代码在维护 Shape
对象列表并绘制它们方面工作得很好。问题是它知道太多关于它应该绘制的形状类型的信息。draw()
方法具有一个你可以看到的对象类型切换。这意味着如果关于应该绘制哪些类型的形状有任何变化,那么这段代码也必须更改。如果我们想向系统中添加一个新的 Shape
,那么我们必须修改这个 switch
语句和相关的 TDD 测试代码。
一个类知道另一个类的技术术语是它们之间存在依赖关系。Shapes
类依赖于 TextBox
和 Rectangle
类。我们可以在以下 UML 类图中直观地表示这一点:

我们可以看到 Shapes
类直接依赖于 Rectangle
和 TextBox
类的细节。这由 UML 类图中箭头的方向表示。拥有这些依赖关系使得使用 Shapes
类更加困难,原因如下:
-
我们必须更改
Shapes
类以添加一种新的形状 -
具体类(如
Rectangle
)的任何更改都将导致此代码更改 -
Shapes
类将变得更长且更不易读 -
我们将得到更多的测试用例
-
每个测试用例将与具体类(如
Rectangle
)耦合
这是一种非常程序化的方法来创建一个处理多种形状的类。它通过做太多事情并知道每种形状对象的太多细节而违反了 SRP。Shapes
类依赖于具体类(如 Rectangle
和 TextBox
)的细节,这直接导致了上述问题。
幸运的是,有更好的方法。我们可以使用接口的力量来改进这一点,使 Shapes
类不依赖于这些细节。这称为 DI。接下来让我们看看那是什么样子。
将依赖注入应用于形状代码
我们可以通过应用前一章中描述的依赖倒置原则(DIP)来改进形状代码。让我们向我们的 Shape
接口添加一个 draw()
方法,如下所示:
package shapes;
public interface Shape {
void draw(Graphics g);
}
这个接口是每个形状具有的单一职责的抽象。每个形状必须知道在我们调用 draw()
方法时如何绘制自己。下一步是使我们的具体形状类实现这个接口。
让我们以 Rectangle
类为例。你可以在这里看到:
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void draw(Graphics g) {
for (int row = 0; row < height; row++) {
g.drawHorizontalLine(width);
}
}
}
我们现在已经将多态性的 OO 概念引入了我们的形状类中。这打破了 Shapes
类对了解 Rectangle
和 TextBox
类的依赖。Shapes
类现在只依赖于 Shape
接口。它不再需要知道每个形状的类型。
我们可以将 Shapes
类重构为如下所示:
public class Shapes {
private final List<Shape> all = new ArrayList<>();
public void add(Shape s) {
all.add(s);
}
public void draw(Graphics graphics) {
all.forEach(shape -> shape.draw(graphics));
}
}
这次重构完全移除了 switch
语句和 getType()
方法,使代码更易于理解和测试。如果我们添加一种新的形状,Shapes
类不再需要更改。我们已经打破了对形状类细节的依赖。
一个小的重构将我们传递给 draw()
方法的 Graphics
参数移动到一个字段中,并在构造函数中初始化,如下面的代码片段所示:
public class Shapes {
private final List<Shape> all = new ArrayList<>();
private final Graphics graphics;
public Shapes(Graphics graphics) {
this.graphics = graphics;
}
public void add(Shape s) {
all.add(s);
}
public void draw() {
all.forEach(shape -> shape.draw(graphics));
}
}
这就是 DIP 在工作。我们在 Shape
接口中创建了一个抽象。Shapes
类是这个抽象的消费者。实现该接口的类是提供者。两组类都只依赖于抽象;它们不依赖于彼此内部的细节。Shapes
类中没有对 Rectangle
类的引用,Rectangle
类中也没有对 Shapes
类的引用。我们可以在下面的 UML 类图中看到这种依赖关系的反转——看看依赖箭头的方向与图 7.4 相比如何变化:

在这个版本的 UML 图中,描述类之间依赖关系的箭头指向相反的方向。依赖关系已经被反转——因此,这个原则的名称。我们的 Shapes
类现在依赖于我们的抽象,即 Shape
接口。Rectangle
类和 TextBox
类的具体实现也是如此。我们已经反转了依赖图并将箭头倒置。DI 完全解耦了类之间的关系,因此非常强大。当我们看第 8 章《测试替身——存根和模拟》时,我们将看到这如何导致 TDD 测试的一个关键技术。
DIP
使代码依赖于抽象而不是细节。 |
我们已经看到 DIP 是我们用来简化代码的一个主要工具。它允许我们编写处理接口的代码,然后使用该代码与实现该接口的任何具体类。这引发了一个问题:我们可以编写一个实现接口但不能正确工作的类吗?这是我们下一节的主题。