📜 ⬆️ ⬇️

Dependency injection patterns. Part 1

Let's look at the implementation of dependencies in .Net, since this topic is one of the must-haves for writing high-quality, flexible to change and tested code. We start with the necessary and basic patterns of dependency injection - implementation through the constructor and through the property. So let's go!

Implementation through the designer


Purpose


Break the rigid connection between the class and its required dependencies.

Description


The essence of the pattern is that all the dependencies required by a certain class are transferred to it as constructor parameters , represented as interfaces or abstract classes .

How can you ensure that the required dependency is always available to the developed class?
')
This is ensured if all callers pass the dependency as a constructor parameter.

A class requiring a dependency must have a constructor with a public access modifier (public) that receives an instance of the required dependency as an argument to the constructor:

private readonly IFoo _foo; public Foo(IFoo foo) { if (foo == null) throw new ArgumentNullException(nameof(foo)); _foo = foo; } 

Dependency is a required constructor argument. The code of any client that does not provide a dependency instance cannot be compiled. However, since both the interface and the abstract class are reference types, the calling code can pass a special null value to the argument, which makes the application compiled. Therefore, the class checks for null , which protects the class from such incorrect use. Since the compiler and protection block work together (checking for null ) ensures that the constructor argument is correct (unless an exception is thrown ), the constructor can simply keep the dependency for future use without figuring out the details of the actual implementation.

It is good practice to declare a field storing the value of a dependency as “ Read-only ”. So we guarantee that it runs, and only once , the initialization logic in the constructor: the field cannot be modified . This is not necessary to implement dependency injection, but in this way the code is protected from accidental field modifications (for example, from setting its value to null ) in some other place in the class code.

When and how should a deployment be used through a constructor


Implementation through the designer should be used by default with the implementation of dependencies. It implements the most popular scenario when a class needs one or more dependencies, and there are no suitable local defaults.

Consider the most best tips and practices on the use of implementation through the designer:


Virtuesdisadvantages
Implementation guaranteedIn some frameworks, it is difficult to use implementation through the constructor.
Ease of implementationThe requirement for immediate initialization of the entire dependency graph (*)
Providing a clear contract between the class and its customers (it’s easier to think about the current class, without thinking about where the dependencies of the higher-level class come from)-
The complexity of the class becomes apparent.-
(*) The obvious disadvantage of implementing a constructor is the requirement to immediately initialize the entire dependency graph — often already when the application is started. Nevertheless, although it seems that this disadvantage reduces the efficiency of the system, in practice it rarely becomes a problem. Even for complex object graphs, creating an object instance is an action that the .NET framework performs extremely quickly. In very rare cases, this problem can be really serious. Then we use the life cycle parameter, called Delayed , which is quite suitable for solving this problem.

A potential problem with using a constructor to pass dependencies may be an excessive increase in constructor parameters. Here you can read more.

Another reason for the large number of constructor parameters is that too many abstractions are allocated. This state of affairs may indicate that we began to abstract away even from what we do not need to abstract from : we began to create interfaces for objects that simply store data, or classes whose behavior is stable does not depend on the external environment and must clearly hide inside the class. instead of being exposed outside.

Examples of using


Constructor Injection is the basic pattern of dependency injection and is used extensively by most programmers, even if they don’t think about it. One of the main goals of the majority of “standard” design patterns (GoF patterns) is to obtain a loosely coupled design, so it is not surprising that most of them use dependency injection in one form or another.

So, the decorator uses dependency injection through the constructor; the strategy is passed through the constructor or “implemented” to the desired method; the command can be passed as a parameter, or it can take the surrounding context through the constructor. An abstract factory is often passed through a constructor and, by definition, is implemented via an interface or an abstract class; the State pattern takes as a dependency the necessary context, etc.

Two examples demonstrating the use of a constructor implementation in BCL are the System.IO.StreamReader and System.IO.StreamWriter classes.

Both of them get an instance of the System.IO.Stream class in the constructor.

 public StreamWriter(Stream stream); public StreamReader(Stream stream); 

The Stream class is an abstract class that acts as the abstraction with which StreamWriter and StreamReader perform their tasks. You can pass any implementation of the Stream class to their constructors, and they will use it. But if you try to pass a null value to the constructor as a Stream , ArgumentNullExceptions will be generated.

 //  var ms = new MemoryStream(); var bs = new BufferedStream(ms); //   var sortedArray = new SortedList<int, string>( new CustomComparer()); //  ResourceReader  Stream Stream ms = new MemoryStream(); var resourceReader = new ResourceReader(ms); // BinaryReader/BinaryWriter, StreamReader/StreamWriter //   Stream   var textReader = new StreamReader(ms); // Icon  Stream var icon = new System.Drawing.Icon(ms); 

Conclusion

Regardless of whether you use DI containers or not, implementation through a constructor ( Constructor Injection ) should be the first way to manage dependencies. Its use will not only make the relationship between classes more explicit, but also allow you to identify design problems when the number of constructor parameters exceeds a certain limit. In addition, all modern dependency deployment containers support this pattern.

Property Injection


Purpose


Break the rigid connection between a class and its optional dependencies.

Description


How can I allow dependency injection as an option in a class if there is a suitable local default?

Using a writable property, which allows the caller to set its value if it wants to replace the default behavior.

A class using a dependency must have a writable property with a public modifier: the type of this property must match the type of dependency.

 public class SomeClass { public ISomeInterface Dependency { get; set; } } 

Here, SomeClass is dependent on ISomeInterface . Clients can pass ISomeInterface interface implementations through the Dependency property. Note that, in contrast to the implementation of the constructor, you can not mark the Dependency property field as “ Read Only ”, since the caller is allowed to change the value of this property at any time during the SomeClass life cycle.

Other members of the dependent class can use the injected dependency to perform their functions, for example:

 public string DoSomething(string message) { return this.Dependency.DoStuff(message); } 

However, such an implementation is unreliable , since the Dependency property does not guarantee the return of an ISomeInterface instance. For example, the code shown below will generate a NullReferenceException , since the value of the Dependency property is null :

 var sc = new SomeClass(); sc.DoSomething("Hello world!"); 

This problem can be fixed by setting in the default instance dependency constructor for a property combined with adding a null test to the property's setter method.

 public class SomeClass { private ISomeInterface _dependency; public SomeClass() { _dependency = new DefaultSomeInterface(); } public ISomeInterface Dependency { get => _dependency; set => _dependency = value ?? throw new ArgumentNullException(nameof(value)); } } 

The difficulty arises if customers are allowed to change the value of a dependency during the life cycle of a class.

What should happen if a client tries to change the value of a dependency during the life cycle of a class?

The consequence of this may be contradictory or unexpected behavior of the class, so it is better to protect against such a turn of events.

 public class SomeClass { private ISomeInterface _dependency; public ISomeInterface Dependency { get => _dependency ?? (_dependency = new DefaultDependency()); set { //  1    if (_dependency != null) throw new InvalidOperationException(nameof(value)); _dependency = value ?? throw new ArgumentNullException(nameof(value)); } } } 

Creating DefaultDependency can be delayed until the property is requested for the first time. In this case, pending initialization will occur. Note that the local default is assigned through a setter with the public modifier, which ensures that all security blocks are executed. The first protection block ensures that the dependency you are installing is not null (we can use NRE when using it). The next security block is responsible for ensuring that the dependency is installed only once.

You may also notice that the dependency will be blocked after the property is read. This is done to protect customers from situations where the dependency later changes without any notice, while the customer thinks the dependency remains the same.

When to use property injection


Implementing a property should be used only if there is a suitable local default for the developed class, but you would like to leave the caller the opportunity to use another implementation of the dependency type. Implementing a property is best applied if the dependency is optional . It should be assumed that the properties are optional , because it is easy to forget to assign a value to them, and the compiler does not react to this.

It may be tempting to set this default implementation for this class at design time. However, if such a proactive default is implemented in another Assembly ( Assembly ), using it in this way will inevitably create an immutable reference to it, which negates many of the advantages of weak binding .

Cautions



Alternatives


If we have a class that contains an optional dependency, then we can use the old approach with two constructors:

 public class SomeClass { private ISomeInterface _dependency; public SomeClass() : this(new DefaultSomeInterface()) { } public SomeClass(ISomeInterface dependency) { _dependency = dependency; } } 

Conclusion


Property Injection is ideal for optional dependencies. They are quite suitable for strategies with the default implementation, but still, I would recommend using Constructor Injection and consider other options only if necessary.

Source: https://habr.com/ru/post/352530/


All Articles