Какие подходы для работы со сложными формами работают?

1,00
р.
Исходные данные:
Имеется настольное приложение. Для простоты будем считать, что приложение содержит только одну форму. Функционал приложения: скачать по сети некоторую модель, натянуть эту модель на форму, пользователь эту модель редактирует, по окончании измененная модель едет дальше по сети Технологии: 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(ev => {.. logic ..}) } }
Проблемы
Как несложно догадаться, спустя всего несколько лет эксплуатирования подобной схемы, бизнес-логика стала просто мешаниной событий, валидация стала ночным кошмаром, отладка событий в многопоточном окружении растягивается при любом расследовании логики работы приложения. Основное, что могу выделить
Код больше не отражает бизнес-процессы. Установка значения для поля может обернуться 10 последовательными событиями. Или параллельными. race condition встречается очень часто тестируемость решения низкая. Чтобы написать тест логики на изменение поля, надо нагородить вьюмоделей и постоянно проверять что проехало по шине + какие изменения произошли в моделях. Так как всё взаимосвязано, то зачастую приходится поднимать все вьюмодели для теста одного изменения. Как итог, тесты либо отсутствуют, либо быстро устаревают. Количество дефектов растет. Расследование дефектов занимает много времени Внесение изменений занимает много времени
Для упрощения, я не стал расписывать другие аспекты формы, например
Баннеры, алерты, всплывающие сообщения асинхронная работа (поля активно работают с сетью) наборы секций и полей на экране сильно зависят от модели. То есть модели с разными данными, грубо говоря, обращаются в разные комбинации вьюмоделей и по разному представлены на форме Фоновые бизнес процессы могут в любой момент вмешаться в работу формы Какие-либо другие сюрпризы, хранящиеся в исходном коде, наподобии самописных AOP, пулах всего подряд и проч.
Вопрос
Я постарался расписать подход работы со сложными формами, который был реализован в работающем приложении. Я не могу сказать, что он совсем нерабочий - нет, он дает некоторое разделение кода, и до определенного момента работает.
Однако, текущая архитектура нуждается если не в полной переработке, то точно в серьезном рефакторинге в целях:
повышения качества продукта, его тестируемости уменьшения количества дефектов уменьшения времени добавления новых фич
Как бы я видел идеальную архитектуру, она должна позволять следующее:
код полностью отражает бизнес процессы. Если в коде есть метод SetModelQuantity(...) - то содержимое метода ясно расскажет, что происходит при этом изменении Код легко тестировать. Должна быть возможность для юнит тестирования любой новой функциональности Вероятно, логика модели должна быть отделена от логики вьюмоделей. При этом встаёт вопрос - а как будут общаться модели разных секций?
Поэтому вопросы звучат так:
какой подход к организации кода/архитектуры вы бы могли посоветовать? Какие примеры архитектур, решающие схожую задачу существуют? Какие этапы рефакторинга вы бы могли посоветовать? И как при этом не упасть в качестве, учитывая примерно 50% покрытие тестами (остальные 50% как то работают, но никто не знает как)? Любые ссылки или литература приветствуются

Ответ
Боюсь показаться банальным, но принципы SOLID пока еще никто не отменял:
Классы вью моделей не должны зависеть от других вью моделей напрямую, только от абстракций (интерфейсов). Уменьшение прямых зависимостей заметно облегчит юнит тестирование. Ответственность вью модели - контролировать состояние вьюхи на основе модели и выступать между ними посредником. Не пытайтесь уместить всю бизнес логику во вью модель, этим должны заниматься сервисы. Шина (медиатор) во многом является анти-паттерном, так как делает зависимости между компонентами неочевидными. Иногда это бывает полезно, но, скорее всего, не в вашем случае. Советую рассмотреть вариант коммуникации через модель/сервисы (когда вью модели подписываются на изменение состояния модели). Шине можно оставить нейтральные нотификации, от которых не зависит поведение конкретной вью модели (например "значения свойства изменилось", но при этом вью модели безразлично кто и как среагирует на эту нотификацию). Избежать проблем с многопоточностью (в частости race condition) можно с помощью очереди background операций, с возможностью их отмены в любой момент (т.е. некое подобие Dispatcher-а).