Dependency injection and lifecycle

The planning and implementation of dependencies is the subject of various principles. Those who do not pay attention to the structure of dependencies during software development quickly end up in chaos. For many developers, however, this means that they are "all over the place" with interfaces. As the CCD Academy, we take a more differentiated view of the topic and recommend the use of the IOSP. What effect this has on Dependency Injection and the Lifecycle management of objects is described in this article.

Principles on the topic of dependencies

The Dependency Inversion Principle (DIP)

In connection with dependencies, the Dependency Inversion Principle (DIP) should be the most widely known. It states that a direct dependency between two classes can be reversed by inserting an interface. Details can thus be made dependent on abstractions.

From the Dependency Inversion Principle to the Integration Operation Segregation Principle - Clean Code Developer Academy - Trainings - Fig. 1
Figure 1
from the Dependency Inversion Principle to the Integration Operation Segregation Principle - Contract between A and B - Fig. 2
Figure 2

The change of direction results from the fact that the implementing class (in this case B) is derived from the interface or Contract (here Bc) is dependent. The direction of dependency is the reverse of the original direction. The architectural patterns Hexagonal architectureOnion Architecture and Clean Architecture take advantage of this: the dependencies run from the outside to the inside through the DIP.

The Integration Operation Segregation Principle (IOSP)

Another principle addressed by the Integration Operation Segregation Principle (IOSP) with the dependency structure. Instead of accepting the structure as a given and using an interface to alleviate the pain, a completely different structure is recommended here. The aspect of integration is separated out as an independent aspect. This results in methods whose only task is to integrate other methods by calling them. Such integration methods therefore have maximum dependency and are not responsible for anything else. For this reason, interfaces can be completely dispensed with here. For details, see the Article DIP or IOSP.

Integration Operation Segregation Principle (IOSP)
Figure 3

In many cases, dependency injection could even be dispensed with, as dummies only have to be used very rarely in the test. However, in addition to testability, there is another Reason for dependency injection: the Lifecycle management of objects.

Dependency injection

Dependency injection is not a principle but a pattern. Nevertheless, it should be mentioned here. With the term Dependency Injection is the process of providing a class with its dependencies from outside. This means that a class does not create its dependencies itself by calling the respective constructors, but expects the required instances via its own structure parameters. For example, if the class NewCustomer a Repositoryto save a new customer object in the database, then it expects the repository via a parameter in the constructor (here using an interface).

				
					public class NewCustomer
{
    private readonly ICustomerRepository _customerRepository;

    public NewCustomer(ICustomerRepository customerRepository) {
        _customerRepository = customerRepository;
    }
}
				
			

Lifecycle management

Transient

Objects have a lifetime. This begins when the object is instantiated and ends when it is disposed of. On managed platforms such as CLR and JVM, objects are removed from the memory fully automatically by the garbage collector if there is no longer a reference to an object. As long as an object can still be accessed via a reference, it is "alive". If it can no longer be accessed via a reference, it can be removed. The lifecycle of such objects is called transient labeled.

Singleton

Things get a little more difficult when you add the topic of state. A class can hold state by containing fields. Fields are located on the heap, i.e. they are provided for each instance of an object. This raises the question of whether we want to have one or more instances of an object within an application. In addition to transient, we have the ever-popular Singleton as a second lifecycle.

So up to this point we have been dealing with two possible lifetimes of the objects: we have to choose between a Singleton or a transients object. A Singleton means that there is exactly one instance of the object within the application. This is used, for example, to provide global configuration data, such as the connection string to the database, which applies to the entire application. With transient objects In contrast, an instance is created as required. This is useful, for example, if certain objects are required for a use case at short notice. As long as the objects exist, they may also maintain a state. As long as this state does not need to be maintained over a longer period of time, a transient object is usually the solution.

Scoped

A third variant arises when an application can be used by several users at the same time, as is the case with web applications. In this case, each user receives their own instance of an object and therefore has their own state. The runtime of such objects should often be linked to the interaction of a user. This so-called scoped Lifecycle means that one object is created per user or per scope. It is also released again when the user's interaction is complete. The scope must be defined in the application. In the case of a web application, this could be the request, for example.

Dependency injection

No matter how you design your dependencies, whether with dependency inversion (DIP) or the separation of integration and operation (IOSP), the objects have to be instantiated and assembled somehow. Dependency injection is a good way to remain flexible. And to avoid having to resolve the required objects "by hand", it is best to use a dependency inversion container (also known as an inversion of control container). The terms DI Container and IoC Container are synonymous.

Use DI Container

The prerequisite for automating lifecycle management is that the instances can be injected. The most common variant is dependency injection via constructor parameters. This has the advantage that it expresses a clear necessity. Alternatively, optional dependencies can be resolved via property injection. This is useful for dependencies that are optional.

Today, DI containers are a standard tool. Frameworks such as ASP.NET Core or Spring (Java) provide this infrastructure and rely on this pattern. The use of a DI container makes a lot of sense in terms of changeability: if the dependency structure and therefore the signature of a constructor changes, the DI container ensures that everything is put together at runtime. This reduces the refactoring effort when changes are made to a constructor. The use of a DI container therefore offers a simplification in terms of changeability.

Secondly, the DI container offers the option of specifying the lifetime of objects via configuration. This means that nobody has to implement a singleton. Instead, the DI Container is instructed to deliver the objects of a specific type as a singleton. In this way, the DI Container ensures that exactly one instance of this type exists at runtime.

The DI container enables the retrieval of instances and ensures that all required objects are provided recursively.

Code examples

The following tests show how the three lifetimes differ for objects.

				
					using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;

namespace dipfordi;

public class DI_Container_Demos
{
    [TearDown]
    public void Teardown() {
        Foo.NumberOfInstances = 0;
    }
    
    [Test]
    public async Task Without_ctor_dependencies() {
        await using var provider = new ServiceCollection()
            .AddTransient()
            .BuildServiceProvider();
        
        var foo = provider.GetService();
        Assert.That(foo, Is.Not.Null);
    }

    [Test]
    public async Task Ctor_with_dependencies() {
        await using var provider = new ServiceCollection()
            .AddTransient()
            .AddTransient()
            .BuildServiceProvider();
        
        var bar = provider.GetService()!
        Assert.That(bar, Is.Not.Null);
        Assert.That(bar.Foo, Is.Not.Null);
    }

    [Test]
    public async Task Multiple_transient_instances() {
        await using var provider = new ServiceCollection()
            .AddTransient()
            .BuildServiceProvider();
        
        var foo1 = provider.GetService();
        var foo2 = provider.GetService();
        var foo3 = provider.GetService();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(3));
        Assert.That(foo1, Is.Not.SameAs(foo2));
        Assert.That(foo2, Is.Not.SameAs(foo3));
    }
    
    [Test]
    public async Task One_Singleton_Instance() {
        await using var provider = new ServiceCollection()
            .AddSingleton()
            .BuildServiceProvider();
        
        var foo1 = provider.GetService();
        var foo2 = provider.GetService();
        var foo3 = provider.GetService();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(1));
        Assert.That(foo1, Is.SameAs(foo2));
        Assert.That(foo2, Is.SameAs(foo3));
    }
    
    [Test]
    public async Task Diverse_scoped_Instances() {
        await using var provider = new ServiceCollection()
            .AddScoped()
            .BuildServiceProvider();

        using (var scope1 = provider.CreateScope()) {
            var foo1 = scope1.ServiceProvider.GetService();
            var foo2 = scope1.ServiceProvider.GetService();
        }

        using (var scope2 = provider.CreateScope()) {
            var foo3 = scope2.ServiceProvider.GetService();
            var foo4 = scope2.ServiceProvider.GetService();
        }
        var foo5 = provider.GetService();
        Assert.That(Foo.NumberOfInstances, Is.EqualTo(3));
    }
}

public class Foo
{
    public static int NumberOfInstances;

    public Foo() {
        NumberOfInstances += 1;
    }
}

public class Bar(Foo foo)
{
    public readonly Foo Foo = foo;
}
				
			

Conclusion

The topic of dependencies is the never-ending story of software development. So many mistakes continue to be made that the quality of the code suffers significantly. With Code quality I mean that the two values Changeability and Correctness can only be achieved with difficulty. The structure of the dependencies has a significant influence on whether the code is easy to understand and easy to change. The structure also determines whether it is easy to test the code with automated tests. The first task is therefore to ensure a good dependency structure.

Dependency injection and DI containers can also be used as tools. They simplify the refactoring of constructors and make it possible to decide by configuration how instances of a type should be created. Even different implementations of an interface can be resolved via the DI container and reduce the coupling at compile time because the resolution only takes place at runtime.

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish