Time to tell me, unreasonable age, the sensible

preface

In the last article, we talked about the ISP principle and learned that when designing interfaces, we should design small interfaces and not make users rely on methods that are not available.

The word dependency is well understood by programmers. It means that I rely on whoever I use in this code. Dependence is easy to have, but can get dependence right, it requires a bit of thinking. If the dependency relationship is not handled well, a small change can affect a large area, and the most typical mistake is to reverse the dependency direction.

So what does that mean? Let’s discuss the dependency design principle: dependency inversion.

Who rely on who

Dependency Inversion Principle (DIP) :

High-level modules should not depend on low-level modules; both should depend on abstractions.

Abstractions should not depend on details; details should depend on abstractions.

The most important thing to learn about this principle is to understand inversion, and to understand inversion you need to understand what normal dependence looks like.

It would be natural to write code like this:

class CriticalFeature {
  private Step1 step1;
  privateStep2 step2; .void run(a) {
    // Perform the first step
    step1.execute();
    // Perform the second stepstep2.execute(); . }}Copy the code

However, there is a natural problem with this unexamined structure: higher-level modules depend on lower-level modules. In this code, the CriticalFeature class is the high-level class, Step1 and Step2 are the low-level modules, and Step1 and Step2 are usually concrete classes. While this is a natural way to write it, it is problematic.

In a real project, code is often directly coupled to a concrete implementation. For example, if we’re using Kafka for messaging, we create a KafkaProducer directly in our code to send messages. We might write code like this:

class Handler {
  private KafkaProducer producer;
  
  void send(a) {
    ...
    Message message = ...;
    producer.send(new KafkaRecord<>("topic", message); . }}Copy the code

You may ask, WHAT’s wrong with me using Kafka to send messages and creating a KafkaProducer? In fact, we need to take a long-term view of what has changed and what has not. Kafka is good, but it’s not a core part of the system, and we’ll probably replace it in the future.

You might be thinking, this is a key component of my implementation, how could I possibly change it? Software design needs to focus on the long term and look at the long term, and everything that is not within your control can be replaced. In fact, replacing a piece of middleware often happens. So relying on something that might change is not a good idea from a design perspective. So what should we do? This is where the upside down comes in.

An inversion is a reversal of this customary practice so that higher-level modules no longer depend on lower-level modules. If so, how will our functionality be accomplished? The answer comes from a famous saying in the computer industry:

All problems in computer science can be solved by introducing a layer of indirection.

Yes, introduce a layer of indirection. This layer of indirection refers to the abstraction described in DIP. That is, the code is missing a model of the low-level module’s role in the process.

Since this module plays the role of MessageSender, we can introduce a MessageSender model:

interface MessageSender {
  void send(Message message);
}

class Handler {
 
  void send(MessageSender sender) {... sender.send(message); . }}Copy the code

Given the message sender model, how do we combine Kafka with this model? Implement a Kafka message sender:

class KafkaMessageSender implements MessageSender {
    
  private KafkaProducer producer;
  
  public void send(final Message message) {
    this.producer.send(new KafkaRecord<>("topic", message)); }}Copy the code

Consumers can consume messages as follows:

Handler handler = new Handler();
handler.send(new KafkaMessageSender());
Copy the code

In this way, instead of relying directly on lower-level modules as before, the dependency relationship is “inverted” and the lower-level modules rely on interfaces defined by the higher-level modules. This has the advantage of decoupling the high-level modules from the low-level implementation.

If we were to replace Kafka with RabbitMQ in the future, we would just rewrite a MessageSender and nothing else would need to be changed. This way, we can keep the higher-level modules relatively stable and not change as the lower-level code changes.

class RabbitmqMessageSend implements MessageSender {
    
  private RabbitTemplate rabbitTemplate;
  
  public void send(final Message message) {
    rabbitTemplate.setExchange(exchangeKey);
    rabbitTemplate.setRoutingKey(routingKey);
    CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
    this.rabbitTemplate.convertAndSend(exchangeKey,routingKey,message,correlationId); }}Copy the code

Consumers can consume messages as follows:

Handler handler = new Handler();
handler.send(new RabbitmqMessageSend());
Copy the code

Dependent on abstraction

Abstractions should not depend on details; details should depend on abstractions.

In fact, this can be more easily understood as: relying on abstraction, from this point we can derive some more specific rules to guide coding:

  • No variable should point to a concrete class;
  • No class should inherit from a concrete class;
  • No method should overwrite a method already implemented in a parent class.

For example, the List declaration follows the first rule here:

List<String> list = new ArrayList<>();
Copy the code

In real projects, these coding rules are sometimes not absolute. If a class is particularly stable, we can use it directly, like a string class. Note, however, that this is very rare. Because most people write code that’s not that stable. Therefore, the above coding rules can be the rule covering most cases, and we need to pay special attention to the exceptions when they occur.

conclusion

  1. If abstraction, the key to the realization of open and close principle, is the goal of object-oriented design, dependency inversion principle is the main mechanism of object-oriented design.
  2. The purpose of the dependency inversion principle is to reduce the coupling between classes by programming to the interface, so we can satisfy this rule in projects by following the following four points in real programming:
    • Each class tries to provide an interface, an abstract class, or both.
    • Variables should be declared as interfaces or abstract classes.
    • No class should derive from a concrete class.
    • Follow the Richter substitution principle when using inheritance.