Улучшаем MVC
Пойдем по порядку, начиная с самого главного. Ответьте себе на вопрос: "Какова основная цель этой программы?". Я отвечу так: "Разработать платежную систему для банка". Красивый интерфейс и контроль пользовательского ввода по сути являются дополнительными возможностями модели! В частности, это означает две вещи:
- Когда нам потребуется визуализировать форму, мы должны вызвать model.Show(). Внутри этого метода уже будет создан объект представления.
- Когда нам потребуется осуществить платеж, мы должны вызвать model.MakePayment(). Внутри этого метода уже будет создан объект контроллера.
Плюсы такого подхода очевидны: работа идет напрямую, а не через избыточные методы. Однако, появляется цепочка новых вопросов:
- Кто должен вызывать model.MakePayment() ?
- Если представление, то они с моделью должны хранить перекрестные ссылки друг на друга?
Очевидно, что если мы собрались улучшать MVC, то такой способ нам не подойдет. На секунду оставим этот вопрос и зайдем с другой стороны. Что значит "представление зависит от модели"? Это означает, что представление неразрывно привязано к модели, и подменить модель никак не получится. Можно попробовать унифицировать существующие модели, введя интерфейс модели, однако в итоге получится накопление избыточности.
Необходимо полностью разорвать связь между представлением и моделью!
При работе с данными с этим отлично справляется Binding в WPF, для методов подходит паттерн Observer. Попробуем разобраться, как они функционируют!
Binding и Observer в домашних условиях
правитьОпределимся, что синхронизацией данных никто из участников MVC заниматься не должен — это обязанность отдельного объекта (назовем его Binding). Он будет хранить ссылки на все предствления и модели. Кроме этого, в декларативном виде прописываются связи вида "ПолеПредставленияA = СвойствоМоделиB". При каждом изменении значения как в модели, так и в представлении генерируется событие PropertyChanged. Это нужно не только для синхронизации, но и для вызова логики по смене значения — так что это есть в любом случае. При инициализации Binding подписывается на все события PropertyChanged. При возникновении события находит нужную связь (заданную в декларативном виде), используя Reflection восстанавливает свойство и выполняет присваивание. Я думаю, что паттерн Observer можно реализовать аналогичным способом: "КнопкаПредставленияА = МетодМоделиВ".
Перейдем к практике
правитьВернемся к задаче о банковских платежах с двумя моделями. При старте приложения выбираем вид платежа ( инициализируем model) и показываем форму приложения ( вызываем model.Show() ). Посмотрим на остальной код архитектуры.
namespace ModelFirst
{
class IncomingPayment
{
private readonly IncomingController controller = new IncomingController();
// Business-logic
public decimal CalculateFees() { /*. . .*/ }
public bool IsCurrencyAllowed() { /*. . .*/ }
public void Show()
{
new View();
}
public void MakePayment(decimal money)
{
money -= CalculateFees();
if(controller.PaymentIsPossible())
// Other logic
}
}
class OutgoingPayment
{
private readonly OutgoingController controller = new OutgoingController();
// Business-logic
public bool IsMoneyEnough(decimal money) { /*. . .*/ }
public decimal CalculateFees() { /*. . .*/ }
public bool NeedReportToNB() { /*. . .*/ }
public void Show()
{
new View();
}
public void MakePayment(decimal money)
{
money -= CalculateFees();
if(controller.PaymentIsPossible(money))
// Other logic
}
}
class IncomingController
{
private IncomingPayment model;
public bool PaymentIsPossible()
{
return model.IsCurrencyAllowed();
}
}
class OutgoingController
{
private OutgoingPayment model;
public bool PaymentIsPossible(decimal money)
{
return model.IsMoneyEnough(money);
}
}
class View
{
public void PayButtonClick()
{
MessageBox("Thank you for using our bank!");
Notify(new NotifyEventArgs("PayButtonClick", Money.Text));
}
}
}
Клик по кнопке "Осуществить платеж" сгенерирует событие для Observer. Последний выполнит поиск метода, необходимого вызвать для PayButtonClick, найдет и вызовет MakePayment с параметром Money.Text (код не стал приводить, он только запутает и отвлечет от главной идеи). Модель запросит контроллер проверить возможность операции, он это выполнит, причем используя только методы модели.
Что получилось?
правитьВ сравнении с полученными результатами применения оригинального MVC (в предыдущей статье), мы добились следующих достоинств:
- Модель создается первой (нет избыточных методов).
- Модель себе создает представление и контроллер по необходимости.
- Представление не знает ни о модели, ни о контроллере — полная независимость!
- Синхронизацией данных и связыванием действий занимаются Binding и Observer.