Серия статей «Внедрение зависимостей и инверсия управления» состоит из 3 частей:

  1. "Определение"
  2. Внедрение внедрения зависимостей в код
  3. Написать DI-контейнер. Применение внедрения зависимостей в ASP.NET MVC

Вы прочитали часть 1, но все равно мало что понимаете в DI, IoC, не знаете, как применить их в коде? Не волнуйтесь, в части 2 будет предоставлен пример кода, более понятно объясните, о чем я говорил в части 1. Прочитав этот раздел, вы вернетесь к части 1, вы увидите ее «насквозь».

Что такое зависимость?

Зависимость — это низкоуровневые модули или сервисы, которые вызываются извне. В обычном коде модули высокого уровня вызывают модули низкого уровня. Модуль высокого уровня будет зависеть, а модуль низкого уровня будет создавать зависимости.

Чтобы понять это, см. ниже функцию Checkout класса Cart. Эта функция сохранит заказ в базе данных и отправит его пользователю по электронной почте. Класс Cart будет инициализировать и вызывать модуль базы данных, модуль EmailSender, модуль Logger, эти модули являются зависимостями.

public class Cart {
public void Checkout(int orderId, int userId) {
Database db = new Database();
db.Save(orderId);

Logger log = new Logger();
log.LogInfo("Заказ оформлен");

EmailSender es = new EmailSender();
es.SendEmail(userId) ;

}

Что-то не так с этим методом? Вроде нет, писать код тоже быстро. Но такой стиль письма «может» привести к некоторым проблемам в будущем:

  • Эту функцию Checkout сложно протестировать, так как она включает модули Database и EmailSender.
  • Если мы хотим изменить модуль базы данных, EmailSender, … мы должны исправить все места инициализации и вызвать эти модули. Делать это долго, чревато ошибками.
  • В долгосрочной перспективе код станет «сплоченным», модуль имеет высокую связность, изменяющийся модуль повлечет за собой ряд изменений. Это кошмар для поддержки кода.
    Инверсия управления и внедрение зависимостей были созданы для решения этих проблем.

Как ограничить связь между классами. Доступна инверсия управления

Для того, чтобы модули не «прилипали» друг к другу, они не связаны напрямую, а через интерфейс. Это последний принцип SOLID.

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

2. Интерфейс (абстракция) не должен зависеть от деталей, а наоборот. (Классы взаимодействуют друг с другом через интерфейсы, а не реализации.)

Мы, в свою очередь, создаем интерфейсы IDatabase, IEmailSender, ILogger, остальные классы изначально будут наследовать эти интерфейсы по очереди. Чтобы упростить понимание, теперь я временно буду называть IDatabase, IEmailSender, ILogger Interface, а такие классы, как Database, EmailSender, Logger — модулями.

// Интерфейс
открытый интерфейс IDatabase
{
void Save(int orderId);
}

открытый интерфейс ILogger
{
> void LogInfo(string info);
}

общедоступный интерфейс IEmailSender
{
void SendEmail(int userId);
}
< br /> // Модуль реализует интерфейс
public class Logger : ILogger
{
public void LogInfo(string info)
{
//…
}
}

База данных открытого класса: IDatabase
{
public void Save(int orderId)
{
//…
}
}

public class EmailSender : IEmailSender
{
public void SendEmail(int userId)
{
//…

}

Новая функция оформления заказа будет выглядеть так:

public void Checkout(int orderId, int userId)
{// Если вы хотите изменить базу данных, нам просто нужно изменить код ниже
// Модули XMLDatabase, SQLDatabase должны реализовывать IDatabase
// IDatabase db = new XMLDatabase ();
// IDatebase db = new SQLDatabase ();
IDatabase db = new Database ();
db.Save (orderId);

Журнал ILogger = new Logger();
log.LogInfo («Заказ оформлен»);

IEmailSender es = новый EmailSender ();
es.SendEmail (userId);
}

Благодаря интерфейсу мы можем легко изменять низкоуровневые модули, не затрагивая модуль корзины. Это первый шаг IoC.

Для удобства управления мы можем поместить все функции инициализации модуля в конструктор класса Cart.

общедоступный класс Cart
{
частный только для чтения IDatabase _db;
частный только для чтения ILogger _log;
частный только для чтения IEmailSender _es;

public Cart ()
{
_db = новая база данных ();
_log = новый регистратор ();
_es = новый EmailSender ();
}

public void Checkout (int orderId, int userId)
{
_db.Save (orderId);
_log.LogInfo («Заказ оформлен»);
_es.SendEmail (userId) ;

}

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

Сначала люди использовали ServiceLocator для решения этой проблемы. Для каждого интерфейса мы устанавливаем соответствующий модуль. При необходимости мы возьмем этот модуль из ServiceLocator. Это также способ реализации IoC.

общедоступный статический класс ServiceLocator
{
public static T GetModule()
{
//….
}
}

//Мы просто вызываем функцию GetModule
public class Cart
{
public Cart()
{
_db = ServiceLocator.GetModule();
_log = ServiceLocator.GetModule();
_es = ServiceLocator.GetModule();

}

Это по-прежнему проблема: все классы зависят от ServiceLocator.

Внедрение зависимостей решает эту проблему.Модули низкого уровня будут внедрены (переданы) в модули высокого уровня через конструктор или через свойства. Проще говоря, легко понять о DI:

Мы не вызываем нового оператора для создания экземпляра, но этот экземпляр будет передан извне (передача вручную или благодаря контейнеру внедрения зависимостей).

После применения DependencyInjection мы будем использовать класс Cart следующим образом:

общедоступная корзина (база данных IDatabase, журнал ILogger, IEmailSender es)
{
_db = db;
_log = log;
_es = es;
}

// Внедрение зависимостей самым простым способом
Cart myCart = new Cart (new Database(),
new Logger(), new EmailSender());
// Когда нужно изменить базу данных, logger
myCart = new Cart (new XMLDatabase (),
new FakeLogger (), new FakeEmailSender ());

Вы, наверное, думаете: после использования Dependency Injection вы должны инициализировать модуль, это хуже, чем ServiceLocator. Обычно мы используем DI Container. Просто определите один раз, DI Container автоматически вставитмодули низкого уровня в модули высокого уровня.

// Для каждого Интерфейса определяем соответствующий Модуль
DIContainer.SetModule ‹IDatabase, Database› ();
DIContainer.SetModule ‹ILogger, Logger› ();
DIContainer.SetModule ‹IEmailSender , EmailSender› ();

DIContainer.SetModule ‹Cart, Cart› ();

// Контейнер DI внедрит базу данных, сам Logger в корзину
var myCart = DIContainer.GetModule ();

// Когда нужно изменить, нам просто нужно исправить код define
DIContainer.SetModule ‹IDatabase, XMLDatabase› ();

После применения Dependency Injection ваш код станет длиннее, будет казаться более «сложным» и его будет сложнее отлаживать. Взамен код будет гибким, легким в изменении и легким в тестировании.

Как я уже говорил в предыдущем посте, DI не всегда правильный выбор, нам нужно рассмотреть за и против сильный>. DI применяется во многих внутренних (ASP.MVC, Struts2) и внешних (AngularJS, KnockoutJS) средах. Большинство крупных проектов в ИТ-компаниях применяют DI, поэтому знание DI будет очень полезно при собеседовании и работе.

Итак, откуда вышеприведенный контейнер внедрения зависимостей берется откуда? Мы можем написать сами или использовать некоторые популярные DI-контейнеры на C#, такие как Unity, StructureMap, NInject. В части 3 я покажу вам, как написать простой DI-контейнер и использовать существующие DI-контейнеры.