Поиск…


замечания

Проблемы, решаемые путем инъекций зависимостей

Если бы мы не использовали инъекцию зависимостей, класс Greeter мог бы выглядеть примерно так:

public class ControlFreakGreeter
{
    public void Greet()
    {
        var greetingProvider = new SqlGreetingProvider(
            ConfigurationManager.ConnectionStrings["myConnectionString"].ConnectionString);
        var greeting = greetingProvider.GetGreeting();
        Console.WriteLine(greeting);
    }
}

Это «контрольный урод», потому что он контролирует создание класса, предоставляющего приветствие, он контролирует, откуда приходит строка подключения SQL, и он контролирует вывод.

Используя инъекцию зависимостей, класс Greeter отказывается от этих обязанностей в пользу единой ответственности, написав приветствие, предоставленное ему.

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

Инъекционная инъекция помогает нам достичь этой инверсии зависимостей, поскольку она приводит к написанию классов, которые зависят от абстракций. Класс Greeter «ничего не знает» о деталях реализации IGreetingProvider и IGreetingWriter . Он знает только, что введенные зависимости реализуют эти интерфейсы. Это означает, что изменения в конкретных классах, которые реализуют IGreetingProvider и IGreetingWriter , не будут влиять на Greeter . Они не заменят их совершенно разными реализациями. Только изменения в интерфейсах будут. Greeter развязан.

ControlFreakGreeter невозможно правильно протестировать. Мы хотим протестировать одну небольшую единицу кода, но вместо этого наш тест будет включать подключение к SQL и выполнение хранимой процедуры. Это также будет включать тестирование вывода консоли. Поскольку ControlFreakGreeter делает так много, невозможно проверить изолированно от других классов.

Greeter легко тестируется на единицу, потому что мы можем вводить в заблуждение реализацию зависимостей, которые легче выполнять и проверять, чем вызов хранимой процедуры или чтение выходных данных консоли. Он не требует строки подключения в app.config.

Конкретные реализации IGreetingProvider и IGreetingWriter могут стать более сложными. У них, в свою очередь, могут быть свои собственные зависимости, которые вводятся в них. (Например, мы бы SqlGreetingProvider строку подключения SQL в SqlGreetingProvider .) Но эта сложность «скрыта» от других классов, которые зависят только от интерфейсов. Это облегчает изменение одного класса без «эффекта пульсации», который требует от нас внести соответствующие изменения в другие классы.

Инъекция зависимостей - простой пример

Этот класс называется Greeter . Его обязанность - выпустить приветствие. Он имеет две зависимости . Ему нужно что-то, что даст ему приветствие для вывода, а затем ему нужен способ вывода этого приветствия. Эти зависимости описываются как интерфейсы, IGreetingProvider и IGreetingWriter . В этом примере эти две зависимости «вводятся» в Greeter . (Дальнейшее объяснение по примеру.)

public class Greeter
{
    private readonly IGreetingProvider _greetingProvider;
    private readonly IGreetingWriter _greetingWriter;

    public Greeter(IGreetingProvider greetingProvider, IGreetingWriter greetingWriter)
    {
        _greetingProvider = greetingProvider;
        _greetingWriter = greetingWriter;
    }

    public void Greet()
    {
        var greeting = _greetingProvider.GetGreeting();
        _greetingWriter.WriteGreeting(greeting);
    }
}

public interface IGreetingProvider
{
    string GetGreeting();
}

public interface IGreetingWriter
{
    void WriteGreeting(string greeting);
}

Класс Greeting зависит как от IGreetingProvider и от IGreetingWriter , но он не несет ответственности за создание экземпляров. Вместо этого он требует их в своем конструкторе. Все, что создает экземпляр Greeting должно обеспечивать эти две зависимости. Мы можем назвать это «инъекцией» зависимостей.

Потому что зависимости предоставляются классу в его конструкторе, это также называется «инъекцией конструктора».

Несколько общих соглашений:

  • Конструктор сохраняет зависимости как private поля. Как только экземпляр класса создается, эти зависимости доступны для всех других нестатических методов класса.
  • private поля - только для readonly . Как только они установлены в конструкторе, они не могут быть изменены. Это означает, что эти поля не должны (и не могут) быть изменены вне конструктора. Это гарантирует, что эти зависимости будут доступны для жизни класса.
  • Зависимости - это интерфейсы. Это не является строго необходимым, но является общим, потому что упрощает замену одной реализации зависимости другой. Он также позволяет предоставлять издеваемую версию интерфейса для целей модульного тестирования.

Как инъекция зависимостей упрощает тестирование единиц

Это основывается на предыдущем примере класса Greeter который имеет две зависимости: IGreetingProvider и IGreetingWriter .

Фактическая реализация IGreetingProvider может извлекать строку из вызова API или базы данных. Реализация IGreetingWriter может отображать приветствие в консоли. Но поскольку Greeter имеет свои зависимости, введенные в его конструктор, легко написать единичный тест, который вводит издевательские версии этих интерфейсов. В реальной жизни мы могли бы использовать фреймворк вроде Moq , но в этом случае я напишу эти издевательства.

public class TestGreetingProvider : IGreetingProvider
{
    public const string TestGreeting = "Hello!";

    public string GetGreeting()
    {
        return TestGreeting;
    }
}

public class TestGreetingWriter : List<string>, IGreetingWriter
{
    public void WriteGreeting(string greeting)
    {
        Add(greeting);
    }
}

[TestClass]
public class GreeterTests
{
    [TestMethod]
    public void Greeter_WritesGreeting()
    {
        var greetingProvider = new TestGreetingProvider();
        var greetingWriter = new TestGreetingWriter();
        var greeter = new Greeter(greetingProvider, greetingWriter);
        greeter.Greet();
        Assert.AreEqual(greetingWriter[0], TestGreetingProvider.TestGreeting);
    }
}

Поведение IGreetingProvider и IGreetingWriter не относится к этому тесту. Мы хотим проверить, что Greeter получает приветствие и записывает его. Конструкция Greeter (с использованием инъекции зависимостей) позволяет вводить насмешливые зависимости без каких-либо сложных движущихся частей. Все, что мы тестируем, это то, что Greeter взаимодействует с этими зависимостями, как мы ожидаем.

Почему мы используем контейнеры для инъекций зависимостей (контейнеры IoC)

Инъекция зависимостей означает написание классов, чтобы они не контролировали свои зависимости - вместо этого их зависимости предоставляются («впрыскивается»).

Это не то же самое, что использование рамки инъекции зависимостей (часто называемой «контейнером DI», «контейнером IoC» или просто «контейнером»), например, Castle Windsor, Autofac, SimpleInjector, Ninject, Unity или другими.

Контейнер просто упрощает инъекцию зависимостей. Например, предположим, что вы пишете несколько классов, которые полагаются на инъекцию зависимостей. Один класс зависит от нескольких интерфейсов, классы, которые реализуют эти интерфейсы, зависят от других интерфейсов и т. Д. Некоторые зависят от конкретных значений. И только для удовольствия некоторые из этих классов реализуют IDisposable и должны быть удалены.

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

public CustomerData GetCustomerData(string customerNumber)
{
    var customerApiEndpoint = ConfigurationManager.AppSettings["customerApi:customerApiEndpoint"];
    var logFilePath = ConfigurationManager.AppSettings["logwriter:logFilePath"];
    var authConnectionString = ConfigurationManager.ConnectionStrings["authorization"].ConnectionString;
    using(var logWriter = new LogWriter(logFilePath ))
    {
        using(var customerApiClient = new CustomerApiClient(customerApiEndpoint))
        {
            var customerService = new CustomerService(
                new SqlAuthorizationRepository(authorizationConnectionString, logWriter),
                new CustomerDataRepository(customerApiClient, logWriter),
                logWriter
            );   
            
            // All this just to create an instance of CustomerService!         
            return customerService.GetCustomerData(string customerNumber);
        }
    }
}

Вы можете удивиться, почему бы не поставить целую гигантскую конструкцию в отдельную функцию, которая просто возвращает CustomerService ? Одна из причин заключается в том, что из-за того, что в него вводятся зависимости для каждого класса, класс не несет ответственности за знание того, являются ли эти зависимости IDisposable или их удаление. Он просто использует их. Поэтому, если у нас была GetCustomerService() которая вернула полностью сконструированную CustomerService , этот класс может содержать несколько одноразовых ресурсов и не иметь доступа к ним или распоряжаться ими.

И помимо утилизации IDisposable , кто хочет вызвать серию вложенных конструкторов, подобных этому, когда-либо? Это короткий пример. Это может стать намного хуже. Опять же, это не значит, что мы неправильно написали классы. Классы могут быть индивидуально идеальными. Задача состоит в их объединении.

Контейнер инъекции зависимостей упрощает это. Это позволяет нам указать, какой класс или значение следует использовать для выполнения каждой зависимости. В этом слегка упрощенном примере используется Castle Windsor:

var container = new WindsorContainer()
container.Register(
    Component.For<CustomerService>(),
    Component.For<ILogWriter, LogWriter>()
        .DependsOn(Dependency.OnAppSettingsValue("logFilePath", "logWriter:logFilePath")),
    Component.For<IAuthorizationRepository, SqlAuthorizationRepository>()
        .DependsOn(Dependency.OnValue(connectionString, ConfigurationManager.ConnectionStrings["authorization"].ConnectionString)),
    Component.For<ICustomerDataProvider, CustomerApiClient>()
         .DependsOn(Dependency.OnAppSettingsValue("apiEndpoint", "customerApi:customerApiEndpoint"))   
);

Мы называем это «регистрирующими зависимостями» или «настройкой контейнера». В переводе это говорит нашему WindsorContainer :

  • Если для класса требуется ILogWriter , создайте экземпляр LogWriter . LogWriter требуется путь к файлу. Используйте это значение из AppSettings .
  • Если для класса требуется IAuthorizationRepository , создайте экземпляр SqlAuthorizationRepository . Для этого требуется строка подключения. Используйте это значение в разделе ConnectionStrings .
  • Если для класса требуется ICustomerDataProvider , создайте CustomerApiClient и AppSettings строку из AppSettings .

Когда мы запрашиваем зависимость от контейнера, мы называем это «разрешающей» зависимость. Это плохая практика, чтобы сделать это непосредственно с помощью контейнера, но это совсем другая история. Для демонстрационных целей мы могли бы теперь сделать это:

var customerService = container.Resolve<CustomerService>();
var data = customerService.GetCustomerData(customerNumber);
container.Release(customerService);

Контейнер знает, что CustomerService зависит от IAuthorizationRepository и ICustomerDataProvider . Он знает, какие классы необходимо создать для выполнения этих требований. Эти классы, в свою очередь, имеют больше зависимостей, и контейнер знает, как их выполнить. Он создаст каждый класс, в котором он нуждается, пока не сможет вернуть экземпляр CustomerService .

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

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

Что относительно классов, которые реализуют IDisposable ? Вот почему мы называем container.Release(customerService); в конце. Большинство контейнеров (включая Windsor) будут отступать от всех созданных зависимостей и Dispose те, которые нуждаются в утилизации. Если CustomerService является IDisposable он тоже удалит это.

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

Это царапины поверхности, почему мы используем контейнеры для инъекций зависимостей. Как мы настроим наше приложение для использования одного (и правильно его используем), это не только одна тема - это несколько тем, так как инструкции и примеры варьируются от одного контейнера к другому.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow