• (50) Act to protect Your Code With Dependency Injection
  • Ben Weidig
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Jiang doesn’t know
  • Proofread by: GJXAIOU, Stuart childe

Decouple your code with dependency injection

No third party framework required

There are not many components that can stand on their own without being dependent on other components. In addition to creating tightly coupled components, we can leverage dependency injection (DI) to improve separation of concerns.

This article will introduce you to the core concepts of dependency injection away from third-party frameworks. All of the sample code will use Java, but the general principles described can be applied to any other language.


Example: Data processor

To visualize how to use dependency injection, we’ll start with a simple type:

public class DataProcessor {

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

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

    public BigDecimal calc(BigDecimal input) {
        return this.calculator.expensiveCalculation(input); }}Copy the code

DataProcessor has two dependencies: DbManager and Calculator. Creating them directly in our type has several obvious disadvantages:

  • A crash may occur when the constructor is called
  • The constructor signature may change
  • Bind tightly to explicit implementation types

It’s time to improve it!


Dependency injection

James Shore, author of The Art of Agile Development, puts it nicely:

“Dependency injection sounds complicated, but its concept is actually quite simple.”

The concept of dependency injection is actually quite simple: give a component everything it needs to do its job.

Typically, this means decoupling components by providing their dependencies externally, rather than creating dependencies directly within them, resulting in over-coupling between components.

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

  • Constructor injection
  • Properties into
  • Methods to inject

Constructor injection

Constructor injection, or initializers based dependency injection, means supplying all necessary dependencies as constructor parameters during instance initialization:

public class DataProcessor {

    private final DbManager manager;
    private final Calculator calculator;

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

    // ...
}
Copy the code

Thanks to this simple change, we can make up for most of our initial shortcomings:

  • Easy to replace:DbManagerCalculatorNo longer tied to a concrete implementation, you can now simulate unit tests.
  • Initialized and “ready” : We don’t have to worry about any child dependencies required by the dependencies (for example, database file names, significant digits, etc.) or the possibility that they can crash during initialization.
  • Mandatory: The caller knows exactly what was createdDataProcessorIs required.
  • Immutability: Dependencies remain the same.

Although constructor injection is the preferred approach of many dependency injection frameworks, it also has significant disadvantages. The biggest drawback is that all dependencies must be provided at initialization time.

Sometimes we can’t initialize a component ourselves, or at some point we can’t provide all of the dependencies of the component. Or we need to use another constructor. Once dependencies are set, they cannot be changed.

However, other injection types can be used to mitigate these problems.

Properties into

Sometimes, we cannot access the actual initialization method of a type, only an already initialized instance. Or at initialization, the required dependencies are not as clear as they are later.

In these cases, we can use property injection instead of relying on constructors:

public class DataProcessor {

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

    // ...

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

    public BigDecimal calc(BigDecimal input) {
        // WARNING: Possible NPE
        return this.calculator.expensiveCalculation(input); }}Copy the code

We don’t need constructors anymore, we can always provide dependencies after initialization. But there is a downside to this method of injection: volatility.

After initialization, we no longer guarantee that the DataProcessor is “always available.” Being able to change dependencies at will may give us more flexibility, but it also comes with the downside of too much runtime checking.

Now we have to deal with the possibility of nullPointerExceptions when accessing dependencies.

Methods to inject

Even if we separate dependency and constructor injection from/or property injection, we still have only one choice. What if there are situations where we need another Calculator?

We do not want to add additional attributes or constructor parameters to the second Calculator class, because a third such class may appear in the future. And every time calc(…) is called It is also not feasible to change the properties before, and it is likely to cause bugs by using the wrong properties.

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

public class DataProcessor {

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        returncalculator.expensiveCalculation(input); }}Copy the code

Now, calc (…). Is responsible for providing a suitable Calculator instance, and the DataProcessor class is completely separated from it.

Providing a default Calculator by mixing different injection types provides 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); }}Copy the code

The caller can provide another type of Calculator, but this is not required. We still have a decoupled, ready-to-use DataProcessor that can be adapted to a particular scenario.

Which injection method to choose?

Each dependency injection type has its advantages and there is no one “right way”. The choice depends entirely on your needs and circumstances.

Constructor injection

Constructor injection is my favorite, and is often favored by dependency injection frameworks.

It clearly tells us all the dependencies needed to create a particular component, and that these dependencies are not optional, but should be required throughout the component.

Properties into

Property injection is better suited for optional parameters, such as listening or delegation. Or we can’t provide dependencies at initialization.

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

Methods to inject

If the dependency is likely to be different each time you call it, method injection is best. Method injection further decouples the component so that the method itself holds dependencies rather than the entire component.

Remember, it’s not either/or. We are free to combine injection types as needed.

Inversion of control container

These simple dependency injection implementations can cover many use cases. Dependency injection is a great decoupling tool, but the reality is that we still need to create dependencies at some point.

But as applications and code bases grow, we may also need a more complete solution to simplify dependency injection creation and assembly.

Inversion of control (IoC) is an abstract principle of control flow. Dependency injection is one of the concrete implementations of inversion of control.

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

Some containers can detect relationships through reflection, while others must be configured manually. Some containers are based on runtime, while others generate all the code needed at compile time.

Comparing the differences between all containers is beyond the scope of this article, but let me use a small example to better understand the concept.

Example: Dagger 2

Dagger is a lightweight, compile-time dependency injection framework. We need to create a Module that knows how to build our dependencies, and we can Inject this Module later by simply adding the @Inject annotation.

@Module
public class InjectionModule {

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

    @Provides
    @Singleton
    static Calculator provideCalculator(a) {
        return new HighPrecisionCalculator(5); }}Copy the code

@Singleton ensures that only one instance of a dependency can be created.

To Inject a dependency, we simply add @Inject to a constructor, field, or method.

public class DataProcessor {

    @Inject
    DbManager manager;
    
    @Inject
    Calculator calculator;

    // ...
}
Copy the code

These are just the basics, which at first glance are unlikely to be very impressive. But inversion of control containers and frameworks not only decouple components, but also maximize the flexibility to create dependencies.

Thanks to advanced features, the creation process becomes more configurable and new ways of using dependencies are supported.

Advanced features

These features vary widely between different types of inversion of control containers and the underlying language, such as:

  • Proxy mode and lazy loading.
  • Lifecycle (e.g., singleton versus one instance per thread).
  • Automatic binding.
  • Multiple implementations of a single type.
  • Cyclic dependencies.

These features are the real power of inversion of control containers. You might think that features like “circular dependencies” aren’t a good idea, and they are.

However, if such strange code constructs are needed because of legacy code or immutable past bad designs, we now have the ability to do so.

conclusion

Designing code in terms of abstractions (such as interfaces) rather than concrete implementations helps reduce code coupling.

The interface must provide the unique information our code needs, and we can’t make any assumptions about the actual implementation.

“Programs should rely on abstraction, not concrete implementation” — Robert C. Martin (2000), Design Principles and Design Patterns

Dependency injection is a good way to do this by decoupling components. It allows us to write more concise code that is easier to maintain and refactor.

Choosing which of the three dependency injection types depends largely on the context and requirements, but we can also use a mix of the three types to maximize the benefits.

Inversion of control containers sometimes work almost magically to provide another convenient layout by simplifying component creation.

Should we use it everywhere? Of course not.

Like other patterns and concepts, we should apply them when appropriate, not when we can.

Never limit yourself to one way of doing things. Perhaps the factory pattern or even the much-hated singleton pattern is a better solution to meet your needs.


data

  • Inversion of control Containers and dependency Injection Patterns (Martin Fowler)
  • Dependency Inversion principle (Wikipedia)
  • Inversion of control (Wikipedia)

Inversion of control container

Java

  • Dagger
  • Spring
  • Tapestry

Kotlin

  • Koin

Swift

  • Dip
  • Swinject

C#

  • Autofac
  • Castle Windsor

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.