Comments 16
Очень увлекательная статья, спасибо за рассмотрение вопроса под новым углом, познавательно
Для новичков можно было написать введение. А то в первом же предложении: "Service Locator - зло." Что это такое? Где? И... по какому праву?
Сильная связность между клиентом и зависимостью никуда не пропадает, что становится причиной множества проблем. Например, таким образом созданный сервис невозможно тестировать, поскольку он будет зависеть от контекста — его можно будет протестировать только в виджете, который находится под InheritedWidget'ом.
Так как виджету требуется зависимость, очевидно, что он будет от чего-то зависеть. Хорошая практика это использовать скоупы на виджетах высокого уровня, экранах к примеру. На виджетах же компонентов, сабкомпонентов и т.д. использовать исключительно параметры и колбеки. Таким образом их можно будет тестировать и представлять в тех же сторибуках без проблем.
У InheritedWidget'a есть scope доступа к сервисам, зарегистрированным в нем, который ограничен его дочерним поддеревом виджетов. т. е. доступ к его сервисам может получить любой виджет его дочернего поддерева, а не любой компонент приложения.
Доступ к сервисам InheritedWidget'a могут получить только через контекст, а другие компоненты системы, не находящиеся в нужном месте дерева виджетов (подInheritedWidget'ом) не могут получить к ним доступ. Если InheritedWidget отвечает за сборку сервиса, доступ к которому он предлагает, то тестирование этого сервиса становится сложным, поскольку компоненты тестирования должны будут располагаться под InheritedWidget'ом.
Никто и не утверждал что нужно собирать зависимости в Inherited Widget, вы используете его не по назначению. Он не должен содержать никакой логики. InheritedWidget используется исключительно для того, чтобы передавать кусочки информации вниз по дереву:
class DependenciesScope extends InheritedWidget {
/// {@macro dependencies_scope}
const DependenciesScope({
required super.child,
required this.dependencies,
super.key,
});
/// Container with dependencies.
final DependenciesContainer dependencies;
/// Get the dependencies from the [context].
static DependenciesContainer of(BuildContext context) =>
context.inhOf<DependenciesScope>(listen: false).dependencies;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
DiagnosticsProperty<DependenciesContainer>('dependencies', dependencies),
);
}
@override
bool updateShouldNotify(DependenciesScope oldWidget) => false;
}
После утилизации скрина, сервисы которые в нем использовались, больше не будут использоваться, поэтому их нужо утилизировать вместе со скрином. Иначе будет происходить утечка ресурсов.
В этом может помочь использование умного DI‑контейнера, например того, который предоставляет пакет get_it.
Вся статья про то, что сервис локатор зло и здесь вы берете сервис локатор.
Зло сервис локатора как раз таки в том, что он не ограничен контекстом, и это буквально глобальная переменная, которую можно дернуть из любой части приложения, что очевидно даёт возможность писать код где бизнес логика будет дергать виджеты, навигацию, другую бизнес логику, вместо правильной архитектуры, нотификаций и описания data flow.
Касательно того, что вы называете get_it DI контейнером это неправильно. Это сервис локатор, потому что вы сами (СВОИМИ РУКАМИ) создаете граф зависимостей, то есть описываете все связи между объектами вручную. Эту проблему решает injectable, его можно считать DI контейнером. Тем не менее, он не решает проблемы сервис локатора, так как основан на нем. Использовать его так же не рекомендуется.
Так как виджету требуется зависимость, очевидно, что он будет от чего-то зависеть. Хорошая практика это использовать скоупы на виджетах высокого уровня, экранах к примеру.
Никто и не утверждал что нужно собирать зависимости в Inherited Widget, вы используете его не по назначению. Он не должен содержать никакой логики. InheritedWidget используется исключительно для того, чтобы передавать кусочки информации вниз по дереву:
Я рассмотрел два варианта использования InheritedWidget'а. Первый - как фабрика зависимостей сразу над виджетом использования. Второй - как высокоуровневый виджет над виджетом скрина, который получает зависимости из DI-контейнера.
Данные примеры были приведены для того, чтобы обосновать почему именно первый способ является плохой практикой и почему второй способ наиболее предпочтителен.
Вся статья про то, что сервис локатор зло и здесь вы берете сервис локатор.
Сервис локатор отличается от DI контейнера только способом использования. Вне контекста использования они представляют из себя одну и ту же сущность - контейнер. Но, многие, почему то, не чувствуют разницы между ними и считают, что это одно и то же.
DI-контейнер - это контейнер, который используется исключительно в composition root'е. Здесь контейнер - не глобальная переменная.
Service Locator - это контейнер, к которому компоненты приложения имеют глобальный доступ из любой точки приложения
get_it - это пакет, который предоставляет функционал контейнера. В документации он, конечно, позиционируется как сервис локатор, но никто не мешает его использовать как di-контейнер.
Касательно того, что вы называете get_it DI контейнером это неправильно. Это сервис локатор, потому что вы сами (СВОИМИ РУКАМИ) создаете граф зависимостей, то есть описываете все связи между объектами вручную. Эту проблему решает injectable, его можно считать DI контейнером. Тем не менее, он не решает проблемы сервис локатора, так как основан на нем. Использовать его так же не рекомендуется.
Возможно, в качестве di-container'а, действительно лучше использовать injectable а не get_it.
И я повторюсь, di-контейнер не используется как глоабальная переменная, поскольку в противном случае его уже нужно называть сервис локатором.
Если использовать get_it как локальную переменную в Composition Root, то в этом еще меньше смысла. Зачем тогда он вообще нужен?) Посмотрите инициализацию в моем шаблоне:
https://github.com/hawkkiller/sizzle_starter
И расскажите, как вы его используете локально, если говорите про "ленивую инициализацию". Вы явно значит обращаетесь к нему и дальше по приложению
Цитата из статьи:
В этом может помочь использование умного DI‑контейнера, например того, который предоставляет пакет get_it.
Я же не говорю, что обязательно нужно использовать именно get_it.
get_it я рассмотрел как частную реализацию di контейнера, которая из коробки предлагает широкий функционал с ресетом сервисов, разными видами фабрик и синглтонов. Разумеется, можно и свою структуру сделать.
Статья, конечно, не о том, как реализовать di-контейнер во флаттере.
Статья помечена маркером "Анализ", поэтому она не предполагает какие то частные примеры. Статья про то, как с точки зрения хорошего дизайна должен работать DI в приложении на Flutter. Рассматриваются варианты использования InheritedWidget'а - его плюсы и минусы, как можно делать, а как не можно, и, самое главное - почему. Ленивая инициализация, ограничение скоупа внутри поддерева скрина - это идеи как можно сделать, чтобы все класно-распрекрасно работало.
Бест практисес многие используют бездумно - делают так просто потому что все так делают, а мне нужны основания и понимание почему одна практика хорошая, а другая плохая. Про InheritedWidget'ы объяснения его выбора в качестве инжектора зависимостей я не встречал, зато встречал толпу индусов с сервис локатором. Поэтому проанализировал - действительно ли он хорош и так ли он хорош как кажется на первый взгляд, и поделился мыслями на его счет в статье.
Посмотрел ваш шаблон. Я в Flutter, да и вообще в mob dev'е не так давно, поэтому сложно понять что хорошо а что плохо. Много статьей, где пишут не самые правильные вещи (как оказывается впоследствии). Поэтому приходится многое придумывать самому. А у вас в шаблоне я увидел ту реализацию DI, к которой сам пришел. Фактически, та же концепция, о которой я писал в статье. Изучу подробнее, буду использовать.
Как оказывается, на ваш блог я уже натыкался ранее, но по объективным причинам не читал)
Интересная тематика, но очень высокий порог входа. Не хватает примеров или хотя бы ссылок на используемые термины. Да и фатализма у вас не отнять
Интересная тематика, но очень высокий порог входа.
Потому статья и помечена средним маркером сложности
Не хватает примеров или хотя бы ссылок на используемые термины.
Тем, кому посвящена статья, примеры не нужны, для них я пишу о стандартных вещах.
За терминами лучше обращаться к классикам - бучам, фаулерам, бруксам и мартинам.
Да и фатализма у вас не отнять
Риторика прочитанных статей и книжек изменила мой лексикон, поэтому теперь и я аналогично изрекаюсь)
Потому статья и помечена средним маркером сложности
Может быть, позволите один маленький вопросик? А что подразумевается по зависимостью? Зависимость чего, с чем или от чего?
На картинке типичное определение термина
Да. Спасибо. Значит, речь идёт о простой функциональной зависимости. Другими словами, при реализации класса A нужно знать класс B.
Только, у Вас на картинке, наверное, какая-то ошибка. Зависимость — это связь, а не класс. Просто, эта связь возникает, в том числе, и указанным образом, когда используется экземпляр. А можно объявить функцию, которая принимает аргментом экземпляр класса B.
Я не спорю, а рассуждаю.
В языке программирования C/C++ головная боль с такими зависимостями — это потенциальные утечки памяти: нужно следить за порядком (и корректностью) выполнения конструкторов/ деструкторов.
Это пример зависимости в контексте DI-паттерна. Он встречается чаще всего. Есть зависимости, например, от интерфейса и от абстрактного класса. В отдельных случаях интерфейсной зависимости клиент может становится зависимым от дефолтной реализации интерфейса. Много всякого есть, но концепция одна и та же.
Слово связь уместна, но не полнозвучна, чтобы понять концепцию.
Анализ InheritedWidget'а в Flutter