4

Читаю "Внедрение зависимостей на платформе .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. Не очень понимаю, каким образом в середине жизненного цикла приложения создавать экземпляры объектов. В книге автор показывает в основном или элементарные примеры, или аспнет, в котором есть заранее продуманная точка - создание контроллера для обработки запроса.
Monk
  • 4,478
  • каким образом я могу тут объявить скоп - это задается при регистрации типа, контейнер сам следит за тем, когда надо создавать новый объект, а когда дать уже готовый, вам надо лишь попросить его через DI (обычно конструктор). Мне надо создать VM настроек - создает/отдает за вас контейнер, если тип помечен как синглтон, то будет один объект на все время работы приложения, если это простая регистрация - объект каждый раз новый, а если это Scope, то объект будет существовать скажем так, всю "цепочку вызовов". – EvgeniyZ May 09 '21 at 16:36
  • 2
    (комментарий джуна) Выглядит логичным создать класс-фабрику для SomeCommand. Созданную фабрику можно хранить как поле класса. Это уберет необходимость делать new SomeCommand. Для использованию using достаточно, что бы scope реализовывал IDisposable/IAsyncDisposable. – Sergey Skvortsov May 09 '21 at 16:37
  • в котором есть заранее продуманная точка - в том же WPF или где либо еще есть также эта точка, например в WPF вы инициализируете контейнер, а потом из него только один раз при старте забираете главное окно и его VM, все, дальше контейнер все типы и все остальное инициализирует за вас. Вообще, если идет разговор про WPF, то посмотрите реализацию Prism, он весь основан на DI контейнерах. – EvgeniyZ May 09 '21 at 16:37
  • @EvgeniyZ по скопу - я же спрашиваю, как его объявить. Понятно, что внутри скопа будут создаваться объекты, как они обозначены при регистрации. Как объявить сам скоп? По созданию объектов - я не понимаю, какой конструкцией заменить псевдокод выше. SomeCommand должен быть тогда не вызовом конструктора, а полем вьюшки? Тогда по ощущениям вся система будет создана на открытии главного окна. А мне хотелось бы создавать объекты только по необходимости. ПС: prism гляну. – Monk May 09 '21 at 16:41
  • @ssa112112 с одной стороны - да. С другой, фабрика только маскирует проблему, т.к. зависимость от какой то команды неявна и не подлежит простой замене, тогда как зависимость через конструктор например это покажет. Но тогда мне блин надо создавать команду заранее, что мне тоже не нравится (пусть её создание и "бесплатно" с точки зрения бизнес логики). В такой ситуации я боюсь придти к VM, которая принимает много параметров, т.к. вынуждена создавать каждую кнопку, которая на ней отображается. Кажется что решение должно быть проще. – Monk May 09 '21 at 16:43
  • Очень хороший вопрос, спасибо. Плюс от меня. Еще интересней увидеть хороший ответ. – aepot May 09 '21 at 16:48
  • Как объявить сам скоп? - вам не надо ничего объявлять, повторю, это все берет на себя контейнер. Регистрируете в нем тип, указываете его как Scope и все, он будет существовать если, очень грубо говоря примерно так, то есть при инициализации SomeClass, для которого требуется Logger и SecondClass (который тоже требует Logger), контейнер сделает один объект Logger и внедрит их в SomeClass и SecondClass. Следующий раз, когда вы повторно попросите SomeClass, у вас Logger будет уже другой. – EvgeniyZ May 09 '21 at 16:50
  • SomeCommand должен быть тогда не вызовом конструктора, а полем вьюшки? - Что такое SomeCommand? ICommand? Если да, то почему это вообще должно быть в контейнере? Если это отдельная логика, то через конструктор просите и дальше уже либо ссылку на него в теле класса держите, либо используете лишь конструктор. вся система будет создана на открытии главного окна - при первом обращение. Если у вас длинная цепочка зависимостей, то да, будут сразу, если у вас открытие нового окна по клику, то объект будет создан при клике и забыт после закрытия. – EvgeniyZ May 09 '21 at 16:53
  • @EvgeniyZ у меня ощущение, что я не могу объяснить проблему. При нажатии на кнопку в UI могут потребоваться новые объекты. Часть из них зарегистрированы как Scoped. Как DI поймет, что конкретный flow выполнения при нажатии - в скопе и экземпляр надо создать единожды? И как он поймет, что при повторном нажатии - надо создать новый экземпляр? ПС: в autofac есть например BeginLifetimeScope. Но он вызывается от контейнера. А контейнер должен быть в корне композиции. – Monk May 09 '21 at 16:58
  • @EvgeniyZ по созданию "команды" условной вроде понял. Хотя меня и смущает, что фактически могут быть созданы классы, которые так и не будут использованы (т.к. создать их в ходе выполнения видимо никак). – Monk May 09 '21 at 17:00
  • Хм, но если я не могу создавать объекты где хочу - то и скоп мне как таковой не нужен, ибо DI уже тоже не создает экземпляры. Фактически, мне надо переписывать код так, чтобы "скоп" принимал один экземпляр зависимости снаружи и отдавал его всем зависимым. Грустно, так переписывать надо очень много. – Monk May 09 '21 at 17:04
  • в UI могут потребоваться новые объекты - Не забывайте, что речь идет про DI, внедрение зависимостей, а они как внедряются? Либо через конструктор, либо через свойство. В вашем псевдокоде new SomeCommand(...) это не DI. Да, вы можете взять контейнер, через конструктор, и в любом месте написать var obj = container.Resolve<SomeType>(), вам это не мешает кто-либо сделать. Вы главное поймите что такое Scope. Когда вы просите корневой объект, то все зависимости для него, которые помечены как Scope, будут иметь один экземпляр и при создание нового корневого, будут и новые объекты. – EvgeniyZ May 09 '21 at 17:07

1 Answers1

5

Давайте сначала определимся с тем, что есть 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 зависит от каких то условия и вы реально не знаете в родительнской, что должно быть отражено в дочернем)
tym32167
  • 32,857
  • У меня примерно такая же картина в голове. Но Марк явно пишет - контейнер только в корне композиции. А вы добавляете контейнеры под вьюшки. У Марка приходится создавать буквально весь мир, чтобы запустить хоть что-то. У вас в итоге мини сервис локатор на вьюшку - вьюшка принимает только контейнер, я так понимаю, а из него уже достаются нужные зависимости и уходят дальше вглубь без самого контейнера? – Monk May 09 '21 at 18:06
  • Нет, с контейнерами работают фабрики. Вьюшка и все остальные зависимости не знают ничего ни о резолвах, ни о скоупах. Скоуп создается тогда, когда какой то фабрике надо создать представление. Вот фабрика уже знает о контейнере. – tym32167 May 09 '21 at 18:22
  • Марка читать полезно, но он сильно теоретик. Не надо воспринимать все его советы как истину в последней инстанции. Поглядите его курсы хотя бы, вы через час просмотра уже запутаетесь а его абстракциях. – tym32167 May 09 '21 at 18:24
  • Идея с фабриками понятна. Тесты бы ещё прикинуть, как тогда писать (Марк упирает на то, что его классы легко тестировать и выглядит это действительно так). ПС: а курсы, которые посмотреть - есть ссылки? Первый раз про них слышу =) – Monk May 09 '21 at 18:30
  • 1
    Тесты чего? Если объекты не знают ни про скоупы, ни про контейнеры, то какая разница будет в тестировании? Я курсы на pluralsight смотрел, это платно, но наверняка они уже утекли куда надо, уже лет 5 прошло с того времени. – tym32167 May 09 '21 at 19:04
  • Понял, спасибо. Вроде стало понятнее, как это должно быть. Теперь думаю, стоит пытаться переписывать старое (а мне понадобились скопы и их почти невозможно добавить "легко"), или костылять дальше хоть как-нибудь, зато дешево =) ПС: ответ пока не принимаю, вдруг ещё кто чего интересного посоветует. – Monk May 09 '21 at 19:11
  • Ссылки на его курс можно найти на его страничке справа, я глядел его курсы по SOLID, они мне тогда показались оторванными от реальности вообще. Он взял простую задачу и написал такую шляпу, что я бы такое ревью не пропустил бы ни за что - но это были мои впечатления того времени, я уже не помню деталей. – tym32167 May 09 '21 at 20:11
  • 2
    Поглядите на то, чем он сейчас занимается - его если и зовут программировать, то тогда, когда на него уже есть бюджет, то есть либо новый проект либо сильно запущенный, что простые смертные не справляются. Вот тогда его бескомпромисссные советы может и работают. Я бы хотел поглядеть его код не в простом микросервисе, а в enterprise настольной UI rich мешанине, где уровень сложности гораздо выше. – tym32167 May 09 '21 at 20:15
  • я не говорю, что он хороший или плохой прогер, просто его советы исходят из его опыта, а мой опыт кричит, что советы для микросервисов и советы для настольных приложений - зачастую разные советы. И надо кртически относиться к тем и другим. Не делайте чтото просто потому, что он так посоветовал. Думайте свой головой в первую очередь. – tym32167 May 09 '21 at 20:17
  • У меня старый старый десктопный проект, в нём не то что DI - в нём даже нормально классы не везде выделены. И новые фичи всё сложнее заводятся. Вот сижу, курю, как бы сделать хорошо и не переписывать целиком =) – Monk May 09 '21 at 20:18
  • 1
    у меня на прошлой работе были несколько WPF проектов, некоторые были 10+ лет возрастом, самый крупный из них представлял собой солюшен с 100+ проектами. Над ними в разное время трудились разные люди, потому там было все, начиная от своих ThreadPool и заканчивая своей реализацией виртуализации. У меня даже как то возникала мысль переписвть всё, но я вовремя опомнился от такого (прочитал тода рефакторинг Фаулера и решил идти этим путем). – tym32167 May 09 '21 at 20:23
  • 1
    @Monk, ну я, например, контейнер в фабрики не тащу, мне 20 строчек не зазорно написать, ответственность у фабрики одна, поэтому проблем не вижу. Соответственно, контейнер только в корне композиции и есть. Конечно, только нового кода это касается, в легаси дичи всякой хватает :) – Андрей NOP May 09 '21 at 20:34
  • @АндрейNOP это смотря что именно называть корнем композиции и что называть фабрикой. Например, если вы модульное приложение пишете, то у вас у каждого модуля будет свой корень композиции. У меня есть пример и не очень хорошего подхода, когда я посто тупо могу сгенерировать вьюшку в корне компзоции модуля, я вообще этим не горжусь ни разу, но это экономит мое время, а говнокод изолирован модулем, потому я в таких случаях сплю спокойно :) – tym32167 May 09 '21 at 20:44