Проблемы InheritedWidget'а в Flutter

5bd7d772ff6706adf2b91a2a88e63383.jpg

InheritedWidget — антипаттерн?

Service Locator — зло. InheritedWidget — это сервис локатор с ограничениями.
В этой статье разберемся, как решают эти ограничения проблемы сервис локатора, и решают ли…

Чем InheritedWidget лучше переноса зависимостей.

Если зависимость создается в высокоуровневом компоненте, а используется на низком уровне, то возникают проблемы с ее передачей. Если передавать зависимости через параметры конструкторов промежуточных компонентов, то это вызовет кучу проблем с точки зрения дизайна (это будет ломать интерфейсы классов, нарушать SRP, DIP, инкапсуляцию и т.д.)
Сервис локатор — самое простое решение этой проблемы. Но оно порождает еще больше проблем, поэтому его нельзя использовать.

Таким образом, паттерн с использованием InheritedWidget’а становится интересным для использования.

Как использовать InheritedWidget.

Если InheritedWidget будет самостоятельно заниматься сборкой сервисов непосредственно над виджетом, в котором эти сервисы используются, то такой подход равносилен подходу, когда виджет, в котором используются зависимости, самостоятельно создает эти зависимости.

Сильная связность между клиентом и зависимостью никуда не пропадает, что становится причиной множества проблем. Например, таким образом созданный сервис невозможно тестировать, поскольку он будет зависеть от контекста — его можно будет протестировать только в виджете, который находится под InheritedWidget’ом.

Скоуп InheritedWidget’a.

У InheritedWidget’a есть scope доступа к сервисам, зарегистрированным в нем, который ограничен его дочерним поддеревом виджетов. Т.е. доступ к его сервисам может получить любой виджет его дочернего поддерева, а не любой компонент приложения.

Доступ к сервисам InheritedWidget’a могут получить только через контекст, а другие компоненты системы, не находящиеся в нужном месте дерева виджетов (подInheritedWidget’ом) не могут получить к ним доступ. Если InheritedWidget отвечает за сборку сервиса, доступ к которому он предлагает, то тестирование этого сервиса становится сложным, поскольку компоненты тестирования должны будут располагаться под InheritedWidget’ом.

Поэтому, InheritedWidget не должен отвечать за сборку сервисов. Сборка всех сервисов приложения должна происходить в DI-контейнере, который должен располагаться в composition root’е. Таким образом, станет возможным тестирование каждого сервиса, используемого в приложении.

InheritedWidget может сам получать нужные ему зависимости и использовать DI-container в composition root’е как service locator, но в использовании сервис локатора есть множество проблем, поэтому такой способ не подходит.
Инжектор, находящийся в composition root’е, должен через конструктор InheritedWidget’а осуществлять внедрение всех необходимых ему зависимостей. Если InheritedWidget будет находится вглуби деерва виджетов, то произойдет проблема переноса зависимостей через конструкторы промежуточных виджетов дерева. Поэтому InheritedWidget должен быть высокоуровневым виджетом и находится близко к корню дерева виджетов.
Наиболее логично использовать InheritedWidget’ы в качестве поставщика сервисов, используемых в скринах приложения. Таким образом, InheritedWidget’ы должны быть коренными виджетами скринов и требовать через свой конструктор от инжектора все зависимости, используемые в скрине.

Сервисы для скринов в InheritedWidget’е.

Сервисы должны создаваться лениво, только когда происходит построение скрина, в котором они будут использоваться.
После утилизации скрина, сервисы которые в нем использовались, больше не будут использоваться, поэтому их нужо утилизировать вместе со скрином. Иначе будет происходить утечка ресурсов.
В этом может помочь использование умного DI-контейнера, например того, который предоставляет пакет get_it.

Проблема раздувания InheritedWidget’ов.

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

Если все виджеты поддерева InheritedWidgetэ'а, отвечающего за все сервисы скрина, могут получить доступ к любому сервису инхеритед виджета, то это нормально, поскольку все эти сервисы должны использоваться в данном скрине. И поэтому не нужно безпричинно разграничивать доступ к сервисам внутри разных виджетов его поддерева. Однако, есть случаи, когда это можно сделать.

Ограничение доступа к сервисам InheritedWidget’а.

Проблема InheritedWidget’а заключается в том, что доступ к зарегистрированным в нем сервисам, которые предназначены для низкоуровневых виджетов поддерева, получают виджеты находящиеся выше по уровню виджетов назначения. Они не должны иметь доступ к этим сервисам из-за принципа меньшего знания, чтобы не иметь доступа к функциям и информации, к которым они не должны иметь доступ.

Можно ограничивать виджетам доступ к опасным для использования сервисам с сайд-эффектами. Например, сервис по модификации конфига скринов (сервис навигации между скринами приложения) можно давать только высокоуровневому виджету скрина, а все остальные малозначащие сервисы можно давать в безграничный доступ.

Чтобы реализовать ограничение доступа сервисов InheritedWidget’а, нужно внутри InheritedWidget’а скрина сделать систему скоупов, которая позволяет получить доступ к своим сервисам только определенным виджетам поддерева — например, виджету, который имеет определенный тип данных класса кастомного виджета.
InheritedWidget при обращении к нему из виджета его поддерва получает контекст — элемент, соответствующий виджету, который производит это обращение. Из контекста InheritedWidget может понять какой именно виджет поддерева пытается получить доступ к определенному сервису, который у него зарегистрирован. Т.е. в InheritedWidget’е нужно регистрировать виджеты поддерва — классы, которые могут получить доступ к определенным сервисам, которые хранятся внутри него.

Сравнение с service locator’ом.

InheritedWidget — это не глобальный объект, в отличие от сервис локатора.
InheritedWidget лучше, чем сервис локатор, поскольку в каждом InheritedWidget’е хранятся сервисы, имеющие отношение к данному скрину, а не ко всем скринам приложения и прочим компонентам приложения, как это происходит в сервис локаторе.

InheritedWidget позволяет сохранит открытый интерфейс, в отличие от сервис локатора.
Если виджет использует InheritedWidget для получения зависимостей, то это отражается в его интерфейсе. Запрос к InheritedWidget’у происходит из специальных методов виджета, которые являются частью его интерфейса. Эти методы извлекают зависимости из InheritedWidget’а и устанавливают их в качестве значений полей зависимостей.
Это позволяет отслеживать сложность виджетов (если виджет обретает слишком много зависимостей, то он становится слишком сложным, и с этим нужно бороться.
Также, позволяет узнать ответственность виджета — по зависимостям можно определить что делает виджет (например, если есть зависимость в виде репозитория для отправки запроса регистрации, значит виджет отвечает за скрин регистрации).

Вывод

Service Locator — это зло, а InheritedWidget — это меньшее зло.
InheritedWidget — это компромисс между сервис локатором, самостоятельным разрешением виджетом своих зависимостей и пробросом зависимостей через конструкторы промежуточных виджетов.

© Habrahabr.ru