Всем привет, читатели Habr! В этой статье я расскажу про DI (Dependency Injection) и также покажу, как я использую его на практике. Погнали!
Сначала простыми словами про DI
Недавно я скачал приложение, которое показывает сколько времени я провожу в своем смартфоне и мою зависимость от него. Что ж, в некоторые дни я проводил больше 5 часов и мне показало сильную зависимость. У меня Xiaomi Redmi Note 8.
На первый взгляд может показаться, что я завишу конкретно от моего Xiaomi. Однако это не так. Был бы у меня любой другой смартфон, результаты бы не изменились. В итоге получается, что я имею зависимость от абстракции - смартфон, а мой текущий - его конкретная реализация. Настало время переходить к практике!
Теперь к практике
Представим себе такую ситуацию, что каждую первую неделю вы должны показывать заказчику UI с моканными данными, а уже в конце спринта полностью готовую логику. Я надеюсь вы знакомы с паттерном Repository, который отвечает за получение данных, если нет - рекомендую ознакомиться. Самый простой способ решение проблемы - написать в репозитории методы, которые отдают моканные данные, а потом добавить методы для получения реальных данных (или заменить). Однако, поступая таким образом, мы нарушаем один из принципов SOLID, а именно Single Responsibility. Как быть в таком случае? Создать абстрактный класс нашего репозитория и после этого сделать 2 реализации, одна из которых отвечает за моканные данные, а другая - за реальные. И в Bloc или в любой другой класс, который отвечает за State Managment, вы инжектите абстракцию, тем самым давая возможность заменить репозиторий с моками на репозиторий для реальных данных.



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


Время повышать уровень
Однако мы должны постоянно повышать свой уровень! И давайте сейчас его повысим :)
Под буквой O в SOLID обозначается принцип открытости/закрытости. Он декларирует, что программные сущности должны быть открыты для расширения, но закрыты для изменения.
Когда мы заменяем одну реализацию на другую, то тем самым нарушаем этот принцип. И разработчики пакета Injectable тоже об этом подумали, поэтому мы можем создавать наши зависимости под разными environments. Для этого нам нужно будет под каждый environment создать main файл, описать их все в launch.json и проинить с каждого файла зависимости с определенным environment.


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

Комментарии (9)
sergeymolchanovsky
03.03.2022 07:05injectable не нужно использовать вообще. Никогда-никогда.
1) "Настоящий" DI позволяет автоматически подменять интерфейсы на инстансы конкретных классов. При обращении к переменной интерфейса, мы магически получаем нужный инстанс. Подобный DI в Дарте невозможен в силу отсутствия рефлексии.
Пример: Zenject в Unity (который я тоже критикую, но в данном случае подойдет для примера)
public class TestInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<IWeapon>().To<Sword>(); } } public class Player { [Inject] IWeapon weapon; }
Готово. Теперь, при обращении к weapon мы гарантированно знаем, что в нем содержится реализация Sword. Чтобы подменить реализацию, достаточно подправить нужную строчку в InstallBindings.
injectable же не избавит нас от ручных обращений GetIt.instance<IWeapon> во всех местах, где требуется Sword. Все что он делает - это создает нечитаемую портянку с кучей registerSingleton, которую проще написать самому. И к которым все равно надо обращаться вручную.
Итого имеем: обмазывание всех классов аттрибутами, дополнительное время к кодогенерации и сомнительную пользу.
2) "Настоящий" DI обязан предоставлять верификацию в compile-time. Мы можем прогнать её до запуска приложения и убедиться, что все абстракции подменяются на реализации. В том же Zenject она есть. injectable такой возможности не предоставляет.
ksbes
03.03.2022 10:03+2Ну такой «настоящий» DI нарушает другой принцип: «явное всегда лучше неявного». С#, конечно не Python, но но на истинность утверждения это не влияет. За удобства всегда приходится платить. И в конкретных случаях надо выбирать что важнее.
Да и «портянки» строк вида
Котенйер.bind<что-то>.to<что-то>.ПриТакихТоУсловиях(<куча_разнообразных_параметров>)
— тоже никуда не деваются. И, в лучшем случае, сгребаются в один файл, а то и разбрасываются по всему коду, так что хрен найдёшь.sergeymolchanovsky
05.03.2022 11:42Этот принцип чисто питоновский. В других языках может быть по-другому. Джависты, например, обожают именовать интерфейсы не IFoo (явно показывать, что это интерфейс), а просто Foo (мотивируя это принципом дядюшки Боба, что пользователю не нужно знать, что он работает с абстракцией).
Где вы тут в примере увидели "расбрасывание" по всему коду? Все упаковывается в Installer.
P.S. Я осуждаю Zenject по другой причине - в Unity и так из коробки отличный механизм связывания через визуальный инспектор. Лепить поверх Zenject = масло масляное, тонны бойлерплейтного кода в инсталлерах и переусложнение проекта. Привел его тут просто как пример хорошего DI.
vovaklh Автор
04.03.2022 00:56+1Injectable позволяет избавиться от GetIt.instance на основе создания модулей или добавления аннотаций к классам. Это аналог Dagger и Hilt из нативы. Так же хочется сказать, что программирование - это не Хогвартс, и в коде никакой магии не должно быть
ookami_kb
SRP тут вообще не при чем. Если так хочется поговорить про SOLID, то это D – Dependency Inversion Principle.
Опять нет. OCP совершенно не про это. Он говорит о том, что надо разделять код на компоненты так, чтобы компоненты вверху иерархии не зависели от изменений в компонентах внизу иерархии.
vovaklh Автор
Мне кажется вы перепутали OCP и Dependency Inversion принципы. Первый из них как раз говорит, что сущности должны быть закрыты для изменений, но открыты для расширений. В моем случае модуль Injectable - сущность, и если я буду заменять одну реализацию на другую, то нарушу OCP.
ookami_kb
Нет, не перепутал. Почитайте Clean Architecture:
Это на уровне архитектуры. На уровне классов/модулей – это, в основном, наследование, хуки, композиция, декораторы и т.д. – все то, что позволяет переиспользовать код, не изменяя его.
Заменить мок на реальный репозиторий, когда он готов – это вообще не про OCP. Подстановка разных реализаций интерфейса через аннотации – тоже. Это же просто прямое следствие инверсии зависимостей: если мы зависим от абстракций, то мы можем подставить любую реализацию.
А то, что в injectable вы описали подстановку с помощью аннотаций, а не через `return isTest ? Mock() : Impl()` – то вы не сделали систему "закрытой для изменений, но открытой для расширений". Попробуйте, например, добавить еще одну реализацию или еще одну environment, не изменяя существующий код.
vovaklh Автор
Ваш пример с isTest ? напомнил мне цитату из чистого кода "Каждый оператор наследования можно заменить с помощью наследования". Плюс у меня сейчас и так 3 environment. В Injectable сейчас всего их 3 и думаю этого достаточно.
По поводу SOLID - это не математические законы, которые могут быть описаны с помощью формул. Тут его каждый трактует по своему.
ookami_kb
Да, только это здесь не при чем.
Хорошо, но к расширяемости кода это не имеет отношения.
Если подходить с позиции: "Я художник, я так вижу", то да, всё так, и смысла дискутировать нет.