Сделайте внедрение зависимостей между пакетами тривиальной задачей с этой настройкой

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, и он запустит этап сборки для вас для всех пакетов.

Что вы думаете об этом решении?

Проверьте мой репозиторий для полной реализации.

Рекомендации