Как объявлять скоп лайфтайма, если контейнер 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 шт):
Давайте сначала определимся с тем, что есть scope, а что есть время жизни объекта.
Scope - это ограниченная область видимости, где существуют множетсво объектов. Например, он может быть привяан к контексту запроса, он может быть привязан к представлению в WPF, он может быть искусственно создан и привязан к какому то бизнес-сценарию. Например, я когда то писал приложение на WPF и каждое представление в нем представляло собой свой собственный scope.
Время жизни объекта - это то, как контейнер управляет объектом, например объект может быть каждый раз, когда он нужен, создан заново или представлять собой сингтон, когда 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, то
- Композицией UI заведовал обычно PRISM (у него есть эта фишка с регионами)
- Созданием же представлений заведовал я через фабрики/прочие вещи.
- При этом, если, например, мне надо было открыть одну форму из другой, я использовал средства слабой связности, как события и EventBus - это помогало изолировать логику создания формы от самих форм. Таким образом, я мог, например, иметь реестр открытых окон и не открывать второй раз окно, которое уже открыто, даже если запросы на открытие приходят из абсолюьно разных мест.
Как результат,
- Мое приложение представляло собой композицию предсталений (и композицию контейнеров),
- Общение между VM разных представлений шло через EvenBus
- Конечно, сами VM тоже представляли собой иерахию. Явную (когда VM родительская имеет ссылку на дочернюю) и неявную (когда ваша дочерняя VM зависит от каких то условия и вы реально не знаете в родительнской, что должно быть отражено в дочернем)