Улучшаем MVC

Материал из Викиверситета

Пойдем по порядку, начиная с самого главного. Ответьте себе на вопрос: "Какова основная цель этой программы?". Я отвечу так: "Разработать платежную систему для банка". Красивый интерфейс и контроль пользовательского ввода по сути являются дополнительными возможностями модели! В частности, это означает две вещи:

  1. Когда нам потребуется визуализировать форму, мы должны вызвать model.Show(). Внутри этого метода уже будет создан объект представления.
  2. Когда нам потребуется осуществить платеж, мы должны вызвать 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 (в предыдущей статье), мы добились следующих достоинств:

  1. Модель создается первой (нет избыточных методов).
  2. Модель себе создает представление и контроллер по необходимости.
  3. Представление не знает ни о модели, ни о контроллере — полная независимость!
  4. Синхронизацией данных и связыванием действий занимаются Binding и Observer.