Сделайте внедрение зависимостей между пакетами тривиальной задачей с этой настройкой
TL; DR: Используя
injectable
,injectable_generator
иget_it
, мы создаем экземплярget_it
и передаем его через разные пакеты, и каждый пакет применяет свою конфигурацию к этому экземпляруget_it
.
вступление
Обычно настройка внедрения зависимостей включает в себя настройку, которая выглядит следующим образом:
Мы извлекаем экземпляр get_it
и используем причудливую магию генерации кода, чтобы что-то происходило в фоновом режиме, и мы аннотируем наши классы такими вещами, как @Injectable
или @LazySingleton
, чтобы сделать их доступными через di.
Все это удобный и простой способ сделать наш di. Это намного лучше, чем постоянно все прописывать вручную.
Но мы говорим только об одном пакете. Обычно это ваше приложение Flutter, например.
Что делать, если у вас есть проект, в котором вы контролируете несколько пакетов, но все равно хотите использовать это простое ди-решение?
Возможно, вам понадобится такая структура пакета, чтобы ослабить зависимости в вашем проекте или применить определенную архитектуру (предвосхищая ;)).
Что ж, у меня есть для вас решение!
Настройка потока зависимостей
Прежде чем мы начнем, я хочу прояснить, как будет выглядеть конечный продукт, с помощью следующей блок-схемы:
Если подумать, это имеет смысл, потому что App
зависит от всех остальных пакетов.
Это действительно настолько просто.
Как это реализовать
Примечание. Для данного примера вся эта настройка чрезвычайно избыточна, но она предназначена для того, чтобы помочь донести идею.
Начнем с определения простого приложения. Обычно я использую счетчик, так что давайте начнем с него.
Наше приложение будет базовым счетчиком, который можно увеличивать, значение которого хранится в памяти. К счастью, Flutter уже предоставляет значительную часть этого, когда мы создаем новый проект.
Приложение будет разделено на четыре пакета:
- пакет
counter
, который экспортирует интерфейсCounter
и интерфейсCounterStorage
, а также реализацию интерфейсаCounter
с именемCounterImpl
- пакет
counter_storage
, реализующий интерфейс хранилища из пакетаcounter
. - пакет
logger
, который экспортирует интерфейсLogger
и определяет реализациюLoggerImpl
. - пакет
app
, сгенерированный Flutter.
Вот график, показывающий, как эти пакеты зависят друг от друга:
Уровень app
берет счетчик и его реализацию из counter
, а также использует Logger
пакета logger
.
Причина, по которой counter_storage
зависит от counter
, заключается в том, что ему нужен интерфейс CounterStorage
, чтобы counter_storage
мог реализовать CounterStorageImpl
.
На практике выполнение кода будет происходить так:
Внедрение зависимостей — вот как мы это делаем. Итак, вот пошагово:
Шаг 1: Настройте пакеты
Для согласованности мы выполним реализацию DI точно так же, как на приведенных выше диаграммах. Вы можете найти готовую реализацию этого шага здесь.
Для создания пакетов вы можете использовать следующие команды (по порядку):
mkdir dart-easy-di cd dart-easy-di # 1st package dart create counter # 2nd package dart create counter_storage # 3rd package dart create logger # Finally, the app package flutter create app
(Примечание: вы можете скопировать и вставить весь этот блок команд, вставить его в свой терминал и запустить)
Мы добавим необходимые классы в наши пакеты, чтобы завершить этот шаг.
В пакет counter
добавьте следующие классы:
CounterImpl
— это базовая реализация Counter
, которая использует CounterStorage
в качестве зависимости.
Нам нужно экспортировать интерфейсы Counter
и CounterStorage
, поэтому мы делаем это здесь:
Первая конфигурация пакета готова! На следующий. Давайте настроим пакет CounterStorage
.
Добавьте следующие файлы:
Нам нужно добавить зависимость от counter
, чтобы мы могли его импортировать.
На этом наш пакет counter_storage
готов. Вы заметите, что мы ничего не экспортируем, так как же мы в конечном итоге получим к этому доступ? С магией DI, конечно!
Мы доберемся до этого в следующих шагах. Давайте позаботимся о logger
дальше. Добавьте следующие файлы:
Наконец, мы экспортируем интерфейс Logger
:
Все в порядке! Остался только пакет app
. Для этого шага мы добавим зависимости в pubspec
:
Шаг 2. Настройка внедрения зависимостей с помощью GetIt и Injectable
Вы можете найти готовую реализацию этого шага здесь.
Идея состоит в том, чтобы экспортировать метод, который берет экземпляр get_it
и настраивает все, что ему нужно.
Во-первых, всем нашим пакетам нужно несколько зависимостей. Давайте добавим их с причудливым сценарием ниже:
# foreach dir for d in */; do cd "$d" echo "$d" dart pub add get_it injectable dart pub add --dev injectable_generator build_runner echo "\n" cd .. done
Не волнуйтесь, это не взломает ваш компьютер (вероятно). Он установит некоторые необходимые зависимости для нашего решения DI для каждого из наших пакетов, а именно get_it
, injectable
, injectable_generator
и build_runner
.
Затем для каждого из пакетов counter
, counter_storage
и logger
мы добавим следующее (пока игнорируйте любые ошибки вашего редактора):
Затем мы экспортируем эту функцию configureDependencies
в каждый из counter/lib/counter.dart
, counter_storage/lib/counter_storage.dart
и logger/lib/logger.dart
:
Наконец, мы собираем все вместе в app
:
В строках 21-23 вы можете видеть, что мы вызываем каждую из configureDependencies
функций нашего пакета, которые мы только что экспортировали. Мы присваиваем импорту ключевое слово as
, чтобы избежать конфликтов имен.
Последний шаг — мы вызываем нашу функцию configureDependencies
app
в app/lib/main.dart
:
Мы сделали 95% необходимой нам настройки, но у нас все еще есть досадные ошибки в файлах di.dart
. Чтобы избавиться от них, запустите следующий скрипт:
# foreach dir for d in */; do cd "$d" echo "$d" dart run build_runner build echo "\n" cd .. done
Вам нужно будет запускать этот скрипт каждый раз, когда вы каким-либо образом обновляете свои зависимости. Примерами этого являются добавление тега @Injectable
, добавление новой зависимости к классу и т. д.
Я покажу вам, как вы можете добавить этот скрипт в качестве простой в использовании функции в вашей конфигурации bash или zsh здесь.
Запуск этого сценария приведет к появлению нового файла в каталоге di/
каждого пакета с именем di.config.dart
. Этот файл содержит фактические связи между классами, которые допускают внедрение зависимостей. Ошибки должны исчезнуть, но мы еще не закончили.
Вы должны игнорировать этот файл в своем .gitignore
, поэтому обязательно добавьте **/di/*.config.dart
или что-то в этом роде. Я лично делаю один файл .gitignore
под dart-easy-di/
(который содержит все наши пакеты) и добавляю оператор ignore только туда.
Итак, на данный момент осталось только аннотировать наши файлы.
Шаг 3: Аннотирование с помощью Injectable
Вы можете найти готовую реализацию этого здесь.
Мы аннотируем наши реализации соответствующими тегами. Сюда входят CounterImpl
, CounterStorageImpl
и LoggerImpl
:
При этом мы связали наши реализации с их интерфейсами. Нам даже не нужно было экспортировать реализации!
Нам нужно снова запустить скрипт build_runner
, так как мы изменили зависимости:
# foreach dir for d in */; do cd "$d" echo "$d" dart run build_runner build echo "\n" cd .. done
Примечание: при запуске этого скрипта вы можете получать предупреждения о некоторых незарегистрированных зависимостях. Вы можете смело игнорировать их.
Теперь мы официально закончили подготовку. Мы можем использовать все это в нашем app
.
Использование наших пакетов в приложении Flutter
Готовую реализацию вы можете найти здесь.
В app/lib/main.dart
делаем следующее:
В строках 3-11 мы добавляем импорт для наших пакетов. Вы также заметите, что файл импорта di.dart
имеет псевдоним app
. Это делается для того, чтобы избежать конфликтов пространств имен.
В строках 16 -> 18 мы используем наш экземпляр DI для получения счетчика и регистратора. Затем мы используем их в строках 22, 23 и 41. Попробуйте и посмотрите, действительно ли работает регистратор!
Если вы обнаружите какие-либо ошибки с экземпляром get_it
, повторный запуск сценария build_runner
может решить их.
Более реалистичный сценарий
Окончательную реализацию этого шага вы можете найти здесь.
Хотя это работает, это не совсем правильно. Мы сделали так, чтобы пакет app
напрямую зависел от пакета counter_storage
, несмотря на то, что app
вообще не взаимодействует с ним (кроме вызова его configureDependencies
.
Как нам сделать так, чтобы мы не ломали наш граф зависимостей, сохраняя при этом DI таким, какой он есть?
Ответ со слоем di
. Это пакет с единственной целью получить экземпляр get_it
и передать его всем пакетам, которые в нем нуждаются.
Таким образом, ваш слой app
зависит только от di
, а di
зависит от остальных и распространяет ваше решение DI.
app
, конечно, по-прежнему будет зависеть от counter
и logger
, потому что ему нужны их экспортированные интерфейсы.
Чтобы реализовать наш di как на схеме выше, сделайте следующее (откройте терминал в dart-easy-di
:
# Create the package dart create di # Install its dependencies cd di dart pub add get_it injectable dart pub add --dev injectable_generator build_runner
Добавьте следующие зависимости в его pubspec.yaml
и теперь мы можем удалить зависимость counter_storage
от app/pubspec.yaml
.
Затем мы настраиваем пакет di
с собственным файлом di.dart
, который выглядит следующим образом:
Это похоже на то, что мы делали в app/lib/di/di.dart
, не так ли? Следующий шаг — экспортировать эту функцию configureDependencies
из di
, импортировать ее в app
и затем вызвать. Вот шаги, чтобы сделать это:
Экспорт из пакета di
:
Импортируйте в app
(возможно, вам придется запустить dart pub get
позже)
Обновление app/lib/di/di.dart
И, наконец, снова запустите скрипт build_runner
.
# foreach dir for d in */; do cd "$d" echo "$d" dart run build_runner build echo "\n" cd .. done
Если вы все сделали правильно (а, конечно же, вы сделали это, вы невероятно умный человек), то вы внедрили межпакетную инъекцию зависимостей.
Бонус
Вот команда для настройки функции build_runner
в конфигурации вашей оболочки. Это упростит запуск скрипта build_runner
.
echo " function build_all { for d in */; do cd "$d" echo "$d" dart run build_runner build echo "\n" cd .. done } " >> ~/.zshrc
Замените .zshrc
своим собственным файлом конфигурации оболочки. После того, как вы запустите это в своем терминале, перезапустите терминал или запустите source ~/.zshrc
.
Теперь вы можете ввести build_all
в своем терминале, когда вы находитесь в каталоге dart-easy-di
, и он запустит этап сборки для вас для всех пакетов.
Что вы думаете об этом решении?
Проверьте мой репозиторий для полной реализации.