Написано: 10.12.2022

Model-View-Controller (MVC)

Предисловие

Model-View-Controller – представляет собой технологию, которая применяется при организации пользовательского интерфейса.

Договор страхования ОПО (опасного производственного объекта) – достаточно сложный, со множеством зависимостей.

Например:

  • от даты заключения договора – зависят тарифы для расчета страховой премии,
  • от даты регистрации опасного объекта – зависит классификатор объектов и выбор объекта страхования
  • от выбора объекта страхования – зависит страховая сумма, признаки опасности и т.д.

То есть, изменение одного параметра договора может привести к изменению множества других параметров договора (которые, в свою очередь, влияют на изменение параметров, которые заввисят от них) и т.д.

При использовании технологии MVC присходит разделение данных и интерфейсных представлений. Благодаря этому реализация упрощается.

Договор страхования ОПО

Договор страхования ОПО выглядит, как окно, содержащее неколько вкладок-фреймов (“Расчет”, “Печать”, “Описание расчета” и т.д.).

На разных вкладках (фреймах) расположены интерфейсные элементы для задания параметров одного договора ОСОПО.

Фреймы модуля ОСОПО наследуются от общего предка, класса TAPOFrame.

Диаграмма классов для фреймов модуля ОСОПО

Класс TAPOFrame

Класс предоставляет базовую функциональность, которая используется в фреймах-потомках.

А именно:

  • Обработку ошибок (и вывод сообщений об ошибках в договоре)
  • Организацию оповещения интерфейсных элементов фрейма о связанных изменениях (часть технологии MVC)

Далее будут перечислены части технологии MVC, реализованные в модуле ОСОПО.

Модель.

Моделью является класс OPO, содержащий полное описание договора ОСОПО (полный набор реквизитов договора и их состояний видимости и редактируемости).

Контроллер.

Контроллером является функция TAPOFrame.FrameController().

Представление.

Представлением является интерфейсный элемент на фрейме.

Распределение ролей.

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

Представление несёт ответственность за отображение того или иного элемента. Представление ничего не знает о бизнес-логике договора. Задача представления – просто отобразить соответствующий элемент на основании значения, которое представление берёт у модели.

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

Наблюдатель.

У класса TAPOFrame есть элемент, который называется наблюдателем, – FObserver (тип TOS_RealObserver). У наблюдателя можно зарегистрировать процедуру, которую наблюдатель вызывает в случае события (завершения изменения в модели).

Таким образом, после того, как модель изменит своё состояние (вместе со всеми зависимостями в модели), наблюдатель вызовет процедуры, которые зарегистрированы.

В качестве процедуры, которую вызовет наблюдатель, указывается процедура TAPOFrame.FrameController().

Процедура представляет собой контроллера.

Цель контроллера состоит в том, чтобы извлечь команды из контейнера наблюдателя и вызвать обработчики представления, которые связаны с этими командами.

TAPOFrame::FrameController()

Фактически, контроллер поочерёдно извлекает команды из контейнера и для каждой совершает вызов RunCommand()

/*------------------------------------------------------------------------------
    Контролер.
    Подписывается на изменения в модели.
------------------------------------------------------------------------------*/
void __fastcall TAPOFrame::FrameController(TObject *Sender)
{
    int cmd;

    bool tmp_Loaded = isLoaded;
    bool RunCalcHeight = false;

    isLoaded = false;   // на время работы контроллера не трогаем модель.

    if(FPrepared == true) {                                                     // если форма готова обрабатывать сообщения,
        while((cmd = FObserver.GetCommand()) != -1) {                           // извлекаем команды из контейнера,
            if(cmd == osopo_cmd::CALC_HEIGHT) {                                 // придерживаем команды, которые нужно выполнить позднее остальных
                RunCalcHeight = true;                                           // так действовать нехорошо, но вводить приоритеты пока муторно.
            } else {
                RunCommand(cmd);
            }
        }
        if(RunCalcHeight == true) {
            RunCommand(osopo_cmd::CALC_HEIGHT);
        }
    }

    isLoaded = tmp_Loaded;
}

TAPOFrame::RunCommand()

Процедура TAPOFrame::RunCommand() просматривает карту обработчиков команд и для полученной команды осуществляет вызов её обработчика.

void __fastcall TAPOFrame::RunCommand(int cmd)
{
    TOS_UpdateViewMap::iterator i;
    TOS_UpdateViewProc p;

    i = FUpdateViewsMap.find(cmd);              // находим для неё обработчик,
    if (i != FUpdateViewsMap.end()) {           // если обработчик валидный,
        p = (*i).second;
        (p)();                                  // делаем вызов.
    }
}

Карта обработчиков.

У класса TAPOFrame есть элемент, который называется картой обработчиков, – FUpdateViewsMap (тип TOS_UpdateViewMap).

Фактически, это карта для числа (которое представляет собой команду из контейнера наблюдателя) и процедуры (которая связана с этой командой).

Процедура обработчика – без параметров и ничего не возвращает.

Карта обработчиков сторится с помощью виртуальной функции MakeUpdateViewsMap()

Вызов виртуальной функции выполняется в конструкторе базового класса TAPOFrame

class TAPOFrame : public TFrame
{
    // пропущено
 protected:
    // пропущено

	// Процедура формирования карты для изменения представлений
	typedef void __fastcall (__closure *TOS_UpdateViewProc)();
	typedef std::map<int, TOS_UpdateViewProc> TOS_UpdateViewMap;
	TOS_UpdateViewMap FUpdateViewsMap;
	virtual void __fastcall MakeUpdateViewsMap() {}

    // пропущено
};

Пример формирования карты обработчиков.

Карта обработчиков – своя для каждого фрейма (потому что в каждом фрейме свой набор интерфейсных элементов и команд для обновления).

Однако, разные фреймы могут реагировать на одну и ту же команду (событие). Потому что в модели может измениться поле, отображаемое в разных фреймах. Например, сумма доплаты или возврата по договору, в случае если она возникла, показывается на вкладке “Расчет” и на вкладке “Печать”.

Ниже, в качестве самого простого, приводится пример формирования карты обработчиков для фрейма TCalcDescription (вкладка “Описание расчёта”).

Для других фреймов код похожий, только событий в карте больше.

/*------------------------------------------------------------------------------
	Процедура формирования карты для изменения представлений.
------------------------------------------------------------------------------*/
void __fastcall TCalcDescription::MakeUpdateViewsMap()
{
	struct InitUpdateViews {int Cmd; TOS_UpdateViewProc Proc; };
	int cmd;

	InitUpdateViews initer[] = {
		{osopo_cmd::SHOW_APO_RESULT             , &UV_ShowApoResult}            // отображение результатов расчета АПО
		, {osopo_cmd::FRAMES_ENABLED            , &UV_FramesEnabled}            // разрешено редактирование формы?
		, {-1}
	};

	for(int i = 0; initer[i].Cmd != -1; i++) {
		cmd = initer[i].Cmd;
		FUpdateViewsMap[cmd] = initer[i].Proc;
	}
}

События обработчиков.

Команды, для которых строится карта обработчиков, определены в файле TOS_Enums.h

У этих команд (событий) отдельный namespace – osopo_cmd.

// команды изменения
namespace osopo_cmd {

typedef enum {
	STATUS                                                  = 1
	, CALC_ID                                               = 2
	, CALL_STR_SUMM                                         = 3                 // выбор страховой суммы
	, CALL_RECALC                                           = 4                 // вызов пересчета
    // пропущено

	// редактируемость
	, PAY_DOC_SER_ENABLED									= 600 		 		// Серия платежного документа
	, START_DATE_ENABLED                                    = 601               // изменение разрешенности даты начала действия
    // пропущено

	// видимость
	, PAY_DOC_SER_VISIBLED									= 700				// Серия платежного документа
	, PAY_DOC_NUM_VISIBLED									= 701				// Номер платежного документа
    // пропущено

	// должны исполняться позднее других команд
	, CALC_HEIGHT                                           = 10000             // изменить координаты панели
}
TOS_ChangeModelCommands;

};

События, которые обрабатываются в нескольких фреймах.

Пример такого события – osopo_cmd::ADD_RET_SUM (сумма доплаты или возврата).

Модель генерирует это событие в случае, если появляется доплата или возврат (при расчёте ДС).

Сумма доплаты (возврата) отображается как на фрейме TCalc (расчёт), так и на фрейме TPrint (печать).

При выполнении изменений, модель ничего не знает о том, какими фреймами будет обрабатываться то или иное событие (и будет ли обрабатываться вообще).

Для того, чтобы событие было обработано фреймом, нужно добавить обработчик события в карту обработчиков.

Ниже показан пример события osopo_cmd::ADD_RET_SUM в карте обработчиков для фрейма TCalc (расчёт)

/*------------------------------------------------------------------------------
    Процедура формирования карты для изменения представлений.
------------------------------------------------------------------------------*/
void __fastcall TCalc::MakeUpdateViewsMap()
{
    struct InitUpdateViews {int Cmd; TOS_UpdateViewProc Proc; };
    int cmd;

    InitUpdateViews initer[] = {
        {osopo_cmd::NSSO_PUBLISH                    , &UV_NssoPublish}
        // пропущено
		, {osopo_cmd::ADD_RET_SUM          			, &UV_AddRetSum}       		// Сумма "Доплата/возврат"
        // пропущено
		, {-1}
	};

	for(int i = 0; initer[i].Cmd != -1; i++) {
		cmd = initer[i].Cmd;
		FUpdateViewsMap[cmd] = initer[i].Proc;
	}
}

Пример обработчика события.

Ниже показан пример обработчика для события osopo_cmd::ADD_RET_SUM для фрейма TCalc (расчёт). Событие обсуждалось чуть выше.

Событие генерируется моделью (классом opo) и только в том случае, когда появляется сумма доплаты (или возврата).

После завершения изменения модели, вызывается Контроллер фрейма, который извлекает события из контейнера Наблюдателя, и вызывает обработчики событий (если они есть для данного события в карте обработчиков фрейма).

Обязанность обработчика события – извлечь данные из модели и изменить нужное представление.

В обработчике, показанном ниже, данные модели – это поле obj->AgrSum, а представление – это метка lDoplataVozvrat

void __fastcall TCalc::UV_AddRetSum() {                                   		// Сумма "Доплата/возврат"
	lDoplataVozvrat->Caption = FormatFloat(_fmt2, obj->AgrSum );
}

Как производятся изменения в модели.

При установке нового значения для какого-либо из свойств модели производится вызов шаблонной функции opo::Set(), код которой показан ниже.

Функция opo::Set() работает совместно с охранником (объектом Guard) и оповещателем (объектом Talker). Подробнее о них будет рассказано.

Функция opo::Set() принимает следующие параметры:

  • указатель на поле, которое нужно изменить (ptr)
  • значение, которое нужно установить для данного поля (value)
  • команду (событие), которая соответствует изменению для данного поля (cmd)
  • процедуру изменения зависимостей для данного поля (p).

В начале процедуры изменения, функция opo::Set() опрашивает охранника, можно ли производить изменения? Если охранник разрешает, установка изменений производится. Перед тем, как произвести изменения, производится установка охранника для полученного события (cmd). После совершения изменений, охранение снимается. Установка охранника защищает от циклических вызовов функции изменения (в случае, когда при вызове процедуры изменения зависимостей, состоится повторный вызов процедуры, которая уже была вызвана прежде: в этом случае установленный охранник предотвратит зацикливание изменений).

После того, как охранник установлен, изменения начинаются с вызова BeginUpdate() и завершаются вызовом EndUpdate().

/*------------------------------------------------------------------------------
    Процедура валидной установки значения переменной модели.
    Во избежание возможного зацикливания процесса, работа просходит
    при установленном охраннике (Guard).
    По завершении изменений будет производится оповещение наблюдателей.
    Внимание! ID охранника совпадает с командой.
------------------------------------------------------------------------------*/
template <class T> void __fastcall opo::Set(T* ptr, T value, int cmd, Func p)
{
    if(Guard->Get(cmd) == false) {                                              // охранника нет? можно приступать к работе.
        if(*ptr != value) {                                                     // полученное значение отличается от значения модели?
            if(FLoading == true) {                                              // в режиме загрузки
                *ptr = value;                                                   //      только устанавливаем значение, больше ничего не делаем.
            } else {
                Guard->Set(cmd);                                                // Устанавливаем охранника.
                BeginUpdate();                                                  // Начинаем изменения.
                *ptr = value;                                                   // меняем значение.
                CallUpdate(cmd);                                                // кидаем команду.
                CallUpdate(osopo_cmd::VALIDATE);                                // и команду о проверке.
                if(p != NULL) { (p)(); }                                        // вызываем процедуру изменения зависимостей.
                EndUpdate();                                                    // Изменения заканчиваются (наблюдатели оповещаются).
                Guard->Clear(cmd);                                              // Охранение снимается.
            }
        }
    }
}

BeginUpdate() и EndUpdate()

С вызовами BeginUpdate() и EndUpdate() связаны события оповещателя (объекта Talker).

Вызовы BeginUpdate() и EndUpdate() могут быть вложенными и могут вызываться в процедурах изменения зависимостей. Функция BeginUpdate() обозначает начало процесса изменения. Функция EndUpdate() обозначает окончание процесса изменения. При вложенных изменениях, EndUpdate() не выполняет чего-то полезного, но при завершении начального изменения EndUpdate() производит вызов наблюдателя, сигнализируя, тем самым, что процесс изменения модели завершился (и можно вызывать контроллер, с последующим обновлением нужных представлений).

void __fastcall opo::BeginUpdate() {Talker->BeginUpdate();}
void __fastcall opo::EndUpdate() {Talker->EndUpdate();}

Оповещатель

Оповещатель (Talker) является глобальным объектом. Определение содержится в файле UOS_Abstarct.h

Оповещатель – это абстрактный класс, предоставляющий для наблюдателя функции подписаться на наблюдение (RegObserver) и отписаться от подписки (UnRegObserver).

Также оповещатель предоставляет функции начала (BeginUpdate) и завершения изменений (EndUpdate) и направления команды в контейнер (Push).

При завершении начального EndUpdate(), оповещатель производит вызов соответствующих методов наблюдателя.

/*------------------------------------------------------------------------------
    Болтун. Уведомитель об изменениях.
    Предоставляет возможности подписываться к слежению за изменениями
    (тот, кто подписался, будет получать уведомления),
    прекращать подписку.
    А также можно отмечать блоки начала и окончания изменений,
    между которыми направлять события в контейнер.
    По окончании изменений события будут направлены тем,
    кто на них подписался.
------------------------------------------------------------------------------*/
class TOS_Talker : public TOS_Product {
public:
    __fastcall TOS_Talker() {}
    virtual __fastcall ~TOS_Talker() {}
    virtual void __fastcall RegObserver(TOS_Observer *o) = 0;	                // Подписка наблюдателя
    virtual void __fastcall UnRegObserver(TOS_Observer *o) = 0;                 // Прекращение наблюдения

    virtual bool __fastcall Used() = 0;              			                // Используется?
    virtual void __fastcall BeginUpdate() = 0;              			        // начало изменений
    virtual void __fastcall Push(int cmd) = 0;     						        // направить команду в контейнер
    virtual void __fastcall EndUpdate() = 0;                			        // завершение изменений
};

Наблюдатель

Наблюдатель (TOS_Observer) – абстрактный класс со свойствами события (Event), которое подписывается для отслеживания изменений в модели, и с функциями работы с очередью команд SetCommand() (помещения команды в очередь) и GetCommand() (извлечения команды из очереди).

/*------------------------------------------------------------------------------
    Интерфейс наблюдателя.
	Имеет свойства:
	1) событие (которое может подписываться для слежения за изменениями в модели).
	2) очередь команд
------------------------------------------------------------------------------*/
class TOS_Observer {
protected:
	virtual int __fastcall GetFCount() = 0;
	virtual TNotifyEvent __fastcall GetFEvent() = 0;
	virtual void __fastcall SetFEvent(TNotifyEvent value) = 0;
public:
    __fastcall TOS_Observer() {}
    virtual __fastcall ~TOS_Observer() {}
    virtual int __fastcall GetCommand() = 0;                				    // извлекает команду из очереди, если -1 -- очередь пустая
    virtual void __fastcall SetCommand(int cmd) = 0;           				    // помещает команду в очередь
  	__property TNotifyEvent Event = {read = GetFEvent, write = SetFEvent};
  	__property int Count = {read = GetFCount};								    // Count - кол-во команд в очереди.
};