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:
- If possible, limit the class to one constructor.
- Overloaded constructors provoke ambiguities: which designer should use dependency injection?
- Do not add any other logic to the constructor.
- Nowhere else in the class does a dependency need to be checked for null, since the constructor guarantees its existence
Virtues | disadvantages |
Implementation guaranteed | In some frameworks, it is difficult to use implementation through the constructor. |
Ease of implementation | The 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.
ConclusionRegardless 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 {
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.