Pull to refresh

Comments 36

А откуда ViewModel будет знать, что именно ложить/класть в тот же protected IProgress progress;?

К примеру у меня два IProgress'а — один текстовый, а второй графический. И они за разное отвечают. Или вообще ещё проще — у меня этих прогрессов 5 (к примеру 5 разных процессов) — что тогда?
Если в один класс модели придется класть разные IProgress — вы в любом случае откажетесь от ServiceLocator, и будете инжектить нужный Progress-класс руками.
Если говорить конкретно про IoC, при инициализации контейнера обычно получается список вида «Интерфейс I1 по дефолту реализовывать классом C1, Интерфейс I2 по дефолту реализовывать классом С2...» В вашем случае возможны пять разных реализаций, а значит, дефолта быть не может.
Может я что-то не понимаю, но какую функцию выполняет в WPF ViewModel? Зачем в неё инжектить интерфейс модальных окон? Разве это не функция пользовтаельского интерфейса, не функция View?
Control Flow осуществляет ViewModel, поэтому и модальные диалоги ей вызывать. Это если паттерн MVVM, а вот если MVPVM, то, м.б. можно и как-то по-другому, но не уверен, у меня нет соответствующего опыта.
Control Flow осуществляет ViewModel, поэтому и модальные диалоги ей вызывать. Это если паттерн MVVM


Это не паттерн MVVM, а черти-что и лицемерие со стороны M$, ИМХО. Да Вы написали MVPVM, но для меня это не паттерн а попытка как-то обозвать (или ввести четкий термин/ярлык) решение из раздела «костыльное программирование» (я в хорошем смысле, то есть непротив), которое в свою очередь появилось из-за невозможности оставаться в рамках чистого MVVM.

Ваша правда, что на практике не получается обойтись без управления частями UI из VM, хотя по ванильному паттерну MVVM иметь ссылку на UI внутри VM категорически запрещено.
Более того, на практике, хранение прямой или опосредованной ссылки на UI ведет с большой вероятностью к массивным утечкам (утечет VM — полбеды; утечет VM+UI — швах). Тогда приходится еще и реализовывать вычистку нелегального барахла через IDisposable.

Отвечая более детально на предыдущий комментарий, приведу пример.

1. В VM есть команда удаления некоей сущности. Изменение это деструктивное и следует показать юзеру запрос «Да — Нет»
2. Чтобы иметь возможность из VM что-то показать юзеру, заводится некий хэлпер, Expression Behavior или еще какая-то сущность, играющая роль прокси между VM и V. Кто-то обзывает ее буковкой :P.
3. Дальше ссылка на эту прокси-сущность гоняется взад и вперед между VM'ми с целью наделить VM арендованным правом запустить тентаклю в UI.
Уж точно не View. Открытием окон управляет VIewModel, которые моделируют UI приложения и могут работать без View вообще. Задача View — уметь отобразить состояние ViewModel на экране. Поэтому во ViewModel так или иначе для этих целей что-то придется инжектить.
Открытием окон обычно занимается некий NavigationManager. Буквально он либо дергает навигационный метод в WPF Frame, либо кладет нужного типа VM в ContentControl с пачкой <DataTemplate DataType="{x:Type SomeViewModel}.

Ванильный MVVM вообще может означать инстанциирование ViewModel через XAML в секции Resources. Много где видел предложение инстанциировать VM'ли в ресурсах App.xaml. Получается, что в изначальной трактовке задача VM — реализовывать жизненный цикл Model, контролировать потоки данных и реализовывать ЛОГИКУ переключений частей UI. Все это следует из главного принципа «Отделяем UI от логики/кода».
Но на практике это накладывает очень много ограничений, поэтому все сначала «говнокодят» исходя из текущих практических нужд, а потом это обзывают MVPVM.
А зачем ViewModel жизненный цикл модели? Доступа к интерфейсу модели недостаточно?
Абсолютно согласен с таким подходом, конструктор тоже может быть God-object. Кстати, в JavaScript использую декораторы ES6 для DI.
class Foo{
    @inject("bar")
    bar;
}
Если мне не изменяет память, Марк в своей книге Dependency Injection in .NET описывал ваш подход как «внедрение зависимостей через свойства» (property injection) и допускал его использование как минимум в следующих случаях:
1) Зависимость нужна в большинстве классов (ваш случай, еще один хороший пример это какой-нибудь ILogger)
2) Зависимость имеет значение по умолчанию
3) Классы зависят друг от друга

Только я не понял, зачем во втором случае вы делаете свойства protected, сделайте их публичными, тогда они станут частью контракта класса без всякой магии.
В реальности, с учётом того, что никто извне не использует ViewModels напрямую — да, можно сделать и публичными.
Вот, так гораздо понятнее — сбило с толку protected
Не совсем так.
2) Зависимости имеют локальные значения по умолчанию.
Иначе получите типовой антипаттерн из того же Симана.
Марк Симан написал мне в блоге в комментариях, что он считает, что когда у ViewModel столько зависимостей, то нарушен SRP. Возможно, он прав. Но, что делать если в реальности три зависимости используются для создания модели, а остальные — просто утилитарные. Кто-нибудь как-то решал эту проблему более элегантным способом, чем описано в этой статье?
Как вариант, можно использовать Factory. В сигнатуру фабричного метода поместить все зависимости для самой модели, а утилитарные зависимости попросить в саму фабрику и передать создаваемому объекту:

public class ViewModelFactory : IViewModelFactory
{
    private readonly IEventAggregator _aggregator;
    private readonly IProgress _progress;
    private readonly IPromptCreator _promptCreator;

    public ViewModelFactory(IEventAggregator aggregator, IProgress progress, IPromptCreator promptCreator)
    {
        _aggregator = aggregator;
        _progress = progress;
        _promptCreator = promptCreator;
    }

    public ViewModel CreateViewModel(IPaymentSystem paymentSystem, IRulesValidator rulesValidator) 
        => new ViewModel(_aggregator, _progress, _promptCreator, paymentSystem, rulesValidator);
}


Не буду утверждать, что это более элегантно, чем Property Injection.
Так ведь конструктор ViewModel, выходит, все равно будет содержать весь этот цирк. Это никак не решает проблему. Чисто технически инжектировать даже в 100 зависимостей конструктора не проблема. Но вод SRP будет злостно нарушен.
А по поводу Property Injection. Мое мнение, что это такая же неявная зависимость. Тестируемость? Что мешает применить Локатор не локально, а к такому же protected-полю? Здесь из плюсов разве что читаемость, ибо сразу видно что имеет место быть внедрение, и что это неявная зависимость. Однако на мой взгляд это все отговорки.
У меня не было самой проблемы.
Мне не нужны ни агрегатор событий, ни специальный IProgress, ни тем более модальные окна.
Даже по отдельности не нужны, не то что одновременно.

Какой бардак! Слишком много шума в декларации конструктора.

Это запах меркаптана от вашей газовой плиты.
Проблема не в декларации конструктора, а в проектировании и все ваши модификации только заметают мусор под ковер.
Все «утилитарные» зависимости на самом деле или не нужны совсем (прогресс) или нужны редко (модальные диалоги с агрегатором событий).
Не нужны вам конкретно? У меня они в каждой VM. Если у вас такая специфика приложений, это не значит, что у других такая же.
Что касается подковёрного мусора: есть более конкретное описание того в чём проблема предложенной модификации и возможные пути решения?
Не нужны вам конкретно? У меня они в каждой VM.

Итак, у вас есть зависимости, которые не нравятся вам самому.
Я от зависимостей, которые мне не нравятся, избавляюсь, а если это сделать по каким-то причинам нельзя, делаю их явными.
По моему мнению лучший способ сделать зависимость явной — сделать ее параметром конструктора.
Если у вас такая специфика приложений, это не значит, что у других такая же.

А в чем специфика? Вам досталось такое легаси, это явное требование заказчика, это навязано каким-нибудь фреймворком? По-настоящему безнадежен только второй вариант.
Что касается подковёрного мусора: есть более конкретное описание того в чём проблема предложенной модификации и возможные пути решения?

Проблема в том, что перенос зависимостей из параметров конструктора скорее вреден чем бесполезен, так как просто ухудшает читаемость без всяких дополнительных плюшек. Ни одна из этих зависимостей не является опциональной для основного функционала. Пропустить любую из них при чтении кода значит составить заведомо неверное представление о данной конкретной ViewModel.
Следовательно — место им в параметрах конструктора и любой другой вариант будет хуже, а все решения связаны с устранением таких зависимостей:
1) Модальные диалоги — по возможности выпилить совсем как наихудшую разновидность фриза. Там, где выпилить не удается, оставить зависимость в конструкторе — такая часть Control Flow должна быть явной.
2) Агрегатор событий можно заменить на конкретные источники событий, они скажут гораздо больше, чем агрегатор, который сам по себе является разновидностью Service Locator со всеми его проблемами.
3) Прогресс — если это не подразумевает фриз, то реализовать как отдельный стек MVVM, т.е. длительные задачи, неважно кем инициированные, публикуются отдельной моделью, над которой есть свои ViewModel и View для отображения прогресса. Ваша ViewModel должна только вызвать метод собственной модели. Если же нужен именно фриз, можно сделать это частью интерфейса сервиса модальных диалогов.
Итак, у вас есть зависимости, которые не нравятся вам самому.

Что значит не нравятся, откуда вы это взяли?

По моему мнению лучший способ сделать зависимость явной — сделать ее параметром конструктора.

В статье идёт речь как раз о том, что утилитарные зависимости бессмысленно вытаскивать в конструктор, поскольку ничего они для чтения хорошего не несут. Никто на нашем проекте ни разу не ощутил боли от того, что мы вынесли их из конструктора.

А в чем специфика? Вам досталось такое легаси, это явное требование заказчика, это навязано каким-нибудь фреймворком? По-настоящему безнадежен только второй вариант.

Специфика в том, что есть куча операций, длительность которых невозможно заранее рассчитать (с устройствами мы работаем очень много, которые никаких нотификаций о прогрессе не поддерживают). И есть куча модальных диалогов, просто потому что они нужны в ПО, не понимаю, что здесь объяснять… они вызваны необходимостью запрашивать подтверждения действий у пользователя.

1) Модальные диалоги — по возможности выпилить совсем как наихудшую разновидность фриза. Там, где выпилить не удается, оставить зависимость в конструкторе — такая часть Control Flow должна быть явной.

Выпилить невозможно, потому что это не разновидность фриза, а необходимые запросы к пользователю. В конструкторе ничего оставлять не надо, потому что нафиг не надо, зачем и кому надо смотреть на наличие этой зависимости — непонятно, они всё равно есть почти везде.
2) Агрегатор событий можно заменить на конкретные источники событий, они скажут гораздо больше, чем агрегатор, который сам по себе является разновидностью Service Locator со всеми его проблемами.

Проблемы зависят от контекста. У нас проблем нет.
3) Прогресс — если это не подразумевает фриз, то реализовать как отдельный стек MVVM, т.е. длительные задачи, неважно кем инициированные, публикуются отдельной моделью, над которой есть свои ViewModel и View для отображения прогресса. Ваша ViewModel должна только вызвать метод собственной модели. Если же нужен именно фриз, можно сделать это частью интерфейса сервиса модальных диалогов.

Святая корова, зачем так сложно? Плодить чего-то там, если можно всего этого не делать?
Что значит не нравятся, откуда вы это взяли?

Из того факта, что вы не хотите видеть эти зависимости на самом заметном месте.
В статье идёт речь как раз о том, что утилитарные зависимости бессмысленно вытаскивать в конструктор, поскольку ничего они для чтения хорошего не несут. Никто на нашем проекте ни разу не ощутил боли от того, что мы вынесли их из конструктора.

Хорошо, что вы чувствуете именно так.
Для меня же все с точностью до наоборот: читаться стало значительно хуже, так как в самом очевидном месте всех необходимых зависимостей нет. Уж поверьте, я был бы крайне огорчен, пропустив любую из этих зависимостей, например, при рефакторинге. До вашей переделки это было бы невозможно.
Мало того, теперь после конструирования получается неполноценный объект: ему еще надо инициализировать свойства. А как вишенка на торт в класс добавлены атрибуты, дающие жесткую привязку к MEF.
Специфика в том, что есть куча операций, длительность которых невозможно заранее рассчитать (с устройствами мы работаем очень много, которые никаких нотификаций о прогрессе не поддерживают). И есть куча модальных диалогов, просто потому что они нужны в ПО, не понимаю, что здесь объяснять… они вызваны необходимостью запрашивать подтверждения действий у пользователя.

Если нет нотификаций о прогрессе как таковых, то как вы его отображаете?
Кучи модальных диалогов не нужны, как и подтверждения действий пользователя. Смотрите хоть у Купера, хоть у Раскина. Подтверждения объективно требуют только необратимые операции (фарш невозможно провернуть назад). И если такого у вас много, то это весьма специфический проект.
Святая корова, зачем так сложно? Плодить чего-то там, если можно всего этого не делать?

Не вижу ничего сложного. Мои ViewModel оказываются проще ваших.
так как в самом очевидном месте всех необходимых зависимостей нет. Уж поверьте, я был бы крайне огорчен, пропустив любую из этих зависимостей, например, при рефакторинге. До вашей переделки это было бы невозможно.

Рефакторинг этих ViewModel вообще никак не зависит от этих зависимостей, мы от этого ни разу не пострадали. Это стабильные зависимости и в ближайшие годы они никуда не уедут и наблюдать их в конструкторе никому в нашем проекте не интересно, видимо, вам было бы интересно по соображениям о мифических плюсах, которые никогда не пригодятся.

Мало того, теперь после конструирования получается неполноценный объект: ему еще надо инициализировать свойства. А как вишенка на торт в класс добавлены атрибуты, дающие жесткую привязку к MEF.

Всё равно придётся прописывать ImportingConstructor и другие атрибуты специфичные для MEF. Как от этого избавляться? Отгораживать полностью MEF?

Если нет нотификаций о прогрессе как таковых, то как вы его отображаете?

Intederminate Progress Bar. Чтобы пользователь понимал, что приложение не зависло наглухо и не дать затыкать UI.

Кучи модальных диалогов не нужны, как и подтверждения действий пользователя. Смотрите хоть у Купера, хоть у Раскина. Подтверждения объективно требуют только необратимые операции (фарш невозможно провернуть назад). И если такого у вас много, то это весьма специфический проект.

Именно так, у нас специфичный проект. Вообще я зачастую акцентирую внимание на том, что не надо выдвигать догмы, мне нравится Марк Симан, но считаю его объективным недостатком — догматизм.

Не вижу ничего сложного. Мои ViewModel оказываются проще ваших

За счёт чего? Куча лишних классов лучше одного, с помощью которого можно показывать прогресс бар?
Всё равно придётся прописывать ImportingConstructor и другие атрибуты специфичные для MEF. Как от этого избавляться? Отгораживать полностью MEF?

Использовать вместо MEF нормальный DI-контейнер, например, Autofac.
Он, кстати, может успешно интегрироваться с MEF, если последний вам для чего-то еще будет нужен.
Intederminate Progress Bar. Чтобы пользователь понимал, что приложение не зависло наглухо и не дать затыкать UI.

Ну с фальшпрогрессом все еще проще чем с настоящим.
Это стабильные зависимости и в ближайшие годы они никуда не уедут и наблюдать их в конструкторе никому в нашем проекте не интересно,

Их интересно наблюдать перед конструктором, причем с попутным увеличением объема кода?
Именно так, у нас специфичный проект. Вообще я зачастую акцентирую внимание на том, что не надо выдвигать догмы, мне нравится Марк Симан, но считаю его объективным недостатком — догматизм.

Вот с этого логично было начинать. Читатели данной статьи не знают ничего о вашем проекте, зато знают что модальные окна, агрегаторы событий и управление прогресс-барами в каждой модели — не слишком приятный «запах» в коде.
Марк Симан прекрасно умеет объяснять, почему он выдвигает те или иные требования. Его книга о внедрении зависимостей в .NET с одной стороны рассказывает о вещах, которые читатели и так знают, а с другой — формирует для готовых паттернов и хороших привычек прочный логический фундамент.
А вот зачем вам превращать три параметра конструктора в публичные свойства, помимо чисто вкусовых эстетических соображений, понять реально сложно.
За счёт чего? Куча лишних классов лучше одного, с помощью которого можно показывать прогресс бар?

Где вы нашли «кучу лишних классов»? У меня в обычных ViewModels просто нет никаких ссылок на специальный сервис для визуализации прогресса, только и всего.
Куча лишних классов лучше одного, с помощью которого можно показывать прогресс бар?

А зачем вам вообще классы? Можно использовать глобальные переменные. Ну, на худой конец, если язык не позволяет писать без классов вообще, то создать один и только один супергод-объект в программе, с помощью которого можно показывать и прогресс-бар, и модальные окна, и день рождения бабушки ПМа…
Ничего если я встряну со своими всего лишь 10 годами опыта разработки интерфейсов?
Инкапсуляция должна скрывать сложность реализации за простотой интерфейса. Это её основная и единственная функция. А не сокрытие данных или выпячивание зависимостей.
То есть нет ничего плохого в том, что интерфейс скрывает зависимости, которые имеют значения по умолчанию. Вы не обязаны думать об этих зависимостях при каждом создании объекта — только тогда, когда вам действительно нужно поменять их реализацию.
О зависимостях приходится думать, когда хочешь создать объект для использования вне «дефолтной» среды исполнения, будь то перенос в другой проект или юнит-тесты.
Нет, там приходится думать не о зависимостях, а именно о среде. Просто меняем дефолтные реализации для этой среды. Пример, как сохранить инкапсуляцию и при этом иметь возможность менять дефолты извне собственно и приведён в этом топике…
Обычно в качестве минуса ServiceLocator упоминают знание класса логики о инфраструктуре, что делает его жестко от нее зависимым.
Дело не только в знании (всегда можно создать дефолтные или mock-значения), но еще и в том, что в другом месте могут не требоваться даже 10% всех функции этого локатора, как часто бывает с большими библиотеками, а тащить приходится весь багаж.
А что мешает реализовать свой локатор с урезанным функционалом? :-)
Ничего не мешает. Вот у меня есть библиотека, для удобства есть SIngleton с методами создания каких-либо компонентов.
Варианты для использования моей библиотеки:
1. Использовать этот singleton сразу, как Service Locator. Например, Bootstrap.getNewNavBar(); И всегда создавать полноценную версию этого локатора со всеми методами.
2. Писать в каждом проекте новый Service Locator, который будет содержать только нужные методы создания компонентов…
3. Один раз создать DI-контейнер. Инъектировать через свойства класса ссылку на тип компонента. Создавать при инициализации класса автоматически.

Допустим у нас в ServiceLocator должен храниться сервис, который используется двумя классами. В какой момент нужно удалить этот экземпляр этого сервиса? Внутри 1 класса, внутри 2? Отслеживать все ссылки на эти классы? В конце программы?
Мокать глобальные объекты тоже то еще удовольствие, либо обнулять ВСЕ сервисы в локаторе перед каждым тестом, либо пересоздавать его целиком заново.
Насколько я понял вашу проблему — она решается посредством WeakRef, либо не решается, если язык это не поддерживает. При чём тут обсуждаемая тема — не очень понятно.
При проектировании сервис-локаторов часто рекомендуется делать так, чтобы они всегда возвращали хоть какую-нибудь реализацию сервиса — в том числе и пустую, которая ничего не делает.
Прочитал на вашей англоязычной страничке, что вы пишете начинку для терминала по продаже билетов на электрички.
Если данная статья относится к этому проекту, то можно узнать, зачем столько модальных окон?
Мне как пользователю подобных устройств реально необратимым представляется только одно решение: завершение покупки с печатью готового билета. Все остальное при необходимости успешно откатывается а значит ни в каких модальных диалогах не нуждается.
Вы как пользователь видите 4 окна всего. Их гораздо больше в режиме для кассиров и вот там есть куча необратимых операций.
Sign up to leave a comment.

Articles