[23 Design Patterns] Dependency Inversion Principle

Personal homepage:Golden Scales Stepping on the Rain

Personal introduction: Hello everyone, I amJinlin, a fledgling Java novice< /span>

Current situation: A 22nd ordinary undergraduate graduate. After many twists and turns, he now works for a large and well-known domestic daily chemical company, engaged in Java development

My blog: This is CSDN, where I learn technology and summarize knowledge. I hope to communicate with you and make progress together ~

Rely on abstractions rather than concrete implementations.

The purpose of dependency inversion is thatlow-level modules can be replaced at any time to improve the scalability of the code.

1. Principle

Rely on abstractions rather than concrete implementations. Following this principle canmake the design of the system more flexible, scalable and maintainable.

  1. High-level modules should not depend on low-level modules, they should all depend on abstractions.
  2. Abstraction should not depend on concrete implementation, concrete implementation should depend on abstraction.

Inversion here does mean " in reverse". In the dependency inversion principle, we need tochange the direction of the dependency relationship so that both high-level modules and low-level modules depend on Abstraction, while not a high-level module directly dependent on a low-level module. In this way, the dependency relationship changes from directly depending on the specific implementation to "depending on the abstract .

This "inverted" dependency reduces the coupling of the system and improves the maintainability and scalability of the system. Because, whenthe specific implementation of the low-level module changes , as long as the abstraction is not changed, the high-level modules do not need to be adjusted. So this principle is called the dependency inversion principle.

2. How to understand abstraction

When we are discussing abstraction in the Dependency Inversion Principle, definitelycannot just understand him is an interface. The purpose of abstraction is to shift the focus from specific implementation to concepts and behaviors, so that we can pay more attention to the essence of the problem when designing and writing code. By using abstractions, we can create more flexible, scalable, and maintainable systems.

In fact, abstraction is a very broad concept, which can includeinterfaces, abstract classes and A higher-level module composed of a large number of interfaces, abstract classes and implementations. By decomposing the system into smaller, reusable components, we can achieve higher levels of abstraction. These components can be replaced and expanded independently, making the overall system more flexible.

1. Interface

Interfaces are a common way of implementing abstraction in Java. An interface defines a set of method signatures that represent the behavior that a class that implements the interface should have. The interface itself does not contain a specific implementation, so it emphasizes the abstraction of behavior.

Suppose we are developing an online shopping system with an order processing module. The order processing module needs to interactwith different payment service providers (such as PayPal, Stripe, etc.). If we are directly dependent on the specific implementation of the payment service provider, we may need to make extensive modifications to the order processing module when changing the payment service provider or adding a new payment service provider. To avoid this, we should rely on interfaces rather than concrete implementations.

First, we define apayment service interface

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

Then, implement this interface for each payment service provider

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

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

Now, we can rely on the PaymentService interface in the order processing module instead of a concrete implementation:

public class OrderProcessor {
    private PaymentService paymentService;

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

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

        boolean paymentResult = paymentService.processPayment(order);

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

In this way, when we need to change the payment service provider or add a new payment service provider, we only need to provide a new implementation class without modifying the OrderProcessor class. We can inject different payment service implementations through the constructor at runtime, making the system more flexible and scalable.

2. Abstract class

Abstract classes are another way to achieve abstraction. Similar to interfaces, abstract classes can also define abstract methods to indicate the behaviors that subclasses should have. However, abstract classes can also contain part of the concrete implementation, which makes themmore flexible than interfaces.

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);
    }
}

In this example, we define an abstract class Shape that has an abstract method getArea that calculates the area of ​​a shape. At the same time, it also contains a specific method displayArea, which is used to print the area.

The Circle and Square classes inherit Shape and implement the getArea method respectively. In other classes we can rely on abstract Shape instead of Square and Circle.

3. How to understand high-level modules and low-level modules

The so-called division ofhigh-level modules andlow-level modules, To put it simply, in the call chain, the caller belongs to the higher layer and the callee belongs to the lower layer. In normal business code development, there is no problem for high-level modules to depend on low-level modules. In fact, this principle is mainly used to guide the design of the framework level, similar to the inversion of control mentioned earlier.

Use Tomcat, the Servlet container, as an example to explain. In terms of business code, a simple example is that the controller depends on the interface of the service rather than the implementation, the service implementation depends on the interface of the dao layer rather than the implementation, and the caller depends on the callee. The interface rather than the implementation.

Taking a simple audio player as an example, the high-level module AudioPlayer is responsible for playing audio, while the decoding of audio files is implemented by the low-level module Decoder. In order to follow the dependency inversion principle, we can introduce an abstract decoder interface:

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 文件解码
    }
}

In this example, we decouple the high-level module AudioPlayer and the low-level module MP3Decoder so that they both depend on the abstract interface AudioDecoder. This way we can easily change the audio decoder as needed (for example, to support different audio formats) without affecting the audio player logic. To support new audio formats, we just need to implement the new decoder class and pass it to AudioPlayer.

Suppose we now want to support audio files in WAV format, we can create a new class that implements the AudioDecoder interface:

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

Then, when creating the AudioPlayer object, we can choose to use MP3Decoder or WAVDecoder as appropriate:

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");
}

By following the dependency inversion principle, we decouple the high-level module AudioPlayer from the low-level modules MP3Decoder and WAVDecoder, making them all depend on the abstract interface AudioDecoder. This design allows us to easily add new audio format support to the audio player while maintaining the flexibility and maintainability of the entire system.

Tomcat

Tomcat is a container that runs Java web applications. The web application code we write only needs to be deployed under the Tomcat container, and then it can be called and executed by the Tomcat container.

According to the previous division principle,Tomcat is the high-level module, and the Web application code we wrote It is the low-level module.

There is no direct dependency between Tomcat and application code. Both rely on the same "Abstract", which is< /span>, at the same time, the servlet implementation (web) project we wrote can also run in different web servers. tomcat can run any application that implements the servlet specificationof this is thatadvantage. The Servlet specification does not depend on the implementation details of specific Tomcat containers and applications, but Tomcat containers and applications depend on the Servlet specification. TheSevlet specification

4. IOC container

The purpose of dependency inversion is thatlow-level modules can be replaced at any time to improve the scalability of the code.

In fact, those of us who have studied spring should all know that it is very simple to implement this in spring. We only need to inject specific beans into the container to switch the specific implementation. At the same time, when we write daily code, we will follow the design principles intentionally or unintentionally.

Inversion of control is a software design principle that reverses the traditional control flow and transfers control< a i=3>Leave it to acentralized container or framework.

Dependency injection refers to not creating dependent class objects inside the class through new(), but after the dependent class objects are created externally, through the constructor, Function parameters are passed (or injected) to the class for use.

By combining control flipping and dependency injection, we As long as you ensurerely on the abstraction rather than the implementation, you can easily replace the implementation. If you inject MySQL data into the container, all parts that rely on the data source will automatically use MySQL. If you want to replace the data source, you only need to inject a new data source into the container without modifying a line of code.

The article ends here. If you have any questions, you can point them out in the comment area~

I hope to work hard with the big guys and see you all at the top

Thank you again for your support, friends! ! !

Guess you like

Origin blog.csdn.net/weixin_43715214/article/details/134082065