80LevelElf about IT Записи Мои проекты Обо мне
# Часть 1. Теоретические основы Dependency Injection и IOC-контейнеров. ####Ссылки на другие части: - Первая часть: Теоретические основы Dependency Injection и IOC-контейнеров - [Вторая часть: Введение в IOC-контейнеры на примере Ninject] (http://80levelelf.com/Post?postId=22) Глава 1. Зачем нам нужен Dependency Injection? ---------------------------------------------- Наверное, это и есть самый главный вопрос, на который нужно ответить. Обычно в этом месте начинаются пространные рассуждения о том, что у нас есть класс Car и класс CarEngine и они слишком связанны между собой и … Это все, конечно, так, но на вопрос это не отвечает. Главный ответ на то, зачем нужен Dependency Injection (внедрение зависимостей) – это тестирование. Но он отнюдь не единственный. А теперь чуть поподробнее. В любом относительно большом программном продукте возникает потребность в логическом разделении частей. Вариантов и идеологий этого – огромное количество, но задача одна – сделать различные части системы максимально независимыми друг от друга по двум основным причинам: - Для того, чтобы упростить разработку системы (чтобы изменения в одной части не вели к большим переписываниям кода в других частях). - Для того, чтобы упростить автоматизированное тестирование (чтобы можно было взять максимально маленькие части системы и тестировать их максимально независимо, не превращая автоматизированное (юнит) тестирование в интеграционное тестирование). *Пример строго структурированной системы.* Для того, чтобы ориентироваться на что-то менее абстрактное, давайте введем пример системы, на который мы будем опираться. Предположим, что мы с вами пишем сервер для мобильного приложения. Графический интерфейс – совершенно не наша забота. Все что нам нужно сделать – предоставить какое-то API для мобильных приложений пользователей (например, мы можем это сделать с помощью Web Api): ![Schema](https://i.imgur.com/ZHEQkHjm.png) В данном примере: **Web Api.** Используется просто как Api для внешних клиентов. Тут мы имеем минимум бизнес логики, только концепции нужные нам для взаимодействия с мобильными клиентами: - Аутентификация пользователей - Дессериализация данных запроса - Серриализация данных ответа - Маршрутизация (сопоставление ссылок на Api с конкретными методами Api) Web Api слой: - НЕ содержит бизнес логики - НЕ обращается к базе данных Web Api слой знает и взаимодействует только с BLL и Entity слоями! **BLL.** Здесь мы храним основную логику для взаимодействия с клиентами (на этом слое мы даже не знаем о такой концепции как «мобильный клиент»). Например, если наше приложение – это приложения для личных финансов, то на BLL слое мы будем анализировать данные клиента для того, чтобы генерировать отчеты или давать советы по расходам. BLL слой: - НЕ содержит логики, специфичной для Api - НЕ обращается к базе данных BLL слой знает и взаимодействует только с DAL и Entity слоями! **DAL**. Здесь мы содержим логику обращения к SQL / NoSql базам данных (например, посредством Linq2DB или Entity Framework). DAL слой: - НЕ содержит никакой логики DAL слой знает и взаимодействует только с Entity слоем! **Entity.** Entity слой – просто набор сущностей (классов, перечислений, интерфейсов), который содержит минимум бизнес логики (а лучше вообще без неё) и используются в основном для передачи данных между слоями (так называемый паттерн DTO – data transfer object). Это архитектура не является единственно верной или правильной, хотя в том или ином виде часто используется. Конечно, внутри каждого большого слоя могут быть свои слои поменьше, а классы внутри каждого слоя использовать и зависеть друг от друга, образуя ту или иную логику зависимостей. Но зачем, спросите вы, в рассказе про внедрение зависимостей, понадобилось тратить столько времени на описание какой-то системы? Потому что правильно структурированная система и способствует максимальному разделению ответственности и минимальной связанности компонентов. А для того, чтобы достичь этой минимальной связанности (а также максимально удобно её использовать) и существует Dependency Injection! Глава 2. Проблемы, которые решает Dependency Injection ------------------------------------------------------ Говоря языком абстракции, главная проблема, которую решает Dependency Injection – это проблема отделения того, что нам нужно сделать (читай - интерфейса) от фактической реализации того, как мы будем это делать (читай – конкретной реализации интерфейса). Но зачем нам это надо? **Существует несколько реализаций того или иного интерфейса.** Наше мобильное приложения набирает обороты. Мы выпустили версии для Android и Ios, взорвали Product Hunt и уже планируем, как мы сможем выйти на пенсию в 25. И тут возникает одна проблема: мы выпускаем новые версии приложения довольно быстро, но люди не успевают так быстро обновлять его на своих девайсах. К тому же основной Android разработчик, решил, что коли дела идут так хорошо, то он заслуживает отпуск, в котором он не был уже пару лет, поэтому версия приложения для Ios немного обогнала по функционалу версию приложения для Android. Короче говоря, возникла проблема поддержки нескольких версий нашей бизнес логики. **Вообще-то всю систему целиком очень сложно тестировать.** Основной логикой (концепцией) юнит-теста является то, что он тестирует только какую-то строго ограниченную часть приложения. Но в нашей текущей системе мы не можем позволить себе, например, тестировать BLL слой, не тестируя неявно и DAL слой тоже! Не говоря уже о том, чтобы тестировать отдельные части BLL слоя в отрыве от других. И вообще говоря, в любой большой системе это очень плохо. Приведем конкретный пример. Скажем, нам нужно протестировать функцию генерации анализа по тратам пользователя за месяц. Что нам для этого нужно? Как минимум сами тестовые данные (а именно траты). Так же, возможно, настройки пользователя, настройки системы и так далее. То есть мы получаем завязку на логику DAL-слоя (получение данных). Что мы тут можем сделать? *Мы можем сделать в нашей базе данных специального пользователя с нужными нам настройками*, но тогда мы будем очень сильно завязаны на эти данные. И любое случайное действие (падение теста, работа другого программиста или другого теста), может сломать нам тестовые данные. *Мы можем добавлять пользователя и данные в каждом тесте*, но: - Как минимум это затратно и тесты будут занимать куда больше времени. - Мы очень сильно завязываемся на множество других компонентов (BLL и DAL код добавления нового пользователя, его настроек и информации о расходах). Но потом мы вводим новую функцию: за 3.99$ в месяц мы будем показывать пользователю на 25% меньше расходов, чем он реально потратил, причем это функция будет включена по умолчанию (в настройки приложения и в его стоимость). После этого мы будем получать другую информацию об отображаемых расходах пользователя, что сломает наш тест (причем причину будет не так просто обнаружить). Но в реальности наш тест на аналитику не имеет никакого отношения к новой функции и никак не должен её затрагиваться! Для того, чтобы избежать этого, мы можем создать тестовую реализацию получения информации о расходах пользователя (так называемую Mock-реализацию), которая просто будет возвращать нам статические тестовые данные. И используя Dependency Injection, мы и можем реализовать эту подмену, не нарушая целостности системы. Глава 3. Зачем нужны IOC контейнеры? ------------------------------------ Конечно, вы можете обеспечить Dependency Injection вручную. Самый распространенный способ – передать ссылки вручную в конструктор создаваемого объекта. Например, наш модуль для анализа расходов будет называться AnalysisBLL, и так как ему нужен доступ к данным о существующих расходах и настройках пользователя, то он будет принимать ссылку на IConsumptionBLL и IUserBLL: public class AnalysisBLL : IAnalysisBLL { private readonly IConsumptionBLL _consumptionBLL; private readonly IUserBLL _userBLL; public AnalysisBLL(IConsumptionBLL consumptionBLL, IUserBLL userBLL) { _consumptionBLL = consumptionBLL; _userBLL = userBLL; } } > Важный нюанс: мы используем именно IConsumptionBLL и IUserBLL, а не > IConsumptionDAL и IUserDAL для получения соответствующих данных, ведь > мы можем использовать какую-то пост-обработку полученных из БД данных, и > она должна быть именно в BLL слое, поэтому хорошим тоном > является ситуация, когда конкретный BLL модуль напрямую обращается > только к соответствующему DAL модулю: > ![Schema 2](https://i.imgur.com/0KundQ2.png) Вроде все просто и хорошо, но такой подход очень скоро становиться сложным и плохо поддерживаемым по следующим причинам: **Это просто большая ручная работа.** Когда у нас есть только две ссылки на другие модули, используемые в одном классе – это не страшно, но с ростом приложения, его функционала и возможностей мы столкнемся с дилеммой: - Или искусственно «раздувать» модули, тем самым обеспечивая большую связность отдельных участков кода, что в долгосрочной перспективе приведет к проблемам при юнит-тестировании. - Или разбивать модули на более мелкие, обеспечивая тем самым минимальную связанность и максимальную тестируемость, что в конце концов приведет к «аду ссылок». Последнюю проблему можно немного сгладить, например, объединив все BLL модули в один объект – IBLLScope (или любое другое название по вашему вкусу), а все DAL модули в IDALScope. Но зависимости могут быть куда сложнее и многограннее, и подобный Scope-объект лишь поможет сгладить проблему, но не решить её полностью. А самое главное, подобный подход приведет к следующей проблеме. **Проблема циклической зависимости.** Объект реализации IBLLScope должен содержать ссылку на IAnalysisBLL. В тоже время, объект реализации IAnalysisBLL должен принимать ссылку на IBLLScope для того, чтобы обращаться к IConsumptionBLL и IUserBLL: public class AnalysisBLL : IAnalysisBLL { private readonly IBLLScope _bllScope; public AnalysisBLL(IBLLScope bllScope) { _bllScope = bllScope; } public void Do() { _bllScope.Consumption.Do(); _bllScope.User.Do(); } } public class BllScope : IBllScope { public IAnalysisBLL Analysis { get; private set; } public BllScope(IAnalysisBLL analysisBll) { Analysis = analysisBll; } } И тут получается так называемая циклическая зависимость. Для того, чтобы создать объект BllScope, нам передать ему уже созданный объект AnalysisBLL. Но для того, чтобы создать объект AnalysisBLL, нам нужно передать ему уже созданный объект BllScope. Да, конечно, и эту проблему можно решить. Например, можно создать метод, который будет передавать объектам нужные реализации вместо конструктора: public class AnalysisBLL : IAnalysisBLL { private readonly IBLLScope _bllScope; public AnalysisBLL() { } public void SetUp(IBLLScope bllScope) { _bllScope = bllScope; } public void Do() { _bllScope.Consumption.Do(); _bllScope.User.Do(); } } Но теперь нам нужно не забывать вызывать тот самый метод инициализации, и это все становиться очень муторным занятием. Показанный выше пример – это очень простой пример, ведь циклическая зависимость только в простейшем случае – это зависимость между двумя объектами. Если в циклической цепочке зависимостей будет задействовано множество объектов, то разрешение такой зависимости – настоящая головная боль. Для решения подобных проблем и упрощения работы с Dependency Injection и были придуманы IOC-контейнеры (Ninject, AutoFac, Unity и другие).
(16.10.2017)

blog comments powered by Disqus