Как объявлять скоп лайфтайма, если контейнер DI не должен покидать корень композиции?

Читаю "Внедрение зависимостей на платформе .NET 2-е издание" (на русском, вышла весной). Автор явно несколько раз пишет -

При использовании DI-контейнера корень композиции должен быть единствен-ным местом, где используется этот контейнер. Применение DI-контейнера вне корня композиции приводит к возникновению антипаттерна «Локатор сервисов» (Service Locator), который будет рассмотрен в следующей главе.

Приводит примеры, показывает что это некрасиво. Хорошо, предположим, я согласен.

Есть у меня WPF (или любое другое) десктоп приложение. Каждое нажатие какой то "кнопки" в UI я считаю действием в отдельном скопе. Т.е. часть API должно создавать соответствующие по жизни экземпляры. Условно, в обработчике ICommand.Execute такой псевдокод:

void ICommand.Execute(object parameter)
{
  using (DI.CreateSomeScope())
    new SomeCommand(...).Execute(parameter);
}

Вопроса два, первый важный, второй в комментах разобрали, но может у кого есть своё видение вопроса - делитесь тоже:

  • каким образом я могу тут объявить скоп? Скоп согласно книге и большинству DI фреймворков - вполне себе ответственность контейнера. Тащить его сюда - явный сервис локатор.
  • И что делать с new SomeCommand? Ну т.е. у меня например уже запущено главное окно программы, отобразилось всё что мне хочется. И тут пользователь нажимает "Настройки". Мне надо создать VM настроек, чтобы потом присвоить в забинженное свойство, для изменения приложения. А за создание нестабильных (вм вполне подходит под нестабильные) зависимостей отвечает опять DI. Не очень понимаю, каким образом в середине жизненного цикла приложения создавать экземпляры объектов. В книге автор показывает в основном или элементарные примеры, или аспнет, в котором есть заранее продуманная точка - создание контроллера для обработки запроса.

Ответы (1 шт):

Автор решения: tym32167

Давайте сначала определимся с тем, что есть scope, а что есть время жизни объекта.

  1. Scope - это ограниченная область видимости, где существуют множетсво объектов. Например, он может быть привяан к контексту запроса, он может быть привязан к представлению в WPF, он может быть искусственно создан и привязан к какому то бизнес-сценарию. Например, я когда то писал приложение на WPF и каждое представление в нем представляло собой свой собственный scope.

  2. Время жизни объекта - это то, как контейнер управляет объектом, например объект может быть каждый раз, когда он нужен, создан заново или представлять собой сингтон, когда 1 объект на контейнер.

Если scope заранее известен (например, request scope), то вы можете указать, что объект будет, например, синглтоном на какой то конкретный scope, но при этом вы указываете и scope и время жизни.

Scope обычно представляется как иерархия контейнеров (по крайней мере у меня так было). То есть есть основной контейнер, а есть дочерний, который только для View. В этом случае, если при резолве в дочернем контейнере тип не найден, поиск произойдет по родительскому. Такое устройство позволяло мне регистрировать дочерние View/ViewModel как синглтоны в дочернем контейнере и все остальные их зависимости управлялись через этот дочерний контейнер Таким образом у меня был Scope уровня View/ViewModel, но при этом была возможность получать объекты из основного контейнера, если типы не были зарегистрированы в дочернем. Например, иметь единый eventBus на предсталение - ViewEvenBus - как синглтон в дочернем, и единый на приложение - ApplicationEvenBus - как синлтон в корневом контейнере.

Теперь перейдем к Service Locator паттерну. Давайте подумаем, почему он считается анти паттерном? Мой мнение - если у вас есть возможность резолвить все, что вы хотите, в классах, которые не предназначены для резолва, то вы теряете контроль. В больших классах вам будет трудно не только отследить время жизни, но и вообще понимание от чего именно зависит класс становится затруднительным, так как получение зависимостей размазано по всему коду, а не сосредоточено в одном месте.

Потому, неапример, я иногда допускаю создание объектом в классах, но я использую для этого фабрики. Например, если класс явно зависит от IFactory<T>, то вы уже понимаете, что 1) класс не может создать ничего, кроме T, 2) У него эта зависимость явно прописана в конструкторе.

Таким образом, если вы пытаетесь объявить scope внутри представления, вы скорее всего делаете что то не так. Scope - это то, что должно работать на уровне порождающих классов, будь то фабрики или конрейнеры.

Потому когда я работал с UI, то

  1. Композицией UI заведовал обычно PRISM (у него есть эта фишка с регионами)
  2. Созданием же представлений заведовал я через фабрики/прочие вещи.
  3. При этом, если, например, мне надо было открыть одну форму из другой, я использовал средства слабой связности, как события и EventBus - это помогало изолировать логику создания формы от самих форм. Таким образом, я мог, например, иметь реестр открытых окон и не открывать второй раз окно, которое уже открыто, даже если запросы на открытие приходят из абсолюьно разных мест.

Как результат,

  1. Мое приложение представляло собой композицию предсталений (и композицию контейнеров),
  2. Общение между VM разных представлений шло через EvenBus
  3. Конечно, сами VM тоже представляли собой иерахию. Явную (когда VM родительская имеет ссылку на дочернюю) и неявную (когда ваша дочерняя VM зависит от каких то условия и вы реально не знаете в родительнской, что должно быть отражено в дочернем)
→ Ссылка