26

Исходные данные:

  • Имеется настольное приложение. Для простоты будем считать, что приложение содержит только одну форму.
  • Функционал приложения: скачать по сети некоторую модель, натянуть эту модель на форму, пользователь эту модель редактирует, по окончании измененная модель едет дальше по сети
  • Технологии: WPF, MVVM

Особенности приложения:

Отличительная черта приложения состоит в том, что эта форма содержит очень много полей, очень много логики, очень много связей между полями. Грубо говоря, 90% кода - это логика и обслуживание полей на форме.

Архитектура:

Как я уже упомянул, используется MVVM паттерн как основа архитектуры. Модель, грубо говоря, здесь выражена как чисто POCO объект - сущность, которая предназначена для сериализации/десериализации при общении с сервером + её можно собрать из классов ViewModel

История:

Изначально всё было просто - одна модель, одна вьюмодель, одна вьюха

введите сюда описание изображения

Однако, требования всё отгружали и отгружали, когда количество полей перевалило за 50, пришлось делить главную вьюмодель, модель и представление на части, но это всё по прежнему собиралось в одну форму. Главная вьюмодель осталась, но, чтобы уменьшить сложность, из неё были вынесены все неосновные поля, эти поля были сгруппированы в более мелкие вьюмодели и код был организован так, что мелкие вьюмодели знали о главной и главная знала о мелких.

введите сюда описание изображения

Форма стала выглядеть как то так:

введите сюда описание изображения

Код выглядел как то так:

public class MainViewModel 
{
    public Field1 Field1{get;set;}
    public Field2 Field2{get;set;}

    public ViewModel1 ViewModel1{get;set;}  
    public ViewModel2 ViewModel2{get;set;}  
    public ViewModel3 ViewModel3{get;set;}  
}


public class ViewModel1
{
    public Field3 Field3{get;set;}

    public ViewModel1(MainViewModel main)
    {       
    }
}

public class ViewModel2
{
    public Field4 Field4{get;set;}

    public ViewModel2(MainViewModel main)
    {
    }
}

public class ViewModel3
{
    public Field5 Field5{get;set;}

    public ViewModel3(MainViewModel main)
    {
    }
}

Но дальше - больше. Если количество полей растет линейно, то количество логических взаимосвязей между полями - растет гораздо быстрей. Очень быстро появились кросс-секционные поля, которые нельзя строго отнести к какой то секции, так как они состоят в логической зависимости от полей из нескольких секций сразу. Конечно, верным решением было бы вынести вообще взаимодействие полей в модель, но этого не было сделано, а вместо этого логика продолжала существовать во вьюмоделях. Таким образом, потребовался механизм взаимодействия полей между вьюмоделями, который был решен с помощью локальной шины. Всё стало ещё интересней

public class ViewModel1
{
    public Field3 Field3
    {
        get => _field3 
        set 
        {
            var oldValue = _field3; 
            if (SetProperty(ref _field3, value))
            LocalBus.Raise(new Field3Cahnged(oldValue, value))
        }   
    }

    public ViewModel1(MainViewModel main)
    {       
    }
}

public class ViewModel2
{
    public Field4 Field4{get;set;}

    public ViewModel2(MainViewModel main)
    {
        LocalBus.Subscribe<Field3Cahnged>(ev => {.. logic ..});
    }
}

Проблемы

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

  • Код больше не отражает бизнес-процессы. Установка значения для поля может обернуться 10 последовательными событиями. Или параллельными.
  • race condition встречается очень часто
  • тестируемость решения низкая. Чтобы написать тест логики на изменение поля, надо нагородить вьюмоделей и постоянно проверять что проехало по шине + какие изменения произошли в моделях. Так как всё взаимосвязано, то зачастую приходится поднимать все вьюмодели для теста одного изменения. Как итог, тесты либо отсутствуют, либо быстро устаревают. Количество дефектов растет.
  • Расследование дефектов занимает много времени
  • Внесение изменений занимает много времени

Для упрощения, я не стал расписывать другие аспекты формы, например

  • Баннеры, алерты, всплывающие сообщения
  • асинхронная работа (поля активно работают с сетью)
  • наборы секций и полей на экране сильно зависят от модели. То есть модели с разными данными, грубо говоря, обращаются в разные комбинации вьюмоделей и по разному представлены на форме
  • Фоновые бизнес процессы могут в любой момент вмешаться в работу формы
  • Какие-либо другие сюрпризы, хранящиеся в исходном коде, наподобии самописных AOP, пулах всего подряд и проч.

Вопрос

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

Однако, текущая архитектура нуждается если не в полной переработке, то точно в серьезном рефакторинге в целях:

  • повышения качества продукта, его тестируемости
  • уменьшения количества дефектов
  • уменьшения времени добавления новых фич

Как бы я видел идеальную архитектуру, она должна позволять следующее:

  • код полностью отражает бизнес процессы. Если в коде есть метод SetModelQuantity(...) - то содержимое метода ясно расскажет, что происходит при этом изменении
  • Код легко тестировать. Должна быть возможность для юнит тестирования любой новой функциональности
  • Вероятно, логика модели должна быть отделена от логики вьюмоделей. При этом встаёт вопрос - а как будут общаться модели разных секций?

Поэтому вопросы звучат так:

  • какой подход к организации кода/архитектуры вы бы могли посоветовать?
  • Какие примеры архитектур, решающие схожую задачу существуют?
  • Какие этапы рефакторинга вы бы могли посоветовать? И как при этом не упасть в качестве, учитывая примерно 50% покрытие тестами (остальные 50% как то работают, но никто не знает как)?
  • Любые ссылки или литература приветствуются
tym32167
  • 32,857
  • 2
    Хороший вопрос. Как-то мне приходилось писать формы по 70 - 90 полей с логикой (оттуда я вынес, что форма должна быть на 5-7 полей, не больше - иначе что-то бизнес перемудрил); поэтому с удовольствием почитаю советы. Меня вот интересуют подробности по форме: приходилось ли лезть в базу за данными для логики или достаточно было оперировать тем, что вводится? – A K Apr 12 '19 at 05:16
  • @AK согласен с картинкой, но это реалии, на которые я не могу повлиять. По поводу формы - тут есть всё, кроме прямого доступа в БД. Поля могут отправлять запросы по сети и реагировать на ответы, некоторые ответы влияют на наборы полей, которые в свою очередь тоже могут отправлять запросы по сети и т.д. Есть подсказки, баннеры, попапы, модальные окна, которые оперируют с сетью. Это одна из причин почему тестирование стало болью - для теста надо поднимать чуть ли не все вьюмодели и мокать очень большое количество сервисов. – tym32167 Apr 12 '19 at 07:39
  • Это в главной форме такая беда? – Vasek Apr 12 '19 at 08:59
  • @Vasek для упрощения считаем что в приложении только одна форма, и в ней такая беда – tym32167 Apr 12 '19 at 09:01
  • Как я понимаю, вопрос по сути сводится к тому как архитектурно работать с взаимодействием между многими модулями (конкретно: "есть что-то лучше, чем локальная шина для этого") но не абстрактно "по Фаулеру и ООПшным заветам" - а в приложении к конкретной области "формы MVVM". Я так вижу, что тут не просто дело в знании паттернов, тут нужны коллеги с подобным багажом в формах, которые уже пробовали все эти приёмы применять и набили шишек, знают какими тропами лучше ходить. – A K Apr 12 '19 at 10:28
  • @AK да, я прошу поделиться конкретным опытом или хотя бы направить куда копать с пониманием профита от копания куда то. Знаний паттернов, ООП и заветов Фаулера у меня самого с избытком :) – tym32167 Apr 12 '19 at 10:30
  • Если взглянуть на схему из этого видео, то вам, похоже, нужен слой Application, туда нужно начать перемещать логику "...логических взаимосвязей между полями..." и соответственно легче будет писать тесты. – Bulson Apr 12 '19 at 20:16
  • @Bulson Я то видео не просто смотрел до дыр, но и применял некоторые идеи в проектах. Оно хорошее, но как мне кажется к этому вопросу оно относится мало, потому что: а) mvc не mvvm б) вопрос больше не об общих принципах и ООПшных заветах, а о практике применения. Ну то есть мало сказать "куда" перемещать, надо понимать "как". – A K Apr 12 '19 at 20:58
  • @AK на мой взгляд эта "луковая" архитектура прекрасно будет работать и в случае MVVM - это же слой Presentation, а для того, чтоб упростить вьюмодели, нужно перенести часть логики куда? Вот как раз создать слой Application и вынести некоторые части туда. – Bulson Apr 12 '19 at 21:07
  • @tym32167 Не очень хочу постить ответом, поэтому скажу так. Мне кажется, что текущий ваш подход при всех его недостатках - это лучшее, что только можно предложить. Или более мягко: на текущий момент в индустрии де-факто ваш подход является господствующим. Возьмите например vue.js: в нём для построения обмена данными между компонентами используется общая шина событий (Event Bus) на базе vuex. У вас кстати нет опыта работы с redux/vuex? Возможно, какие-то идеи для проекта можно было бы заимствовать из этих фреймворков. – A K Apr 17 '19 at 07:13
  • @Bulson спасибо за видео. Я пока немного приболел, как голова прояснится, я обязательно погляжу. – tym32167 Apr 17 '19 at 11:18
  • @AK спасибо. Event Aggregator - это известный паттерн, потому его суют везде, где только можно. Я то не против паттерна, но вот сейчас хапнул проблем со слишком его повсеместным использованием, так как он не только разделил какие то части моего приложения, но и раздробил каждый бизнес-сценарий на много кусочков, и теперь не понятно, что из себя бизнес сценарии вообще представляет, код стал мешаниной – tym32167 Apr 17 '19 at 11:22
  • @AK в отличие от нашего подхода, тот же redux сам Ден Абрамов рекомендует использовать только если надо шарить какое то состояние с другими компонентами, а если речь о форме идет, то вроде он топил за то, чтобы данные формы оставались на форме. Если говорить про какой нить redux-form, но там хранение состояния в глобальном хранилище как раз является минусом решения. Я это знаю, так как описываемый здесь код пытался портировать на react+redux, но мне пока не дали карт бланш – tym32167 Apr 17 '19 at 11:27
  • 1
    @tym32167 желаю скорейшего выздоровления :) – Bulson Apr 17 '19 at 12:59
  • @Bulson спасибо! – tym32167 Apr 17 '19 at 13:09

3 Answers3

4

Боюсь показаться банальным, но принципы SOLID пока еще никто не отменял:

  1. Классы вью моделей не должны зависеть от других вью моделей напрямую, только от абстракций (интерфейсов). Уменьшение прямых зависимостей заметно облегчит юнит тестирование.
  2. Ответственность вью модели - контролировать состояние вьюхи на основе модели и выступать между ними посредником. Не пытайтесь уместить всю бизнес логику во вью модель, этим должны заниматься сервисы.
  3. Шина (медиатор) во многом является анти-паттерном, так как делает зависимости между компонентами неочевидными. Иногда это бывает полезно, но, скорее всего, не в вашем случае. Советую рассмотреть вариант коммуникации через модель/сервисы (когда вью модели подписываются на изменение состояния модели). Шине можно оставить нейтральные нотификации, от которых не зависит поведение конкретной вью модели (например "значения свойства изменилось", но при этом вью модели безразлично кто и как среагирует на эту нотификацию).
  4. Избежать проблем с многопоточностью (в частости race condition) можно с помощью очереди background операций, с возможностью их отмены в любой момент (т.е. некое подобие Dispatcher-а).
Kromster
  • 13,809
Raider
  • 875
  • Каким именно образом добавление 600 интерфейсов мне облегчит тестирование? Каждая мини вьюмодель у меня знает только о родительской вьюмодели, таким образом прямая зависимость уже минимальна.
  • – tym32167 Apr 12 '19 at 15:10
  • Можете показать масштабируемый пример? Как сервисы будут общаться друг с другом? Кто их будет вызывать? Как сервисы будут взаимодействовать с вьюмоделью и моделью? Как при этом не получить мешанину сервисов? (учитывайте, что вьюмодель, как и модель, разбита на множество классов)
  • – tym32167 Apr 12 '19 at 15:11
  • Сейчас у меня есть шина и примерно 200 (может и больше) разных классов - событий. Вы предлагаете выпилить 200 классов и добавить 200 событий в модель? Я не очень понимаю, как это поможет?
  • – tym32167 Apr 12 '19 at 15:11