Статьи

Учебник для начинающих по инверсии зависимостей, контролю инверсии и внедрению зависимостей

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

Фон

Прежде чем мы начнем говорить о внедрении зависимостей (DI), мы должны сначала понять проблему, которую решает DI. Чтобы понять проблему, нам нужно знать две вещи. Первый принцип инверсии зависимости (DIP) и второй инверсии управления (IoC). давайте начнем наше обсуждение с DIP, затем поговорим о IoC. как только мы обсудим эти два вопроса, мы сможем лучше понять внедрение зависимостей, поэтому рассмотрим введение зависимостей более подробно. Затем, наконец, мы обсудим, как мы можем реализовать внедрение зависимости.

Принцип обращения зависимостей

Принцип инверсии зависимостей — это принцип разработки программного обеспечения, который дает нам рекомендации по написанию слабосвязанных классов. Согласно определению принципа обращения зависимостей:

  1. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Что означает это определение? Что он пытается передать? давайте попробуем понять определение, посмотрев на примеры. Несколько лет назад я занимался написанием службы Windows, которая должна была работать на веб-сервере. Единственная ответственность этой службы заключалась в том, чтобы регистрировать сообщения в журналах событий всякий раз, когда в пуле приложений IIS возникает какая-либо проблема. Итак, что наша команда сделала изначально, мы создали два класса. Один для мониторинга пула приложений и второй для записи сообщений в журнал событий. Наши занятия выглядели так:

class EventLogWriter
{
    public void Write(string message)
    {
        //Write to event log here
    }
}

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    EventLogWriter writer = null;

    // This function will be called when the app pool has problem
    public void Notify(string message)
    {
        if (writer == null)
        {
            writer = new EventLogWriter();
        }
        writer.Write(message);
    }
}

С первого взгляда вышеупомянутый дизайн класса кажется достаточным. Выглядит отлично, код. Но есть проблема в вышеуказанном дизайне. Этот дизайн нарушает принцип инверсии зависимостей. модуль высокого уровня AppPoolWatcher зависит от EventLogWriter, который является конкретным классом, а не абстракцией. Как это проблема? Хорошо, позвольте мне рассказать вам следующее требование, которое мы получили для этой услуги, и проблема станет очень ясно видна.

Следующим требованием, которое мы получили для этой службы, было отправить электронное письмо на электронный адрес администратора сети для какого-то определенного набора ошибок. Теперь, как мы это сделаем? Одна из идей — создать класс для отправки электронных писем и сохранять его дескриптор в AppPoolWatcher, но в любой момент мы будем использовать только один объект — EventLogWriter или EmailSender .

Проблема усугубится, когда мы будем выборочно выполнять больше действий, таких как отправка SMS. Тогда у нас будет еще один класс, экземпляр которого будет храниться внутри AppPoolWatcher . Принцип инверсии зависимостей говорит о том, что нам необходимо разделить эту систему таким образом, чтобы модули более высокого уровня, т.е. AppPoolWatcher, в нашем случае зависели от простой абстракции и использовали ее. Эта абстракция, в свою очередь, будет отображена в некоторый конкретный класс, который будет выполнять фактическую операцию. (Далее мы увидим, как это можно сделать)

Инверсия контроля

Инверсия зависимостей была принципом разработки программного обеспечения, она просто утверждает, что два модуля должны зависеть друг от друга. Теперь возникает вопрос, как именно мы собираемся это сделать? Ответ — инверсия контроля. Инверсия управления — это реальный механизм, с помощью которого мы можем заставить модули более высокого уровня зависеть от абстракций, а не от конкретной реализации модулей более низкого уровня.

Поэтому, если мне придется реализовать инверсию управления в вышеупомянутом проблемном сценарии, первое, что нам нужно сделать, — это создать абстракцию, от которой будут зависеть более высокие уровни. Итак, давайте создадим интерфейс, который предоставит абстракцию для обработки уведомлений, полученных от AppPoolWacther .

public interface INofificationAction
{
    public void ActOnNotification(string message);
}

Теперь давайте изменим наш модуль более высокого уровня, т.е. AppPoolWatcher, чтобы использовать эту абстракцию, а не конкретный класс более низкого уровня.

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    INofificationAction action = null;

    // This function will be called when the app pool has problem
    public void Notify(string message)
    {
        if (action == null)
        {
            // Here we will map the abstraction i.e. interface to concrete class 
        }
        action.ActOnNotification(message);
    }
}

Так как изменится наш конкретный класс нижнего уровня? как этот класс будет соответствовать абстракции, т.е. нам нужно реализовать интерфейс выше в этом классе:

class EventLogWriter : INofificationAction
{   
    public void ActOnNotification(string message)
    {
        // Write to event log here
    }
}

Так что теперь, если мне нужно иметь конкретные классы для отправки электронной почты и смс, эти классы также будут реализовывать тот же интерфейс.

class EmailSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // Send email from here
    }
}

class SMSSender : INofificationAction
{
    public void ActOnNotification(string message)
    {
        // Send SMS from here
    }
}

Таким образом, окончательный дизайн класса будет выглядеть так:

iocClassDesign

Итак, что мы сделали здесь, так это то, что мы инвертировали управление, чтобы соответствовать принципу инверсии зависимостей. Теперь наши модули высокого уровня зависят только от абстракций, а не от конкретных реализаций нижнего уровня, что в точности соответствует принципу инверсии зависимостей.

Но есть еще одна недостающая часть. Когда мы смотрим на код нашего AppPoolWatcher , мы видим, что он использует абстракцию, т.е. интерфейс, но где именно мы создаем конкретный тип и присваиваем его этой абстракции. Чтобы решить эту проблему, мы можем сделать что-то вроде:

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    INofificationAction action = null;

    // This function will be called when the app pool has problem
    public void Notify(string message)
    {
        if (action == null)
        {
            // Here we will map the abstraction i.e. interface to concrete class 
            writer = new EventLogWriter();
        }
        action.ActOnNotification(message);
    }
}

Но мы снова вернулись к тому, с чего начали. Создание конкретного класса все еще внутри класса более высокого уровня. Разве мы не можем сделать его полностью отделенным, так что даже если мы добавим новые классы, производные от INotificationAction , нам не придется менять этот класс.

Это как раз то место, где вводится зависимость. Настало время детально рассмотреть внедрение зависимостей.

Внедрение зависимости

Теперь, когда мы знаем принцип инверсии зависимостей и увидели инверсию методологии управления для реализации принципа инверсии зависимостей, Dependency Injection в основном предназначена для внедрения конкретной реализации в класс, который использует абстракцию, то есть интерфейс внутри. Основная идея внедрения зависимостей состоит в том, чтобы уменьшить связь между классами и убрать привязку абстракции и конкретную реализацию из зависимого класса.

Внедрение зависимости может быть сделано тремя способами.

  1. Конструктор инъекций
  2. Метод впрыска
  3. Внедрение недвижимости

Конструктор Инъекция

При таком подходе мы передаем объект конкретного класса в конструктор зависимого класса. Поэтому для реализации этого нам нужно иметь конструктор в зависимом классе, который возьмет конкретный объект класса и назначит его дескриптору интерфейса, который использует этот класс. Итак, если нам нужно реализовать это для нашего класса AppPoolWatcher :

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    INofificationAction action = null;

    public AppPoolWatcher(INofificationAction concreteImplementation)
    {
        this.action = concreteImplementation;
    }

    // This function will be called when the app pool has problem
    public void Notify(string message)
    {   
        action.ActOnNotification(message);
    }
}

В приведенном выше коде конструктор возьмет конкретный объект класса и свяжет его с дескриптором интерфейса. Поэтому, если нам нужно передать конкретную реализацию EventLogWriter в этот класс, все, что нам нужно сделать, это

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher(writer);
watcher.Notify("Sample message to log");

Теперь, если мы хотим, чтобы этот класс отправлял электронную почту или смс, все, что нам нужно сделать, — это передать объект соответствующего класса в конструктор AppPoolWatcher . Этот метод полезен, когда мы знаем, что экземпляр зависимого класса будет использовать один и тот же конкретный класс в течение всего времени его существования.

Инъекция метода

В инжекторе конструктора мы увидели, что зависимый класс будет использовать один и тот же конкретный класс в течение всего срока его службы. Теперь, если нам нужно передать отдельный конкретный класс при каждом вызове метода, мы должны передать зависимость только в методе.

Таким образом, в методе внедрения метода мы передаем объект конкретного класса в метод зависимого класса, который фактически вызывает действие. Поэтому для реализации этого нам нужно, чтобы функция действия также принимала аргумент для конкретного объекта класса и присваивала его дескриптору интерфейса, который использует этот класс, и вызывает действие. Итак, если нам нужно реализовать это для нашего класса AppPoolWatcher :

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    INofificationAction action = null;

    // This function will be called when the app pool has problem
    public void Notify(INofificationAction concreteAction, string message)
    {
        this.action = concreteAction;
        action.ActOnNotification(message);
    }
}

В приведенном выше коде метод действия, т.е. Notify , возьмет конкретный объект класса и свяжет его с дескриптором интерфейса. Поэтому, если нам нужно передать конкретную реализацию EventLogWriter в этот класс, все, что нам нужно сделать, это

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
watcher.Notify(writer, "Sample message to log");

Теперь, если мы хотим, чтобы этот класс вместо этого отправлял электронную почту или смс, все, что нам нужно сделать, — это передать объект соответствующего класса в методе вызова AppPoolWatcher, т.е. метод Notify в приведенном выше примере.

Инъекция собственности

Теперь мы обсудили два сценария, в которых при внедрении конструктора мы знали, что зависимый класс будет использовать один конкретный класс за все время жизни. Второй подход заключается в использовании метода внедрения, где мы можем передать конкретный объект класса в самом методе действия. Но что, если ответственность за выбор конкретного класса и вызов метода лежит в разных местах? В таких случаях нам нужно введение свойства.

Таким образом, в этом подходе мы передаем объект конкретного класса через свойство setter, которое было выставлено зависимым классом. Поэтому для реализации этого нам нужно иметь свойство или функцию Setter в зависимом классе, который будет принимать конкретный объект класса и назначать его дескриптору интерфейса, который использует этот класс. Итак, если нам нужно реализовать это для нашего класса AppPoolWatcher :

class AppPoolWatcher
{
    // Handle to EventLog writer to write to the logs
    INofificationAction action = null;

    public INofificationAction Action
    {
        get
        {
            return action;
        }
        set
        {
            action = value;
        }
    }

    // This function will be called when the app pool has problem
    public void Notify(string message)
    {   
        action.ActOnNotification(message);
    }
}

В приведенном выше коде установщик свойства Action возьмет конкретный объект класса и свяжет его с дескриптором интерфейса. Поэтому, если нам нужно передать конкретную реализацию EventLogWriter в этот класс, все, что нам нужно сделать, это

EventLogWriter writer = new EventLogWriter();
AppPoolWatcher watcher = new AppPoolWatcher();
// This can be done in some class
watcher.Action = writer;

// This can be done in some other class
watcher.Notify("Sample message to log");

Теперь, если мы хотим, чтобы этот класс вместо этого отправлял электронную почту или смс, все, что нам нужно сделать, — это передать объект соответствующего класса в установщик, предоставляемый классом AppPoolWatcher . Этот подход полезен, когда ответственность за выбор конкретной реализации и вызов действия выполняется в отдельных местах / модулях.

В языках, где свойства не поддерживаются, есть отдельная функция для установки зависимости. Этот подход также известен как инъекция сеттера. В этом подходе важно отметить, что существует вероятность того, что кто-то создал зависимый класс, но никто еще не установил конкретную зависимость класса. Если мы попытаемся вызвать действие в таких случаях, у нас должна быть какая-то зависимость по умолчанию, сопоставленная с зависимым классом, или какой-то механизм, обеспечивающий правильное поведение приложения.

Замечание о контейнерах IoC

Внедрение в конструктор является наиболее часто используемым подходом, когда речь идет о внедрении зависимостей. Если нам нужно передавать разные зависимости при каждом вызове метода, тогда мы используем внедрение метода. Внедрение свойства используется реже.

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

Достопримечательность

В этой статье мы обсудили принцип инверсии зависимостей (который является частью D в объектно-ориентированном принципе SOLID). Мы также обсудили, как инверсия управления используется для реализации инверсии зависимостей, и, наконец, мы увидели, как внедрение зависимостей помогает в создании разделенных классов и как реализовать внедрение зависимостей. Эта статья была написана с точки зрения начинающего. Я надеюсь, что это было информативно.