【23种设计模式】依赖倒置原则

个人主页金鳞踏雨

个人简介:大家好,我是金鳞,一个初出茅庐的Java小白

目前状况:22届普通本科毕业生,几经波折了,现在任职于一家国内大型知名日化公司,从事Java开发工作

我的博客:这里是CSDN,是我学习技术,总结知识的地方。希望和各位大佬交流,共同进步 ~

要依赖于抽象而不是具体实现

依赖倒置的目的是,低层模块可以随时替换,以提高代码的可扩展性。

一、原理

要依赖于抽象而不是具体实现。遵循这个原则可以使系统的设计更加灵活、可扩展和可维护

  1. 高层模块不应该依赖于低层模块,它们都应该依赖于抽象。
  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 接口,而不是具体的实现:

public class OrderProcessor {
    private PaymentService paymentService;

    public OrderProcessor(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void processOrder(Order order) {
        // 其他订单处理逻辑...

        boolean paymentResult = paymentService.processPayment(order);

        // 根据 paymentResult 处理支付结果
    }
}

通过这种方式,当我们需要更换支付服务提供商或添加新的支付服务提供商时,只需要提供一个新的实现类,而不需要修改 OrderProcessor 类。我们可以在运行时通过构造函数注入不同的支付服务实现,使得系统更加灵活和可扩展。

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。

三、如何理解高层模块和底层模块

所谓高层模块低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类似。

用 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 接口的新类:

class WAVDecoder implements AudioDecoder {
    @Override
    public AudioData decode(String filePath) {
        // 实现 WAV 文件解码
    }
}

然后,在创建 AudioPlayer 对象时,我们可以根据需要选择使用 MP3Decoder 或 WAVDecoder:

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 和 WAVDecoder 解耦,使它们都依赖于抽象接口 AudioDecoder。这样的设计使得我们可以轻松地为音频播放器添加新的音频格式支持,同时保持整个系统的灵活性和可维护性。

Tomcat

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。

按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块

Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。这样做的好处就是tomcat中可以运行任何实现了servlet规范的应用程序,同时我们编写的servlet实现(web)工程也可以运行在不同的web服务器中。

四、IOC容器

依赖倒置的目的是,低层模块可以随时替换,以提高代码的可扩展性。

其实我们学过spring的同学应该都清楚,在spring中实现这个很简单的,我们只需要向容器中注入特定的bean就能切换具体实现。同时我们在编写日常代码时,有意无意的都会遵循设计原则

控制反转是一种软件设计原则,它将传统的控制流程颠倒过来,将控制权交给一个中心化的容器或框架

依赖注入是指不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

通过控制翻转依赖注入结合,我们只要保证依赖抽象而不是实现,就能很轻松的替换实现。如给容器注入一个MySQL的数据,则所有依赖数据源的部分会自动使用MySQL,如果想替换数据源则仅仅需要给容器注入一个新的数据源就好了,不需要修改一行代码。

文章到这里就结束了,如果有什么疑问的地方,可以在评论区指出~

希望能和大佬们一起努力,诸君顶峰相见

再次感谢各位小伙伴儿们的支持!!!

猜你喜欢

转载自blog.csdn.net/weixin_43715214/article/details/134082065
今日推荐