Отличительная черта приложения состоит в том, что эта форма содержит очень много полей, очень много логики, очень много связей между полями. Грубо говоря, 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 ..});
}
}
Как несложно догадаться, спустя всего несколько лет эксплуатирования подобной схемы, бизнес-логика стала просто мешаниной событий, валидация стала ночным кошмаром, отладка событий в многопоточном окружении растягивается при любом расследовании логики работы приложения. Основное, что могу выделить
Для упрощения, я не стал расписывать другие аспекты формы, например
Я постарался расписать подход работы со сложными формами, который был реализован в работающем приложении. Я не могу сказать, что он совсем нерабочий - нет, он дает некоторое разделение кода, и до определенного момента работает.
Однако, текущая архитектура нуждается если не в полной переработке, то точно в серьезном рефакторинге в целях:
Как бы я видел идеальную архитектуру, она должна позволять следующее:
SetModelQuantity(...)
- то содержимое метода ясно расскажет, что происходит при этом измененииПоэтому вопросы звучат так:
Боюсь показаться банальным, но принципы SOLID пока еще никто не отменял:
Неоднократно приходится сталкиваться с подобными проблемами, поэтому выскажу свои мысли по этому вопросу:
Запутывание начинается с момента когда дочерняя VM начинает знать о родительской VM.
Бизнес-логика должна быть в модели. Если это правило не соблюдается, то она размазывается по вью-моделям, что приводит к двусторонней связи между дочерней и родительской VM.
public class MainViewModel
{
public Field1 Field1 { get; set;}
public ViewModel1 ViewModel1 { get; set;}
ModelModel mainModel;
}
public class ViewModel1
{
public Field3 Field3{get;set;}
public ViewModel1(MainViewModel mainVM)
{
}
}
в дочернюю VM необходимо передавать либо часть главной модели, либо всю модель сразу(как в вашем случае). Оттого и ViewModel и переводится как "представление модели" потому что она представляет модель, а не вью-модель не правда ли?)
public class ViewModel1
{
public Field3 Field3{get;set;}
public ViewModel1(MainModel model)
{
}
}
"двустороннюю" связь можно реализовать через события модели. Главная/дочерняя вью-модель подписываются на нужные события модели и реагируют на изменения.
public class ViewModel1
{
public Field3 Field3{get;set;}
public ViewModel1(MainModel model)
{
model.SomePropertyChanged += modelSomePropertyChanged;
}
void modelSomePropertyChanged(object sender, EventArgs e)
{
//что-то делаем...
}
}
Вероятно, логика модели должна быть отделена от логики вьюмоделей. При этом встаёт вопрос - а как будут общаться модели разных секций?
Разные секции получились в результате дробления главной модели и они должны быть между собой связаны? Вероятно лучше оставить одну большую модель, нежели раздробить ее на мелкие. Тот случай когда дробление больше усложняет чем упрощает.
Вариант 1. Бывает, что трудность реализации вызвана проблемой X/Y. Заказчик, имея проблему X, но не обладая техническими знаниями, предполагает, что эта проблема решается способом Y и просит сделать Y. Программист делает Y, но этот способ сложный, плохо ложится на парадигму реляционных данных, или объектов, или другую известную парадигму. Оказывается, что если попросить программиста решить задачу X, он предложит другое решение, гораздо более простое.
Типичный пример: медленные запросы к базе. Если спросить человека, который просит отчёт на 30 страниц, что он с ним будет делать, окажется, что он планирует просматривать его глазами в поисках каких-то важных данных. Но с этой работой гораздо лучше справляется компьютер, и для этого нужен другой запрос, гораздо более простой и производительный.
Поэтому первая рекомендация: ещё раз рассмотреть сценарии использования. Возможно, вместо одного сложного приложения лучше написать три простых для трёх разных ролей.
Вариант 2. Циклы в зависимостях возникают при попытке «формализовать неформализуемое». Предположим, есть поле A, от которого зависит поле B, а от того, в свою очередь — поле C. Построим направленный граф, и, если он ациклический, то формально мы можем построить модель и протестировать её. Но если вдруг поле A зависит от C, граф циклический, и у нас проблема. Уточню, что ациклический граф, это не всегда дерево, в нём ветки могут и сходиться, но они не образуют циклов.
Один из способов решения этой проблемы предложил Купер в своей книге «Психбольница в руках пациентов». Вы не пересчитываете C при изменении A, а проверяете попадание в диапазон. Если не попали, показываете это пользователю, но не запрещаете дальнейшее редактирование. Похожим образом действует проверка правописания: вы видите те слова, которые не известны программе, но сами решаете, ошибается программа, или нет. Если программа ошибается, вы просто игнорируете то, что подчёркнуто красной чертой. Но опечатку вы не пропустите.
Вариант 3. Подходящий инструмент. Бывает и так, что правила вполне можно формализовать, но сам язык не очень подходит для решения этой задачи. Часто нужно что-то вроде Пролога. Решением здесь являются кодогенерация и предметно-ориентированные языки (domain specific languages, DSL), которые можно использовать и в паре.
Например, вы можете описать бизнес-правила на языке собственной разработки, и при сборке проекта компилировать его в основной язык проекта. В современных ОО-языках наподобие C++, Java, C# хорошим решением является так называемая текучая нотация (fluent syntax), когда цепочка методов записывается через точку.
Скажем, в Entity Framework такой своеобразный DSL используется для описания отображений из БД в объекты:
public void Configure(EntityTypeBuilder<OperationData> builder)
{
builder.HasOne(x => x.Order)
.WithMany(x => x.Operations)
.HasForeignKey(x => x.OrderId)
.OnDelete(DeleteBehavior.Restrict);
}
Здесь с помощью цепочки методов строится модель, которая может быть даже формально проверена специально написанным методом.
Эта тема подробно освещена Фаулером сотоварищи в книге о предметно-ориентированных языках.
Виртуальный выделенный сервер (VDS) становится отличным выбором
Пытаюсь поменять цвет отображения точек в элементе управления glControl с помощью GLColor3, но цвет не меняется
Я оптимизирую игру и у меня генерируется дорога в FixedUpdate, что лучше использовать FixedUpdate или InvokeRepeat?
Делаю проект в котором отображается таблица и в последней колонке в зависимости от данных в таблице sql планировал или текст или кнопка, если...
Смотря на заголовки COM, очень часто замечаю что у большинства enum присутствует значение FORCE_DWORD = 0xffffffff