依赖倒置原则--设计模式

五、依赖倒置原则
关于 SOLID 原则,我们已经学过单一职责、开闭、里式替换、接口隔离这四个原
则。今天,我们再来学习最后一个原则:依赖倒置原则。
1、原理
依赖倒置原则(Dependency Inversion Principle,简称 DIP)是面向对象设计的五
大原则(SOLID)之一。这个原则强调要依赖于抽象而不是具体实现。遵循这个原则
可以使系统的设计更加灵活、可扩展和可维护。
依赖倒置原则有两个关键点:


1. 高层模块不应该依赖于低层模块,它们都应该依赖于抽象。
2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

倒置(Inversion)在这里的确是指“反过来”的意思。在依赖倒置原则(Dependency
Inversion Principle, DIP)中,我们需要改变依赖关系的方向使得高层模块和低层
模块都依赖于抽象,而不是高层模块直接依赖于低层模块。
这样一来,依赖关系就从
直接依赖具体实现“反过来”依赖抽象了。

在没有应用依赖倒置原则的传统软件设计中,高层模块通常直接依赖于低层模块。这
会导致系统的耦合度较高,低层模块的变化很容易影响到高层模块。
当我们应用依赖
倒置原则时,高层模块和低层模块的依赖关系发生了改变,它们都依赖于抽象(例如
接口或抽象类),而不再是高层模块直接依赖于低层模块。这样,我们就实现了依赖
关系的“倒置”。
 

这个地方例如在我们的tomcat中,我们到高层模块的tomcat并不会直接去依赖于我们在模块所引入的相关的jar包

而是我们在使用tomcat中他们会调用更高的抽象类

这种“倒置”的依赖关系使得系统的耦合度降低,提高了系统的可维护性和可扩展性。
因为当低层模块的具体实现发生变化时,只要不改变抽象,高层模块就不需要进行调
整。所以这个原则叫做依赖倒置原则。


2、如何理解抽象
当我们在讨论依赖倒置原则中的抽象时,绝对不能仅仅把他理解为一个接口。抽象的
目的是将关注点从具体实现转移到概念和行为,使得我们在设计和编写代码时能够更
加关注问题的本质。通过使用抽象,我们可以创建更加灵活、可扩展和可维护的系
统。
事实上抽象是一个很广泛的概念,它可以包括接口、抽象类以及由大量接口,抽象类
和实现组成的更高层次的模块。通过将系统分解为更小的、可复用的组件,我们可以
实现更高层次的抽象。这些组件可以独立地进行替换和扩展,从而使整个系统更加灵
活。
在依赖倒置原则的背景下,我们可以从以下几个方面理解抽象:
(1)接口
接口是 Java 中实现抽象的一种常见方式。接口定义了一组方法签名,表示实现该接
口的类应具备哪些行为。接口本身并不包含具体实现,所以它强调了行为的抽象。
假设我们正在开发一个在线购物系统,其中有一个订单处理模块。订单处理模块需要
与不同的支付服务提供商(如 PayPal、Stripe 等)进行交互。如果我们直接依赖于
支付服务提供商的具体实现,那么在更换支付服务提供商或添加新的支付服务提供商
时,我们可能需要对订单处理模块进行大量修改。为了避免这种情况,我们应该依赖
于接口而不是具体实现。
首先,我们定义一个支付服务接口:

public interface PaymentService {
boolean processPayment(Order order);
}

对支付的服务创建相关的接口


护性。
public class PayPalPaymentService implements PaymentService {
@Override
public boolean processPayment(Order order) {
// 实现 PayPal 支付逻辑
}
}
public class StripePaymentService implements PaymentService {
@Override
public boolean processPayment(Order order) {
// 实现 Stripe 支付逻辑
}
}

现在,我们可以在订单处理模块中依赖 PaymentService 接口,而不是具体的实
现:
通过这种方式,当我们需要更换支付服务提供商或添加新的支付服务提供商时,只需
要提供一个新的实现类,而不需要修改 OrderProcessor 类。我们可以在运行时通
过构造函数注入不同的支付服务实现,使得系统更加灵活和可扩展。
这个例子展示了如何依赖接口而不是实现来编写代码,从而提高系统的灵活性和可维
护性。
public class PayPalPaymentService implements PaymentService {
@Override
public boolean processPayment(Order order) {
// 实现 PayPal 支付逻辑
}
}
public class StripePaymentService implements PaymentService {
@Override
public boolean processPayment(Order order) {
// 实现 Stripe 支付逻辑
}
}

public class OrderProcessor {
private PaymentService paymentService;
public OrderProcessor(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void processOrder(Order order) {
// 其他订单处理逻辑...
boolean paymentResult = paymentService.processPayment(order);
// 根据 paymentResult 处理支付结果
}
}

(2)抽象类
抽象类是另一种实现抽象的方式。与接口类似,抽象类也可以定义抽象方法,表示子
类应该具备哪些行为。不过抽象类还可以包含部分具体实现,这使得它们比接口更加
灵活。

abstract class Shape {
abstract double getArea();
void displayArea() {
System.out.println("面积为: " + getArea());
}
}
class Circle extends Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double getArea() {
return Math.PI * Math.pow(radius, 2);
}
}
class Square extends Shape {
private final double side;
Square(double side) {
this.side = side;
}
@Override
double getArea() {
return Math.pow(side, 2);
}
}

在这个示例中,我们定义了一个抽象类 Shape ,它具有一个抽象方法 getArea ,
用于计算形状的面积。同时,它还包含了一个具体方法 displayArea ,用于打印面
积。 Circle 和 Square 类继承了 Shape ,分别实现了 getArea 方法。在其他类
中我们可以依赖抽象Shape而非 Square和Circle。

(3)高层模块
在某些情况下,我们可以通过将系统分解为更小的、可复用的组件来实现抽象。这些
组件可以独立地进行替换和扩展,从而使整个系统更加灵活。这种抽象方法往往在软
件架构和模块化设计中有所体现。
让我们来看另一个关于高层策略抽象的例子:插件化架构,案例中的插件十分简单,
仅仅只有一个接口,事实上我们日常实现插件功能时往往比这个复杂的多。
假设我们正在构建一个文本编辑器,它允许用户通过插件来扩展功能。我们可以将插
件系统作为一个高层策略抽象,以便于在不修改核心编辑器代码的情况下,添加新功
能。
首先,我们可以定义一个插件接口:

public interface Plugin {
String getName();
void execute();
}
public class TextEditor {
private List<Plugin> plugins = new ArrayList<>();
public void loadPlugin(Plugin plugin) {
plugins.add(plugin);
}
public void executePlugins(String name) {
for (Plugin plugin : plugins) {
plugin.execute();
}
}
}

现在,如果我们想要添加一个新功能,例如支持 Markdown 格式的预览,我们可以
创建一个实现 Plugin 接口的新类:

 Plugin 接口的新类:
最后,我们可以将新的插件加载到文本编辑器中:
通过采用插件化架构,我们将特定功能的实现与核心编辑器代码解耦,使得整个系统
更加灵活和可扩展。这是一个典型的高层策略抽象的应用示例。
有的朋友就说了,这还不是一个接口吗?你能用一个接口写出一个markdown的插件
吗?
总之,抽象是一个广泛的概念,它有助于我们将注意力从具体实现转移到行为和概
念。在依赖倒置原则的背景下,抽象可以帮助我们实现更加灵活、可扩展和可维护的
系统。
3、如何理解高层模块和底层模块
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被
调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题
的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类
似。我们拿 Tomcat 这个 Servlet 容器作为例子来解释一下。
public class MarkdownPreviewPlugin implements Plugin {
@Override
public String getName() {
return "Markdown Preview";
}
@Override
public void execute() {
// 实现 Markdown 预览功能
}
}
public static void main(String[] args) {
TextEditor editor = new TextEditor();
editor.loadPlugin(new MarkdownPreviewPlugin());
editor.executePlugin("Markdown Preview");
}

3、如何理解高层模块和底层模块
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被
调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题
的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类
似。我们拿 Tomcat 这个 Servlet 容器作为例子来解释一下。
 

从业务代码上讲,举一个简单的例子就是controller要依赖service的接口而不是实
现,service实现要依赖dao层的接口而不是实现,调用者要依赖被调用者的接口而不
是实现。
以一个简单的音频播放器为例,高层模块 AudioPlayer 负责播放音频,而音频文
件的解码由低层模块 Decoder 实现。为了遵循依赖倒置原则,我们可以引入一个抽
象的解码器接口:

interface AudioDecoder {
AudioData decode(String filePath);
}
class AudioPlayer {
private final AudioDecoder decoder;
public AudioPlayer(AudioDecoder decoder) {
this.decoder = decoder;
}
public void play(String filePath) {
AudioData audioData = decoder.decode(filePath);
// 使用解码后的音频数据进行播放
}
}
class MP3Decoder implements AudioDecoder {
@Override
public AudioData decode(String filePath) {
// 实现 MP3 文件解码
}
}

在这个例子中,我们将高层模块 AudioPlayer 和低层模块 MP3Decoder 解耦,使
它们都依赖于抽象接口 AudioDecoder 。这样,我们可以根据需要轻松地更换音频
解码器(例如,支持不同的音频格式),而不影响音频播放器的逻辑。为了支持新的
音频格式,我们只需要实现新的解码器类,并将其传递给 AudioPlayer 。
假设我们现在要支持 WAV 格式的音频文件,我们可以创建一个实现
AudioDecoder 接口的新类:

通过遵循依赖倒置原则,我们将高层模块 AudioPlayer 与低层模块 MP3Decoder
和 WAVDecoder 解耦,使它们都依赖于抽象接口 AudioDecoder 。这样的设计使
得我们可以轻松地为音频播放器添加新的音频格式支持,同时保持整个系统的灵活性
和可维护性。
再举一个例子,我们从更加高层的一个角度来理解:
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要
部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,
Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和
应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是
Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而
Tomcat 容器和应用程序依赖 Servlet 规范。这样做的好处就是tomcat中可以运行任
何实现了servlet规范的应用程序,同时我们编写的servlet实现(web)工程也可以
运行在不同的web服务器中。
class WAVDecoder implements AudioDecoder {
@Override
public AudioData decode(String filePath) {
// 实现 WAV 文件解码
}
}
public static void main(String[] args) {
AudioDecoder mp3Decoder = new MP3Decoder();
AudioPlayer mp3Player = new AudioPlayer(mp3Decoder);
mp3Player.play("example.mp3");
AudioDecoder wavDecoder = new WAVDecoder();
AudioPlayer wavPlayer = new AudioPlayer(wavDecoder);
wavPlayer.play("example.wav");
}

在这个例子中,我们将高层模块 AudioPlayer 和低层模块 MP3Decoder 解耦,使
它们都依赖于抽象接口 AudioDecoder 。这样,我们可以根据需要轻松地更换音频
解码器(例如,支持不同的音频格式),而不影响音频播放器的逻辑。为了支持新的
音频格式,我们只需要实现新的解码器类,并将其传递给 AudioPlayer 。
假设我们现在要支持 WAV 格式的音频文件,我们可以创建一个实现
AudioDecoder 接口的新类:


Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和
应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是
Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而
Tomcat 容器和应用程序依赖 Servlet 规范。这样做的好处就是tomcat中可以运行任
何实现了servlet规范的应用程序,同时我们编写的servlet实现(web)工程也可以
运行在不同的web服务器中。
class WAVDecoder implements AudioDecoder {
@Override
public AudioData decode(String filePath) {
// 实现 WAV 文件解码
}
}

public static void main(String[] args) {
AudioDecoder mp3Decoder = new MP3Decoder();
AudioPlayer mp3Player = new AudioPlayer(mp3Decoder);
mp3Player.play("example.mp3");
AudioDecoder wavDecoder = new WAVDecoder();
AudioPlayer wavPlayer = new AudioPlayer(wavDecoder);
wavPlayer.play("example.wav");
}

4、IOC容器
我们现在思考:依赖倒置的目的是,低层模块可以随时替换,以提高代码的可扩展
性。
其实我们学过spring的同学应该都清楚,在spring中实现这个很简单的,我们只需要
向容器中注入特定的bean就能切换具体实现。同时我们在编写日常代码时,有意无
意的都会遵循设计原则,我相信此时此刻不会再有同学问service层为什么要设计接
口了。
控制反转是一种软件设计原则,它将传统的控制流程颠倒过来,将控制权交给一个中
心化的容器或框架。
依赖注入是指不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在
外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
通过控制翻转和依赖注入结合,我们只要保证依赖抽象而不是实现,就能很轻松的替
换实现。如给容器注入一个myslq的数据,则所有依赖数据源的部分会自动使用
mysql,如果想替换数据源则仅仅需要给容器注入一个新的数据源就好了,不需要修
改一行代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值