• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Раскрытие принципа DI, DI-контейнер

Доброго времени суток, codeby. Сегодня хочу развеять туман над одним из ключевых принципов SOLID - Dependency Injections (DI), внедрение ( инъекция ) зависимостей.
Для начала стоит сказать пару слов о принципах SOLID вообще. Это аббревиатура полученная путем сложения названия принципов проектирования выведенных Робертом Мартином на протяжении многих лет практики разработки архитектур программного обеспечения. Эти принципы можно увидеть чуть ли не во всех его книгах. Принципы SOLID - это набор эвристических правил, которые помогают строить легко масштабируемые и легко поддерживаемые в дальнейшем системы.
S - Single responsibility principle ( принцип единственной ответственности )
O - Open/Closed principle ( принцип открытости/закрытости )
L - Liskov Substitution principle principle (принцип подстановки Барбары Лисков)
I - Interface Segregation principle ( принцип разделения интерфейсов )
D - Dependency Injections principle ( принцип инъекции зависимостей )
Всё это про ооп языки. ( кстати, интересно, считается ли упоминание общеизвестных терминов - плагиатом? ) Сегодня мы сосредоточимся на последнем принципе.
Типичная формулировка этого принципа звучит так: “Модули верхнего уровня не должны зависеть от низкоуровневых модулей. И те и другие должны зависеть от абстракций”. Теперь давайте постепенно погружаться и разжевывать для себя эту достаточно туманную формулировку.
В своё время, при изучении принципов SOLID, в частности принципа DI, я не понимал, как минимум что означает слово “уровни”, почему они могут быть высокие и низкие. Где находится граница определяющие эти параметры. Для того чтобы понять философию уровней, задумаемся о природе программного обеспечения. Ведь по-сути, любое программное обеспечение, а особенно корпоративное - это выражение намерений организации, выражение политик организации. Исходя из этого, можно сказать, что создание архитектур программного обеспечения - это не что иное, как группировка политик и намерений. Причем, группирую, мы стараемся следовать конкретному условию - политики, которые изменяются по одинаковым причинам - должны хранится в одном модуле, в противовес политикам, которые изменяются по другим. Если перенести теорию на практику, то политика, регламентирующая нам расчет НДФЛ ( налог для физического лица, тот самый который 13% если вы живёте в России ), ничего не должна знать о политики, которая регламентирует как должен выглядеть расчётный лист ( документ с расчетами заработной платы, в том числе и с указанием НДФЛ, который сотрудник получает на руки перед или после заработной платы ). То есть, можно сказать, что политика, регламентирующая расчет НДФЛ, находится на более высоком уровне, чем политика, регламентирующая как должен выглядеть НДФЛ. Тогда, можно сформировать чёткое определение того, что такое уровень и как он определяется. Уровень - это удалённость от ввода\вывода. И действительно, политике расчета НДФЛ абсолютно всё равно откуда она получила значение зп, в отличее от политики, которая регламентирует как должен выглядеть расчётный лист.
Идём дальше, теперь состояние недоумение вызывает слово абстракция. Вернее тот факт что от неё должны зависеть все уровни. Я постараюсь объяснить это. Вы помните, что мы должны программировать для интерфейса, а не для реализации, так вот тут точно так же. Абстрактные классы могут рассматриваться как условные представители конкретных классов реализации. Абстрактный класс определяет поведение, а субклассы это поведение реализуют. При программировании для интерфейса ваш код будет работать со всеми субклассами этого интерфейса, даже с теми, которые еще не созданы. Как мы знаем, такая разработка помогает повысить сцепление. Сцепление - степень логического сопряжения элементов одного модуля, класса или объекта. Чем выше сцепление программного продукта, тем четче определены обязанности каждого класса приложения. Каждый класс обеспечивает выполнение четко определенного набора взаимосвязанных действий.
Класс с высоким сцеплением хорошо делает что-то одно и не пытается делать что-то другое. Классы с высоким сцепление сконцентрированы на выполнении конкретных задач.
А как насчёт зависимости? Это не что иное, как ссылка на модуль в исходном коде, которая выражается инструкциями include, using и так далее …
Резюмируем - с одной стороны мы имеем понимание что такое уровень и какой он бывает. И мы имеем понимание, что такое абстрактный класс, интерфейс и почему программировать нужно основываясь на интерфейсах и абстрактных классов, с другой стороны. Теперь попробуем соединить это и получим понимание принципа DI.
Давайте посмотрим на элементарную программу для криптографии. В ней мы имеем следующие политики:
  • политика чтения символа
  • политика преобразования символа по какому либо криптографическому алгоритму
  • политика записи символа
Итак, давайте теперь разобьем эти политики на уровни. Политика чтения символа и политика записи символа наиболее близки к вводу\выводу, думаю этот факт очевиден. Что же касается политики криптографирования символа? Ей по барабану - как вы отдаёте ей символ, да хоть азбукой Морзе, главное что передаете. Так же ей абсолютно всё равно куда вы собираетесь передать свой символ дальше. Следовательно это политика самого высокого уровня.

1.png

Поток данных - это как движутся данные в приложение, показано сплошной стрелкой, а пунктиром обозначены зависимости. Так мы соблюли первый пункт. Теперь добавим щепотку с абстракциями. Бадабум, и диаграмма выглядит так.
2.png

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


Вот благодаря тому, что мы разбили политики на уровни и сделали зависимость от абстракций, мы можем легко расширять, менять, крутить, вертеть наше приложение. Захотели вывовод зашифрованного сообщение в телеграм? Реализовали интерфейс ICharWriter, описав, что символ нужно отправлять в телеграм. Захотели читать символ из твиттера Илона Маска, реализовали интерфейс ICharReader. Захотели читать символы с сообщений codeby, ну вы поняли. Не можете придумать куда хотите отдавать символы и откуда брать? Не беда, решите потом. Помимо всех этих выигрышей, мы получили ещё один бонусом - тестопригодность. Мы с лёгкостью можем застабить интерфейс, и сосредоточится на тестировании политики шифрования.

Надеюсь теория по принципу, а также выигрыши, которые мы получаем следую ем - стали очевидней. Переходим к практике.

На практике есть 4 способа (паттерна) внедрения зависимостей.
  1. Внедрение через конструктор
  2. Внедрение через свойство
  3. Внедрение через метод
  4. Внедрение через контекст

В 98% случаев, можно обойтись первыми двумя.
Первый способ, используется по-умолчанию, когда используется внедрение зависимостей ( читай всегда ). Это наиболее важный паттерн для внедрения зависимостей. Собственно говоря - это единственный паттерн, который гарантирует, что необходимая зависимость всегда доступна для класса. Как он это гарантирует? - Очень просто - запрашивая все вызывающие элементы, чтобы доставить зависимость в качестве параметра для конструктора.

Для реализации внедрения через конструктор, класс имеющий зависимость должен иметь публичный конструктор, где зависимость передается в качестве аргумента. Мы предполагаем, что клиентский код, знает о необходимой зависимости.
Рассмотрим на примере: имеется класс Client, который выступает в роли клиентского кода. Класс Client зависит от реализации интерфейса IService, в его коде это указано явно.
4.png

Интерфейс IService предоставляет метод DoSomething.
5.png

Также имеется реализация интерфейса IService - класс Service.

6.png

Чтобы реализовать внедрение зависимости через конструктор, необходимо в коде клиента задать конструктор, благодаря этому - код использующий класс Client, гарантировано передаст необходимую зависимость. Сделаем это
7.png

Однако мы предусмотрели не всё, поскольку, абстрактные классы и интерфейсы - это ссылочные типы, ничего не мешает нерадивому программисту передать null в качестве зависимости, поэтому стоит добавить проверку, для защиты, например так
8.png

Далее класс Client имеет единственный метод SomethingWithLogic который использует зависимость от IService. Таким образом если клиентский код не передаст зависимость ( реализацию IService ) выполнение метода SomethingWithLogic завершится ошибкой.
Полный код класса Client выглядит так:
9.png

посмотрим теперь на создание экземпляра класса Client и вызов метода.
10.png

Как видно мы явно передали зависимость в результате мы получили вывод:
11.png

продемонстрируем удобство - Ситуация изменилась и теперь нам нужно, чтоб сервис выводил фразу “DoSomething IMP2”. Для этого мы просто реализуем отдельный класс ServiceTwo
12.png

и в коде где мы вызываем класс заменим строку создания экземпляра класса Client
13.png

По сути, мы можем даже не удалять струю реализацию Service. Таким образом мы свели изменения к минимуму. Надеюсь понятно, что изменение 1 символа взято для примера, на практике - может поменяется большая часть бизнес логики реализации порядка 300 строк и больше.

Рассмотренный выше способ - это ручное внедрение зависимости. Очевидной проблемой является тот факт, что создание экземпляров нужно где-то хранить, причем желательно в одном месте, дабы не терять преимуществ. Однако умные парни придумали такую штуку как DI-контейнеры. Та же реализация происходит автоматически и не требует явного создания экземпляра
Рассмотрим второй способ. Сразу же отвечу на вопрос - какой способ и когда использовать - всё очень просто: Если зависимость обязательная - используем внедрение через конструктор, если зависимость опциональная - используем внедрение через свойство. Рассмотрим всё тот же пример с сервисами, только теперь скажем, что иногда мы хотим выводить строку в консоль в обратном порядке.
Для этого объявим интерфейс IReverseStringService
14.png

И его реализацию

15.png


Теперь дадим возможность классу Client принимать зависимость через свойство
16.png

и добавим логику в метод SomethingWithLogic
17.png

Как видите, мы проверяем наличие зависимости и в случае если она передана, отдаём строку задом наперед. Ну в в клиентском коде это делается так:
18.png

Ну и вот как это работает
19.png

Таким образом, клиенты ( клиентский код ), желающие воспользоваться клиентом как есть - могут не объявлять зависимость, в то время как клиенты, которым нужен reverse строки, могут передать зависимость через свойство.
Как видно из реализации, чтобы реализовать паттерн внедрение через свойство, класс, который использует зависимость должен предоставить открытое, доступное для записи свойство.
 
Последнее редактирование:
S

someone

Спасибо за содержательную статью, достаточно доходчиво. (y) По моему, последний принцип solid - это dependency inversion (инверсия зависимостей), а не injection и ты как раз описал инверсию зависимостей. А dependency injection - это паттерн, согласно которому одна сущность зависит от другой, не более, и инъекция этих зависимостей передаётся сторонним сервисам, так называемым инжекторам. Поправь меня, если ошибаюсь :unsure:
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!