Dependency injection patterns. Part 1

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

    Constructor injection


    Appointment


    Break the hard link between a class and its required dependencies.

    Description


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

    How can we guarantee that the required dependency will always be available to the class being developed?

    This is ensured if all the calling classes pass the dependency as a constructor parameter.

    A class requiring a dependency must have a constructor with a public access modifier 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 an instance of the dependency 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 is checked for null , which protects the class from such improper use. Since the joint work of the compiler and the protection unit (checking for null ) ensures that the constructor argument is correct (unless an exception occurs (Exception )), the constructor can simply save the dependency for future use without figuring out the details of the actual implementation.

    It is good practice to declare a field that holds a value based, as " read-only » ( the Read-Only ). So we guarantee that, only once , the initialization logic in the constructor is executed : the field cannot be modified . This is not necessary for implementing 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 implementation through the constructor should be used


    Constructor injection should be used by default with dependency injection. It implements the most popular scenario when a class needs one or more dependencies and there are no suitable local defaults.

    Consider the best tips and practices for using implementation through the constructor:

    • If possible, you need to limit the class to one constructor.
    • Overloaded constructors provoke ambiguity: which constructor should use dependency injection?
    • Do not add any other logic to the constructor
    • The dependency is nowhere else in the class to be checked for null, since the constructor guarantees its presence

    Advantagesdisadvantages
    Deployment GuaranteedIn some frameworks, it is difficult to use implementation through the constructor.
    Ease of implementationThe requirement to immediately initialize the entire dependency graph (*)
    Ensuring a clear contract between the class and its clients (it’s easier to think about the current class without thinking about where the dependencies of the higher-level class come from)-
    Class complexity becomes apparent-
    (*) An obvious drawback of the constructor implementation is the requirement to immediately initialize the entire dependency graph - often already when the application starts. Nevertheless, although it seems that this drawback reduces the efficiency of the system, in practice it rarely becomes a problem. Even for complex object graphs, instantiating an object is an action that the .NET framework performs extremely quickly. In very rare cases, this problem can be really serious. Then we’ll use a life-cycle parameter called Delayed , which is quite suitable for solving this problem.

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

    Another reason for a large number of constructor parameters may be that too many abstractions are highlighted . This state of affairs may indicate that we began to disengage even from what we do not need to disengage at all : we began to make interfaces for objects that simply store data, or classes whose behavior is stable, does not depend on the external environment and should obviously be hidden inside the class rather than sticking out.

    Examples of using


    The introduction of the constructor ( Constructor Injection ) is the basic pattern of dependency injection and it is intensively used by the majority of programmers, even if they do not think about it. One of the main goals of most “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, implemented through an interface or abstract class; The State pattern takes the necessary context as a dependency, etc.

    Two examples that demonstrate the use of constructor injection in BCL use 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 by 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 to the constructor as a Stream value is null , will be generated ArgumentNullExceptions .

    
    // Декораторы
    var ms = new MemoryStream();
    var bs = new BufferedStream(ms);
    // Стратегия сортировки
    var sortedArray = new SortedList(
                            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 via the Constructor Injection should be the first way to manage dependencies. Its use will not only allow making the relations between classes more explicit, but also will allow to identify design problems when the number of constructor parameters exceeds a certain limit. In addition, all modern dependency injection containers support this pattern.

    Property Injection


    Appointment


    Break the hard link between a class and its optional dependencies.

    Description


    How can I enable dependency injection as an option in the 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 the public modifier : the type of this property must match the type of the dependency.

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

    Here, SomeClass is dependent on ISomeInterface . Clients can pass implementations of the ISomeInterface interface through the Dependency property . Please note that in contrast to the introduction of the designer, you do not can note field properties Dependency as " read-only » ( the Read Only ), since the caller is allowed to change the value of this property at any time of the class lifecycle SomeClass .

    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 because the Dependency property does not guarantee the return of an ISomeInterface instance . For example, the code shown below will throw a NullReferenceException , since the value of the Dependency property is null :

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

    This problem can be resolved by setting the default dependency in the instance constructor for the property combined with the addition of a null check in the property 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 the dependency during the class life cycle.

    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 inconsistent or unexpected behavior of the class, so it is better to protect yourself from 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 a DefaultDependency can be delayed until the property is requested for the first time. In this case, delayed initialization will occur. Note that local defaults are assigned through a setter with the public modifier , which ensures that all protection blocks are executed. The first block of protection ensures that the dependency being set is not null (we can catch NRE when using it ). Next the protective unit is responsible for ensuring that the relationship was established only one time.

    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 addiction later changes without any notice, while the client thinks that the addiction remains the same.

    When to apply property embedding


    Property injection should be applied only when there is a suitable local default for the class being developed , but at the same time you would like to leave the caller the opportunity to use another implementation of the dependency type. Property injection is best used if the dependency is optional . It should be considered that the properties are optional , because it is easy to forget to assign a value to them, and the compiler will not react in any way to this.

    It might seem tempting to set this default implementation for a given class at design time. However, if such an early default is implemented in another Assembly , using it this way will inevitably causean immutable reference to it, which negates many of the benefits of weak binding .

    Warnings


    • Use Property Injection for required dependencies. This is one of the most common mistakes in using this pattern. If a class necessarily needs some dependency, then it should be passed through the constructor so that immediately after creating the object it is in a valid state.
    • Using Foreign Default instead of Local Default. One of the dangers of using the default dependency implementation is the use of a specific dependency located in an assembly that our service should not be aware of ( Foreign Default ). If there are many such services, then we will get dozens of extra physical connections that will complicate understanding and maintenance. The default implementation must be in the same assembly ( Local Default ).
    • Complexity. The problem with using Property Injection for required dependencies is that it greatly increases the complexity of the class. A class with three fields, each of which can be null, leads to 8 different combinations of the state of the object. An attempt to check the state in the body of each open method leads to an unnecessary jump in complexity.
    • Attachment to the container. In most cases, we should use the container in a minimum number of places. Using Constructor Injection as a whole allows you to achieve this, since using it does not bind your class to any specific container. However, the situation changes when using Property Injection . Most containers contain a set of specialized attributes for managing dependencies through properties ( SetterAttribute for StructureMap , Dependency for Unity , DoNotWire for Castle Windsoretc.). Such a tight connection will not allow you to “change your mind” and switch to another container or completely refuse to use them.
    • Write-only properties. Not always do we want to expose a property that returns dependency. In this case, we will either have to set the property for writing ( set-only property ), which contradicts the generally accepted design principles on the .NET platform or use the method instead of the property ( Setter Method Injection ).

      public class SomeClass
      {
         private ISomeInterface _dependency;
         public void SetDependency(ISomeInterface dependency)
         {
            _dependency = dependency;
         }
      }

    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


    Implementation through the property ( the Property Injection ) is ideal for optional dependencies. They are quite suitable for strategies with a default implementation, but anyway, I would recommend using Constructor Injection and consider other options only if necessary.

    Also popular now: