Как правильно сделать Dependency Injection. Теоретический вопрос

165
01 апреля 2019, 23:40

Изучаю вопрос связанный с IOC (Инверсия управления), как я понял это некий абстрактный паттерн, который говорит,что нужно делать слабое связывание между элементами системы, и элементы зависят только от абстракции. А реализация этого паттерна это или Dependency injection или использование контейнера. Иcпользование контейнера более менее понятно, на примере Autofac. А вот как сделать просто Dependency Injection. Правильно ли я делаю реализация через конструктор? И что значит делать инжекцию через метод? Код

 public interface IServise
 {
    string Print();
 }

Класс ,который его реализует.

 class Class1:IServise
 {
     public string Print()
     {
         string g = "строка";
         return g;
     }
 }

Класс в который делаю инжекцию.

class  ClassForInject
{
        private IServise _srServise;
        private string _a;
        //  в конструктор внедряю зависимость
        public ClassForInject(IServise _srServise)
        {
           _a=_srServise.Print();
        }

       public void Metod2()
       {
           Console.WriteLine(this._srServise.Print());
           // или 
           Console.WriteLine(_a);
       }
   }

И метод Main

  class Program
  {
       static void Main(string[] args)
       {
             // тут я должен в конструктор добавить ссылку
             ClassForInject k = new ClassForInject();
             k.Metod2();
             Console.ReadKey();
        }
   }
Answer 1

Для начала, давайте разберемся, что же такое инверсия управления (inversion of control - IoC). Когда мы пишем какую-то простую программу, она (программа) выглядит как то так:

Точка входа -> наш код -> наш код вызывает и контролирует выполнение стороннего кода. 

Например,

void Main()
{
    Console.WriteLine($"What is your name?");
    var name = Console.ReadLine();
    Console.WriteLine($"Hello, {name}!");
}

Что тут произошло? Сразу после точки входа НАШ код использовал консоль, чтобы запросить юзера его имя, считать его и вывести приветствие. Наш код полностью управляет процессом работы нашего приложения. Наш код отвечает за старт, за обработку ошибок, за все, что происходит в приложении. То есть управление исходит от нашего кода.

Что же теперь означает инверсия управления? Инверсия управления означает ситуацию, когда ВНЕШНИЙ код начинает вызывать наш код, когда внешний код начинает управлять нашим кодом. Покажем на примере. Допустим, мы пользуемся фрейморком, который состоит из 1 класса:

// Это сторонний код! Не наш!
public class Bootstrapper
{
    public void RunAndGo(Action action) => action?.Invoke();    
}

Теперь, изменим наш код следующим образом:

// НАШ код
void Main()
{
    var boot = new Bootstrapper();
    boot.RunAndGo(MyLogic);
}
public void MyLogic()
{
    Console.WriteLine($"What is your name?");
    var name = Console.ReadLine();
    Console.WriteLine($"Hello, {name}!");
}

Вроде ничего особенно не изменилось, но теперь сторонний код решает, когда наша логика отработает. Сторонний код запускается и вызывает нашу функцию когда ему удобно. Сторонний код может быть изменен, дополнен, обновлен, без изменения нашего кода. Обычно, в подобных сценариях, программист может сконцентироваться на конкретной задаче, что он хочет решить, и все, больше его ничего не должно заботить. Сторонний код обращается к вам: скажи мне что ты хочешь и я позабочусь обо всем остальном. Примеры этого: контроллеры в asp.net - вы говорите, что они должны возвращать и инфраструктура позаботится об остальном; PRISM в WPF - вы описываете модули, представления, регионы, логику, фреймворк позаботится чтобы это все согласовано работало; в .NET Framework - вы говорите фреймворку, что вы хотите выполнить, и он позаботится о совместимости с платформой. То есть IoC - это широкий термин.

Теперь о внеднерии зависимости (Dependency injection). Для начала, разберемся с тем, что же такое зависимость. Допустим есть класс

public class Logic : ILogicElement
{
    ConsoleReader _reader;
    ConsoleWriter _writer;
    public Logic()
    {
        _reader = new ConsoleReader();
        _writer = new ConsoleWriter();
    }
    public void Go()
    {
        _writer.Write("What is your name?");
        var name = _reader.Read();
        _writer.Write($"Hello, {name}!");
    }
}

Мы видим, что класс явным образом использует 2 других класса (реализация которых нас сейчас не интересует). Наш класс не только взял на себя отвественность за создание других классов, но и ещё его работа явно ЗАВИСИТ от того, как другие классы реализованы, так как он явно вызывает их методы внутри себя. То есть такие вспомогательные классы назовем зависимостями, потому что наш класс явно от них ЗАВИСИТ.

Но как выглядит поток работы с этими зависимостями? Примерно так:

Наш класс --> наш класс создает зависимости самостоятельно

Вообще, есть довольно много вопросов по этому поводу. Например - а надо ли нашему классу вообще знать о том, как его зависимости были созданы и как они реализованы? Его ли это ответсвенность? Тут, конечно, нарушение принципа единственности ответственности. Что тут можно сделать? А вот что:

public class Logic : ILogicElement
{
    ConsoleReader _reader;
    ConsoleWriter _writer;
    public Logic(ConsoleReader reader, ConsoleWriter writer)
    {
        _reader = reader;
        _writer = writer;
    }
    public void Go()
    {
        _writer.Write("What is your name?");
        var name = _reader.Read();
        _writer.Write($"Hello, {name}!");
    }
}

Теперь поток выполнения программы выглядит так:

Вызывающий код создает зависимости для нашего класса --> наш класс

Как видите, если раньше наш класс управлял зависимостями, теперь же сторонний код управляет всем. При создании нашего класса зависимости должны будут быть ВНЕДРЕНЫ в констурктор нашего класса - это и есть суть внедрения зависимости. Возможно, вы уже уловили связь между внедрением зависимости и IoC. Она действительно есть - внедрение зависимостей - частный случай IoC.

Внедрение зависимостей может происходить не только в констурктор, но и в свойство или метод. Но лично я использую только констуктор для этого - так как:

  • Без указания всех зависимостей в конструторе класс не может быть создан
  • Все зависимости находятся в одном месте, их все явно видно

Чтож, идем дальше. Принцип инверсии зависимостей.

В отличие от IoC или внедрения зависимостей, принцип инверсии зависимостей является одним из принипов SOLID и гласит следующее:

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Давайте начем с модулей. Что такое модуль? Модуль, если говорить своим языком, то это набор взаимосвязанных классов. Связанных именно своим назначением. Вы наверняка уже не раз слышали фразу "модуль бизнес-логики" или "модуль доступа к данным". Далее, говорится, что у модулей есть уровни, где одни модули выше других. По опыту скажу, что обычно модуль верхнего уровня вызывает модуль нижнего.

Давайте ещё раз взглянем на пример:

public class Logic : ILogicElement
{
    ConsoleReader _reader;
    ConsoleWriter _writer;
    public Logic(ConsoleReader reader, ConsoleWriter writer)
    {
        _reader = reader;
        _writer = writer;
    }
    public void Go()
    {
        _writer.Write("What is your name?");
        var name = _reader.Read();
        _writer.Write($"Hello, {name}!");
    }
}

Что тут относится к модулю верхнего уровня? Определенно, наша логика - это верхний уровень. Что относится к модулям нижнего уровня? Очевидно, классы чтения/записи в консоль не относятся к логике, то есть они относятся к модулю нижнего уровня, так как наш модуль их использует.

Зависит ли наш верхний модуль логики от модуоя нижнего уровня - работы с консолью? Очевидно, да! Ведь он о нем знает и использует его методы. То есть если изменить сигнатуры методов модуля нижнего уровня, то придется менять и модуль верхнего. Как с этим быть? Ответ на поверхности: Оба типа модулей должны зависеть от абстракций. - то есть нам нужна абстракция. Вот такая, например

public interface IUserInteractionReader { string Read(); }
public interface IUserInteractionWriter { void Write(string message); }

Классы для работы с консолью

public class ConsoleReader : IUserInteractionReader 
{ 
    public string Read() => Console.ReadLine(); 
}
public class ConsoleWriter : IUserInteractionWriter 
{ 
    public void Write(string message) => Console.WriteLine(message); 
}

Класс логики

public class Logic : ILogicElement
{
    IUserInteractionReader _reader;
    IUserInteractionWriter _writer;
    public Logic(IUserInteractionReader reader, IUserInteractionWriter writer)
    {
        _reader = reader;
        _writer = writer;
    }
    public void Go()
    {
        _writer.Write("What is your name?");
        var name = _reader.Read();
        _writer.Write($"Hello, {name}!");
    }
}

Видите, что произошло? Наша логика больше не знает ни о каком модуле работы с консолью. Наша логика теперь зависит от 2 интерфейсов - абстракций. А что модуль консоли? Он теперь тоже зависит от этих интерфейсов, так как классы должны эти интерфейсы реализовывать.

Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций - это, по идее, означает простую вещь - нашей логике, по идее, надо только считать строку и записать строку. Это 2 очень простые для нашего класса операции, так как он полностью полагается на реализацию интерфейсов. Классу не важно КАК именно реалиованы чтение и запись - не имеет значения, работа это с консолью или опрос пользователя идет по электронной почте или телефону - эти детали никак не должны влиять на предоставленный интерфейс, поскольку самое важное, что требуется уже указано в интерфейсе. То есть детали реализации не должны влиять на абстрацию.

Собираем все вместе

Итак, допустим у нас есть наши модули и абстракции

public interface ILogicElement { void Go(); }
public interface IUserInteractionReader { string Read(); }
public interface IUserInteractionWriter { void Write(string message); }
public class ConsoleReader : IUserInteractionReader 
{ 
    public string Read() => Console.ReadLine(); 
}
public class ConsoleWriter : IUserInteractionWriter 
{ 
    public void Write(string message) => Console.WriteLine(message); 
}
public class Logic : ILogicElement
{
    IUserInteractionReader _reader;
    IUserInteractionWriter _writer;
    public Logic(IUserInteractionReader reader, IUserInteractionWriter writer)
    {
        _reader = reader;
        _writer = writer;
    }
    public void Go()
    {
        _writer.Write("What is your name?");
        var name = _reader.Read();
        _writer.Write($"Hello, {name}!");
    }
}

Далее, есть какой то фреймворк (в качестве DI контейнера я испольовал Unity)

public class Bootstrapper
{
    IUnityContainer _container;
    public Bootstrapper(IUnityContainer container)
    {
        _container = container;
    }
    public void RunAndGo()
    {   
        foreach(var logic in _container.ResolveAll<ILogicElement>())
            logic.Go();
    }   
}

Для этого нашего фреймворка, нам надо сначала настроить контейнер. Выглядит это примерно так:

void Main()
{
    var container = new UnityContainer();
    container.RegisterType<IUserInteractionReader, ConsoleReader>();
    container.RegisterType<IUserInteractionWriter, ConsoleWriter>();            
    container.RegisterType<ILogicElement, Logic>("my awesome logic name");
    container.Resolve<Bootstrapper>().RunAndGo();
}

Что мы тут происходит? Тут мы только производим настройку всех деталей, что нам потребуются, и передаем выполнение фреймворку (это я про IoC), все зависимости логики объявлены в конструкторе (это про внедрение зависимостей - для внедрения зависимостей я использовал контейнер, но это совсем необязательно), все 2 модуля не зависят друг от друга, а зависят от абстракций (принцип инверсии зависимостей).

Надеюсь, после такого количества текста тема инверсий, внедрений и принципов стала немного понятней.

READ ALSO
hmac(вывод ключа) RandomNumberGenerator c#

hmac(вывод ключа) RandomNumberGenerator c#

Вывод ключа неверный, в чем может быть проблема?

232
Взаимодействие WindowsForm C# и DLL написанной на С#

Взаимодействие WindowsForm C# и DLL написанной на С#

Программа на С# загружает динамическую библиотеку написанную на том же С#Соответственно dll выполняет одну только функцию

157
Изменение PolygonCollider2D скриптом

Изменение PolygonCollider2D скриптом

Не могу решить проблемуПри создании игрового объекта из префаба по высчитанным точкам рисуется треугольник (рисуется ровно и правильно)...

193
C# Speech Recognition

C# Speech Recognition

Что не так может быть в коде? Не распознает голос

168