MCV в целом и реализация контроллера в частности

MCV в целом и реализация контроллера в частности

Логическое продолжение всех тем, посвященных парадигме модель-контроллер-представление. Я заметил, что очень многие участники форума пишут и поддерживают собственные системы, в основе которых лежит MCV model 2. Большинство знакомы с такими решениями, как Tapestry, Struts, php-mcv (php), OpenInteract (perl), Maypole (perl), поэтому, я думаю, мы легко поймем друг друга.

В данной теме хотелось бы прежде всего обсудить реализацию контроллера и его взаимодействие с представлением. Этот вопрос актуален независимо от того, какой подход используется для реализации бизнес логики или самого представления. Хочется напомнить, что мы говорил о написании MCV фреймворка на нетипизированном языке (PHP, Perl, Ruby etc.), поэтому иногда правильностью можно пожертвовать ради гибкости и простоты. "Объекты Perl не плохи; просто они другие" (с) Кристиансен.

Итак, к теме. Большинство MCV реализованы с использование контроллера запросов. Все запросы направляются на определенный скрипт, который, на основе конфигурационного файла (ini, xml, языковой структуры) находит, создает и передает управление обработчику команды. При этом обработчику передается глобальный конфигурационный файл, запрос и прочие данные. Обработчик манипулирует объектами модели, а затем либо передает управление шаблонизатору, либо другому обработчику (форвардинг).

Момент первый, который хотелось бы обсудить. Я знаком с двумя способами реализации обработчиков:
1. Обработчиком является метод некоторого класса. Часто один класс содержит все методы-обработчики для модуля.
2. Обработчиком является отдельный класс, наследующий базовый класс-обработчик, в котором реализован стандартный набор методов (new, init, process). Переопределяя метод process происходит управление поведением обработчика.
Я долгое время использовал первый вариант, но затем перешел на классы-обработчики.

Момент второй. Необходим класс, который будет на основе переменных окружения определять модуль и команду, должным образом обрабатывать параметры, загружать и предоставлять доступ к глобальному конфигу (базовый URL, основные пути и т.д.), устанавливать и считывать некоторые атрибуты, устанавливать значения, которые будут доступны из шаблона и т.д. Плюс к этому, установка заголовков, плюшек, управление буфером.

В PHP-порте Struts, например, существуют отдельные классы request, response, context (порты java классов). С одной стороны, такое разделение оправдано, с другой - не очень удобно передавать в каждый класс 4-5 объектов (на входе они все равно собираются), да и иногда не понятно, какой метод в каком классе нужно искать. В одной из Perl систем, встретившихся мне, реализован только класс request, который берет на себя практически все обязанности (установка заголовков, обработка параметров, установка переменных для шаблона). Я в своей системе пришел к компромиссному решению: написал классы Response, Config, Displayer и Request. В Request существуют три поля, которые хранят экземпляры объектов Response, Config, Displayer. Таким образом, имеем четкую структуру, сохраняя возможность держать все в одном объекте и передавать только его. Кстати, была идея сделать ничего не делающий контейнер для всех объектов, но т.к. Request у меня самый используемый решил поручить эту задачу именно ему.

Итак, вот затравка для начала дискуссии.
 

camka

не самка
Поправьте меня, но я бы сделал эти классы статическими, поскольку несколько объектов подобного класса в любом случае не встречается, по крайней мере для конфигуратора. Очень удобно оперировать таким конфигуратором из любой точки программы, будь то модель вью или контроллер, и не надо ничего передавать в параметрах. Ну или по крайней мере реализовать их через синглтон с доступом к его объекту через тот же статичекий метод.
 

syfisher

TDD infected!!
Обработчиком является отдельный класс, наследующий базовый класс-обработчик, в котором реализован стандартный набор методов (new, init, process). Переопределяя метод process происходит управление поведением обработчика.
Я долгое время использовал первый вариант, но затем перешел на классы-обработчики.
Мы тоже пользуем этим методом, так как позволяет более качественно повторно использовать код.

Проблема в том, как организовывать эти команды (мы называем их имени шаблона) для разных приложений. Я уже поднимал тему в том, как объединять команды и метод, на котором я остановился - это StateMachine.

Необходим класс, который будет на основе переменных окружения определять модуль и команду, должным образом обрабатывать параметры, загружать и предоставлять доступ к глобальному конфигу (базовый URL, основные пути и т.д.), устанавливать и считывать некоторые атрибуты, устанавливать значения, которые будут доступны из шаблона и т.д. Плюс к этому, установка заголовков, плюшек, управление буфером.
У нас класс, который определяет модуль и команду, отличен от response и request. Вообще принцип организации приблизительно такой: сначала запускается приложение, которое по сути состоит из набора InterceptionFilters (такие как кеширование, старт сессии, логи, определение модуля и т.д.). На одном из последних этапов - это будет фильтр, который запустит StateMachine и команды будут исполняться.

Связь модели и View происходит в этих командах. Если отключить из StateMachine команды, которые обеспечивают связь со View, то получится, что система может работать без View вообще. Тем более, что View у нас на 90% является активным и данные получает в основном сам.

Почему это именно так? Просто мы стараемся, где это возможно делать клиентом системы не человека, а другое приложение, которое может слать любые request-ы в наше приложение.

Насчет передачи request, responce. Эта тема меня также волнует. Пока мы остановились на том, что у нас есть глобальная фабрика, у которой можно спросить любой из интересующих тебя классов, из наиболее важных для системы. Например,

PHP:
 $datasource = Limb :: toolkit()->getDatasource('navigation_datasource');

 $request= Limb :: toolkit()->getRequest();
Нам такой подход очень понравился, тем более, что он значительно облегчает тестирование системы и более не приходится для каждого класса, который используется внутри другого класса писать методы вроди таких:

PHP:
 protected function _getRequest()
 {
    return Request :: instance();
 }
 

Domovoj

Guest
Я тоже сейчас нахожусь в процессе обдумывания основы своих приложений и меня заинтересовал вопрос, является ли MVC действительно полезным.

Т.е. понятно, что MVC - это прорыв с простым написанием php-скриптов.

Вопрос в другом - не устарел ли сам pattern MVC?

В частности, то, что мне в нём не нравится, так это отсутствие такого понятия как "страница", что, вообще-то, не всегда жёстко связано с понятием "action".

На мой взгляд более современными являются компонентно-ориентированные frameworks, а не гибрид MVC + template engine. В таких системах, насколько я понимаю, понятия страницы и action разделены, а сами action закреплены за компонентами, которые располагаются на страницах.
 
Автор оригинала: Domovoj
На мой взгляд более современными являются компонентно-ориентированные frameworks, а не гибрид MVC + template engine. В таких системах, насколько я понимаю, понятия страницы и action разделены, а сами action закреплены за компонентами, которые располагаются на страницах.
MVC – это парадигма, гласящая о разделении приложения на модель (бизнес логика), котроллер и представление. То, каким образом ты реализовываешь каждый из этик компонент – твое дело. Если тебе не нравится контроллер запросов, используй контроллер страниц.
 

mrjazz

Новичок
Ужасно интересная тема затронута, только начал осознавать силу паттернов в веб программировании, но разбираться на примере таких систем как struct, tapestry несколько сложновато, для начала. Не посоветуете ли какую-нибудь простую но наглядно демонстрирующюю эти парадигмы систему? Заранее очень брагодарен.
 
Проблема в том, как организовывать эти команды (мы называем их имени шаблона) для разных приложений. Я уже поднимал тему в том, как объединять команды и метод, на котором я остановился - это StateMachine.
Не совсем понял, в чем заключается проблема. У меня есть базовый класс-команда, а так же набор common классов (добавление, удаление, редактирование, отображение и т.д.). Кстати, во многих системах есть опция, позволяющая в конфигурационном фале указать, что для определенного action-а нужно просто отобразить шаблон (для реализации этого используются дополнительные методы). Для того, чтоб избежать излишней детализации и написания дополнительного кода, можно и эту проблему решать с использованием обычного класса-команды.

У меня все работает примерно так (перл код):
PHP:
my $r = WCP::Request::CGI->new;
$r->setDisplayer(WCP::Displayer->new);
$r->setConfig(WCP::Config->new);
$r->setResponse(WCP::Response->new);
// парсинг параметров, определение команды и т.д. Вынес для отладочных целей.
$r->prepare;
my $fc = WCP::FC->new();
$fc->process($r);
FC – это сам фронт контроллер.

$r передается везде и через $r->displayer->*, $r->config->* и т.д. предоставляет доступ к нужным объектам. Фактически, для каждого запроса создается и используется только один $r.

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

Кстати, какие классы вы используете помимо request и response? Где реализованы методы установки параметров для шаблона и прочите, которые нельзя отнести ни к request ни к response? Каким образом предоставляется доступ к конфигу?
 

syfisher

TDD infected!!
Проблема вот в чем.

У нас есть framework, где есть такие же как у тебя базовые команды для добавления, удаления и т.д. Однако объекты в рамках любого приложения могут разными и выполнять различную функцию.

Например, заказ выполненный в эл. магазине. Это доменный объект. Для него мы пишем команды, которые обеспечивают все, что касается процессинга заказа. Заказы где-то там сохраняют в таблице. Заказы и все, что с ними связано, мы помещаем в отдельный пакет и забываем о нем.

Далее мы решаем, что неплохо бы поместить заказы в единое дерево сайта и разделить зону ответственности над заказами в соответствие с ветками сайта. При создании заказа будет указываться, кто будет иметь к нему доступ. С точки зрения заказа ничего не меняется. Однако функциональность уже другая. При создании заказа его нужно уже вязать в дерево, писать права доступа, может еще что. Причем во framework уже есть команды для связей любого объекта с деревом и команды для установки прав доступа.

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

Теперь про View. У нас есть UseViewCommand($template_name),который в toolkit кладет handler на template и RenderViewCommand(), который получает из toolkit handler, делает реальный объект, выполняет и помещает в response. Это практически все. Конечно, таких команд может быть побольше (для разных систем), но мы пользуемся только WACT и все пока устраивает. Тем более что все, что нужно в шаблонах, они забирают сами и очень редко получают через другие команды (обычно в случае сложной доменную логики, когда данные получаются непосредственно из доменных объектов). Модель никогда ничего не знает про то, что ее отображают. Те, команды, которые знают о View легко выбрасываются из StateMachine и можно легко получить приложение без View совсем.

Немного расскажу про то, как у вообще нас устроена работа системы. Она openSource (http://limb-project.com), так что никакого криминала здесь нет :) По пути или по node_id определяется site_node (некоторые имеют это страницей, однако в действительности это не так), который может быть связан определенным доменным объектом плюс содержит так называемый behaviour. Behaviour используется для параметрирования StateMachine для определенного action. Выполнение StateMachine комманд, относящихся к action приводит к тому, что в Limb :: toolkit()->getResponse() содержит ответ системы на Limb :: toolkie()->getRequest().

Вообще мне очень понравился подход, который гласит, что если объект нужен во многих местах, то передавать его не смысла, просто поместите его в легко доступное место.

К сожалению ни одного релиза данной версии пока нет (это ветка php5 и Limb 3.+), поэтому чтобы все это потрогать, нужно качать из репозитория (svn), причем функциональность доступна только в виде тестов - мы начали процесс выделения WACT, поэтому многое, что не покрыто тестами не работает.

Насчет того, каким образом предоставляется доступ к конфигу. Приблизительно так:
PHP:
Limb :: toolkit()->getIni()->getOption('common', 'some_option').
Так как tookit доступен везде, то любой может получить любой параметр из конфигу. Так как toolkit можно заменить, то можно использовать любой способ загрузки конфиг-файлов.
 
Насчет команд проблему понял, но, если честно, с ней еще не сталкивался, посему не было никаких идей касательно решения.

Касательно обработки View. Я уже писал, что решил вынести это в отдельный класс и сейчас подробнее опишу почему. В классе Request (судя по всему аналог вашего Limb), существуют всего два метода, связанные с шаблонами:
setTemplate
setTemplateVar
Первый устанавливает имя (не ссылку) шаблона, при помощи второго можно установить доступные для шаблона переменные. Класс WCP::Displayer содержит методы:
- получение шаблона (из файла, базы и т.д.);
- установка переменных для шаблона;
- создание объекта для обработки шаблона, обработка, возврат результата;
- метод render, который последовательно выполняет 3 метода и пишет контент в responce.
Удобно, нет привязки к конкретному шаблонизатору, можно получать шаблоны, например, из БД, а не файла.

Выполнение StateMachine комманд, относящихся к action приводит к тому, что в Limb :: toolkit()->getResponse() содержит ответ системы на Limb :: toolkie()->getRequest().
Этот момент не понял.

Вообще мне очень понравился подход, который гласит, что если объект нужен во многих местах, то передавать его не смысла, просто поместите его в легко доступное место.
Во-первых, в перле способ реализации методов класса немного хромает, во-вторых, перл предает объекты по ссылке автоматически посему писать каждый раз два символа ($r) совсем не сложно, ну а в третьих:
$r->response->setHeader('content-type', 'text/html');
WCP::Request->getResponce()->setHeader('content-type', 'text/html');

Хотя для логирования я использую MLog::log() и, судя по всему, то же самое буду использовать для ошибок.

Кстати, конфиг я тоже вынес в отдельный класс. Опять же, наглядное разделение, и удобно, если нужно, например, научить читать не только ini, но и xml конфиги.

А каким образом вы делаете маппинг? XML конфигом? Что указываете для каждого экшена кроме класса, который его обрабатывает (класс-команда) и класса модели? Иногда бывает, что для разных экшенов нужно выполнять одинаковые действия, но выводить разные шаблоны. Практикуете ли установку в конфиге списка шаблонов для экшена?
 

syfisher

TDD infected!!
Возможно вот эта тема многое прояснит насчет StateMachine

http://phpclub.ru/talk/showthread.php?s=&threadid=56936&rand=5

-~{}~ 01.12.04 11:43:

А каким образом вы делаете маппинг?
Маппинг можно делать по разному. Это зависит от функциональности отдельного фильтра (в теории :)). Пока только по node_id из таблицы. Все, что касается actions у нас знает Behaviour.

Такая возможность есть, но ей ни разу не пользовались. Возможно в будущем при построении упрощенных приложений.

Что указываете для каждого экшена кроме класса, который его обрабатывает (класс-команда) и класса модели?
Каждый action у нас состоит из нескольких команд. Различные команды могут принимать в конструкторе имя шаблона, название Dataspace, имя доменного класса, может быть что-то еще. По-разному, хотя мы стремимся передавать как можно меньше параметров в объекты - так проще тестировать. Поэтому у нас есть такие команды:
create_document_action вместо create_action('document').

Иногда бывает, что для разных экшенов нужно выполнять одинаковые действия, но выводить разные шаблоны. Практикуете ли установку в конфиге списка шаблонов для экшена?
StateMachine позволяет легко это реализовать.
 
Сверху