Decoupling your code [translated] dependency injection

With dependency injection decoupling your code

You do not need third-party frameworks

[Icons8 team] (https://unsplash.com/@icons8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) taken in [Unsplash] (https://unsplash.com/s/photos/ingredients?utm_source=unsplash&utm_medium=referral&utm_content= creditCopyText)

Not many components that can exist independently without depending on other components. In addition to creating tightly coupled components, we can also use dependency injection (DI) to improve the separation of concerns .

This article will introduce you out of a third-party dependency injection framework of core concepts. All sample code will use Java, but the general principles described also apply to any other language.


Example: the data processor

How to use dependency injection in order to allow more figurative, we start from a simple type:

public class DataProcessor {

    private final DbManager manager = new SqliteDbManager("db.sqlite");
    private final Calculator calculator = new HighPrecisionCalculator(5);

    public void processData() {
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        return this.calculator.expensiveCalculation(input);
    }
}
复制代码

DataProcessorThere are two dependencies: DbManagerand Calculator. Created directly in our type they have several distinct disadvantages:

  • A crash may occur when you call the constructor
  • The constructor signature may change
  • Tightly bound to explicitly implement type

It is time to improve it!


Dependency Injection

"Art of Agile Development" author James Shore well pointed out :

"Dependency Injection sounds complicated, but in fact it's very simple concept."

Dependency injection concept is actually very simple: to provide all the components required to complete its work.

Typically, this means to decouple components by providing components from external dependencies, not to create a dependency directly in the assembly, so that excessive coupling between components.

We can provide the necessary dependencies for the example of a number of ways:

  • Constructor injection
  • Properties injection
  • Method Injection

Constructor injection

Constructor injection, or dependent on said initializer injection means providing all required dependencies during initialization example, as parameters to the constructor:

public class DataProcessor {

    private final DbManager manager;
    private final Calculator calculator;

    public DataProcessor(DbManager manager, Calculator calculator) {
        this.manager = manager;
        this.calculator = calculator;
    }

    // ...
}
复制代码

Because of this simple change, we can make up most of the shortcomings of the beginning:

  • Easy to replace: DbManagerand Calculatorno longer be bound to a particular implementation, the analog unit can now be tested.
  • Has been initialized and is "ready": We do not have to worry about dependencies require any child dependencies (for example, database file name, significant digits (translator's note) , etc.), they do not have to worry about the possibility of a crash may occur during initialization .
  • Mandatory: the caller know exactly create the DataProcessordesired content.
  • Invariance: dependency always as ever.

Although constructor dependency injection is the method of choice for many injection framework, but it also has significant disadvantages. The biggest drawback is: You must provide all dependency upon initialization.

Sometimes, we can not initialize a component of their own, or at some point we can not provide all the dependencies of the components. Or we need to use another constructor. Once you've set dependency, we will not be able to change them.

But we can use other types of injection to alleviate these problems.

Properties injection

Sometimes, we can not access the actual type of initialization method can only access the instance has been initialized. Or during initialization, the required dependencies are not as clear as after.

In these cases, we can use the properties of injection rather than relying on constructors:

public class DataProcessor {

    public DbManager manager = null;
    public Calculator calculator = null;

    // ...

    public void processData() {
        // WARNING: Possible NPE
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        // WARNING: Possible NPE
        return this.calculator.expensiveCalculation(input);
    }
}
复制代码

We no longer need a constructor, after initialization we can offer dependencies at any time. However, this injection method also has disadvantages: volatility .

After initialization, we no longer guarantee DataProcessoris "readily available" in. You can freely change the dependency may give us more flexibility, but will also bring too many shortcomings to check runtime.

Now we must deal occurs when accessing a dependency NullPointerExceptionpossibilities.

Method Injection

Even if we would dependency and constructor injection and / or implantation separation properties, we still have only one choice. If we need another in some cases, Calculatorhow to do it?

We do not want to second Calculatoradditional attributes or class constructor argument, because the third such classes may occur in the future. And each call calc(...)to change the properties is not feasible before, and probably because of the wrong property caused by bug.

A better approach is parameterized call the method itself and its dependencies:

public class DataProcessor {

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return calculator.expensiveCalculation(input);
    }
}
复制代码

Now, the calc(...)caller is responsible for providing a suitable Calculatorexample, and the DataProcessorclass with completely separate.

The use of different types of injection by mixing to provide a default Calculator, so you can get more flexibility:

public class DataProcessor {

    // ...

    private final Calculator defaultCalculator;
    
    public DataProcessor(Calculator calculator) {
        this.defaultCalculator = calculator;
    }

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return Optional.ofNullable(calculator)
                       .orElse(this.calculator)
                       .expensiveCalculation(input);
    }
}
复制代码

The caller can provide another type Calculator, but this is not necessary is. We still have a decoupling, readily available DataProcessor, it can be adapted to the specific scene.

The choice of which injection method?

Dependency Injection each type has its own advantages and none of the "right way." Specific choice depends entirely on your actual needs and circumstances.

Constructor injection

Constructor Injection is my favorite, it often favored by dependency injection framework.

It clearly tells us create all the dependencies needed for a particular component, and these dependencies are not optional, these dependencies in the overall assembly should all be required.

Properties injection

Injecting property more suitable for optional parameters, such as monitor or delegate. But we can not provide or dependency during initialization.

Other programming languages, such as Swift, extensive use with attributes delegate pattern . Therefore, the injection will allow developers to use the property for other languages more familiar with our code.

Method Injection

If the dependencies may be different in each call, then the use of injected better and better. The method of injecting further decoupling components, a method which per se holds a dependency, instead of the entire assembly.

Remember, it's not either-or. We can inject various types of free combination according to need.

Inversion of Control container

These simple dependency injection cover many use cases can be achieved. Dependency injection is a good tool for decoupling, but in fact we still need to create a dependency at some point.

But with the growth of applications and code libraries, we may also need a more complete solution to simplify the creation and dependency injection assembly process.

Inversion of Control (IoC) is to control the flow of abstract principles. Dependency injection is one specific implementation of the inversion control.

Inversion of Control container is a special type of an object that knows how to instantiate and configure other objects, it also knows how to help you perform dependency injection.

Some containers may be detected by reflection relationships, while others must be manually configured. Some of the container, while others generate all the code needed at compile time based on running time.

Compare different from all the container is beyond the scope of this article, but let me through a small example to better understand this concept.

Example: Dagger 2

Dagger is a lightweight framework for compile-time dependency injection. We need to create a Module, you know how to build our dependency later on we just add @Injectannotations can inject this Module.

@Module
public class InjectionModule {

    @Provides
    @Singleton
    static DbManager provideManager() {
        return manager;
    }

    @Provides
    @Singleton
    static Calculator provideCalculator() {
        return new HighPrecisionCalculator(5);
    }
}
复制代码

@Singleton To ensure that only create an instance of a dependency.

To inject dependencies, we only need to @Injectconstructors, fields or methods to add.

public class DataProcessor {

    @Inject
    DbManager manager;
    
    @Inject
    Calculator calculator;

    // ...
}
复制代码

These are just some of the basics, at first glance can not give people impressed. But the inversion of control container and framework not only decoupling components, but also to create a dependency of flexibility is maximized.

By providing advanced features, creation process configurability becomes stronger, and supports a new method using dependencies.

Advanced Features

These characteristics vary widely between different types of containers and a bottom inversion control language, for example:

  • Proxy mode and lazy loading.
  • Life cycle (e.g.: Singleton with a thread per instance).
  • Automatic binding.
  • A single type of multiple implementations.
  • Circular dependency.

Inversion of Control container These characteristics are true ability. You might think that such "circular dependency." This feature is not a good idea, indeed.

If, however, due to the legacy code or design errors of the past can not be changed while the need for such a strange code constructs, we now have the ability to do so.

to sum up

We should be based on abstract (eg interfaces) rather than specific implementation to design code, which can help us to reduce code coupling.

The interface must provide a unique code information we need, we can not make any assumptions about the actual achievement.

"Procedure should rely on the abstract rather than concrete realization" - Robert C. Martin (2000), " Design Principles and Design Patterns"

Dependency injection is to achieve this in a good way by decoupling components. It enables us to write more concise, easier code maintenance and reconstruction.

Choice of three types of dependency injection which depends largely on the environment and needs, but we can also mix the three types to maximize profits.

Inversion of Control container sometimes almost in a magical way by simplifying the process of creating components to provide another convenient layout.

We should use it everywhere it? of course not.

Like other models and concepts, we should apply them at the right time, it can be used instead.

Never put limitations on doing things their own way. Perhaps factory pattern and even disgust widely Singleton pattern is a better solution to meet your needs.


data


Inversion of Control container

Java

Kotlin

Swift

C#

If you find there is a translation error or other areas for improvement, welcome to Denver translation program to be modified and translations PR, also obtained the corresponding bonus points. The beginning of the article Permalink article is the MarkDown the links in this article on GitHub.


Nuggets Translation Project is a high-quality translation of technical articles Internet community, Source for the Nuggets English Share article on. Content covering Android , iOS , front-end , back-end , block chain , product , design , artificial intelligence field, etc., you want to see more high-quality translations, please continue to focus Nuggets translation program , the official micro-blog , we know almost columns .

Guess you like

Origin juejin.im/post/5e80a1d46fb9a03c8c03fe51