Впервые услышав такие слова, как Model View Controller, мне было интересно, но немного не ясно, поскольку, не зная ничего о шаблонах проектирования, разобраться в конкретном их случае, достаточно сложно. Мешало малое количество опыта в разработке приложений, да и вообще малая осведомлённость в подобном вопросе.
Новичку, пожалуй, сразу начинать с рассмотрения вопросов шаблонов не следует, но по мере продвижения в области разработки ПО, уделить внимание этой области, так или иначе, необходимо.
Итак, давайте рассмотрим теоретические выкладки, но применительно к веб-приложениям.
Стандартная схема архитектуры «Модель-Вид-Контроллер» изображена на следующем рисунке: (схема заимствована из книги «Ajax in action» издательского дома «Вильямс»)
Разберём по пунктам данную схему.
В шаблоне MVC, как следует из названия, есть три основных компонента: Модель, Представление, и Контроллер.
Представление (вид) отвечает за отображение информации, поступающей из системы или в систему.
Модель является «сутью» системы и отвечает за непосредственные алгоритмы, расчёты и тому подобное внутреннее устройство системы.
Контроллер является связующим звеном между «представлением» и «моделью» системы, посредством которого и существует возможность произвести разделение между ними. Контроллер получает данные от пользователя и передаёт их в «модель». Кроме того, он получает сообщения от модели, и передаёт их в «представление».
Применительно к интернет-приложениям бытует мнение, что части контроллер и представление объединены, потому что за отображение и одновременно за ввод информации отвечает браузер. С этим можно согласиться, а можно не соглашаться и выделить-таки контроллер в отдельную часть, что мы и сделаем.
Итак, условимся:
Представление. Модуль вывода информации. Это может быть шаблонизатор или что-либо подобное, цель которого является только в выводе информации в виде HTML на основе каких-либо готовых данных.
Контроллер. Модуль управления вводом и выводом данных. Данный модуль должен следить за переданными в систему данными (через форму, строку запроса, cookie или любым другим способом) и на основе введённых данных решить:
Передавать ли их в модель
Вывести сообщение об ошибке и запросить повторный ввод (заставить модуль представление обновить страницу с учётом изменившихся условий)
Кроме того, контроллер обязан определять тип данных, полученных от модели (есть ли это готовый результат, отсутствие оного, либо сообщение об ошибке) и передавать информацию в модуль представления.
Модель. Модуль, отвечающий за непосредственный расчёт чего-либо на основе полученных от пользователя данных. Результат, полученный этим модулем, должен быть передан в контроллер, и не должен содержать ничего, относящегося к непосредственному выводу (то есть должен быть представлен во внутреннем формате приложения).
Достаточно сложно с первого раза разобраться и понять. На это требуется время и подходящий проект.
Но на самом деле ничего сверхсложного в этом нет.
Представим себе в качестве представления какой-либо класс, который с помощью шаблонизатора выводит результат или сообщение об ошибке. На его вход подаётся либо массив с данными (объект или что-либо иное), либо переменная, содержащая текст с ошибкой.
В качестве контроллера будет выступать класс, производящий все необходимые проверки корректности данных и генерирующий сообщения об ошибках. Проверку данных целесообразно поместить именно в класс контроллера, их используют достаточно часто. Как вариант, можно просто наследовать класс контроллера от более общего класса, реализующего проверку входных данных по заданным правилам. Или, если так будет удобнее, включить в класс контроллера класс или серию функций проверки данных.
Этот же класс должен передать данные, полученные в результате работы модели, в класс представления для вывода.
Одними словами схему потоков данных в этой архитектуре объяснить сложно, поэтому обратимся к языку UML и к диаграмме последовательностей в частности (незначительные отступления от UML, принятые в диаграммах, заключаются в том, что в некоторых случаях вместе с именами сущностей или объектов, даны переводы в скобках).
На этой диаграмме показана последовательность действий, а также последовательность передаваемых данных: от пользователя, к пользователю и между модулями.
Схема отображает типичный процесс вывода формы, заполнения её пользователем и возврат пользователю результатов. Никаких ошибок в данном случае не происходит.
Как видно из диаграммы, обращение к модели происходит лишь в случае посылки пользователем верных данных. На внутреннем же уровне приложения, модель отделена от представления и контроллера. Контроллер также отделён от модели и представления, и его функция состоит в управлении и проверке.
Теперь попробуем составить диаграмму классов для большей наглядности.
Диаграмма классов содержит три класса, по одному для каждого компонента архитектуры MVC. Для удобства, они так и названы: Model, View, Controller.
В представлении есть три функции (хотя,
вполне возможно обойтись только лишь одной), которые отвечают за отображение
состояния приложения:
displayDefault() – вывод формы по умолчанию.
displayError(error
= false) – вывод
формы с сообщением об ошибке, либо отдельной формы с ошибкой
displayResults() – вывод результатов вычислений
Контроллер имеет не только методы, но и поля. С полями всё просто: это ошибка и результаты вычислений. По умолчанию им задаётся значение false, что свидетельствует о том, что пока нет ни ошибки, ни результатов.
Три метода, присутствующие в контроллере, служат для управления и проверки. Метод для проверки (validate()) является необязательным, и вполне может отсутствовать, если никаких проверок не требуется.
Метод processData() служит для вывода формы по умолчанию, однако он включает также метод userRequest(), функциональность которого выполняется лишь в том случае, когда есть введённые пользователем данные. Именно метод userRequest() содержит в себе функцию validate() (если данные не введены, следовательно, незачем делать их проверку) и, кроме того, должен содержать вызов конструктора класса модели.
В модели может содержаться любое
количество полей и методов. Однако два метода должны быть
обязательными (или даже один. Как удобнее будет).
calculate() – функция,
производящая основной расчёт
getData() – функция, возвращающая данные результата.
Разделение функций модели скорее смысловое. Вполне достаточно создать один метод, который будет и считать, и возвращать результат.
Возвращаемся в метод userRequest() контроллера. После того, как в нём был посчитан результат и возвращён в том или ином виде, его можно смело отдавать на вход функции displayResults() класса View. Однако заметим, что в принципе, можно отдавать на вход представления и экземпляр класса модели, если вывод оного хранится в его полях, а их много (если лень, так сказать, создавать структуру, массив либо ещё какой-либо объёмный тип данных).
Если функция validate() контроллера выявила ошибку, и установила значение поля error в значение, отличное от false, контроллер сам вызовет метод displayError() класса View.
Теперь уместно привести ту же самую диаграмму последовательности, но заменив в ней смысловые значения, названиями функций классов из соответствующей диаграммы.
Итак, собственно, у нас есть три класса и алгоритм взаимодействия между ними. Суть архитектурного шаблона MVC состоит в том, чтобы чётко разделить представление, управление и модель системы. Это очень удобно, ведь если что-либо поменяется в одной из частей системы, других частей эти изменения не коснутся.
Например, в представлении мы можем написать:
// Это код на PHP public function displayDefault() { echo "<p>Введите имя: "; echo "<input type='text' name='name' value=''> "; }
А потом через месяц ужаснуться, и перейти к использованию шаблонизатора. Скажем, smarty.
Или, например, в модели изменить пару расчетных формул. Или в контроллере убрать пару ограничений, или изменить метод приёма-передачи данных. Если же взять в расчёт принципы наследования в ООП, то архитектура MVC станет ещё удобнее. Скажем, когда есть две формы, выглядящие одинаково, но с несколько отличающимися алгоритмами расчёта.
В заключении, хотелось бы всё же привести некоторый скелет кода на PHP для лучшего усвоения идеи MVC.
<? /** * Пример реализации MVC на PHP * **/ class Controller { private $error; private $result; function __construct() { $this->error = false; $this->result = false; } function processData() { $this->userRequest(); if ($this->error) View::displayError($this->error); else if ($this->result) View::displayResults($this->result); else View::displayDefault(); } function userRequest() { // данные отправлены if (isset($_POST['send'])) { $this->validate(); if (!$this->error) { // основные вычисления $model = new Model(); $model->calculate($_POST['name']); $result = $model->getData(); // проверка на ошибки в самой модели if (!is_array($result)) $this->error = $result; else $this->result = $result; } } } function validate() { if (empty($_POST['name'])) $this->error = 'Не введено имя!'; else if (strlen(strval($_POST['name'])) < 3) $this->error = 'Имя слишком короткое!'; } } // class Controller class View { static function displayDefault() { echo "<form method='POST' action=''>"; echo "<p>Введите имя: "; echo "<input type='text' name='name' value=''> "; echo "<input type='submit' name='send' value='Отправить'>"; echo "</form>"; } static function displayError($error) { echo "<p><b>Ошибка:</b> {$error}"; View::displayDefault(); } static function displayResults($results) { echo "<p><b>Результаты:</b>"; echo "<p>Ваше имя <b>".$results[0]. "</b> означает <i>".$results[1]."</i>"; echo "<p><a href='".$_SERVER['REQUEST_URI']. "'>Узнать ещё об одном имени</a>"; } } // class View class Model { private $data; function __construct() { $this->data = false; } function calculate($name) { $this->data[] = $name; $len = strlen($name); if ($len == 3) $this->data[] = 'краткость - сестра таланта'; else if (($len > 3) && ($len < 6)) $this->data[] = '...нет особого значения'; else $this->data[] = 'невероятно богатая фантазия родителей'; } function getData() { if ($this->data) return $this->data; else return 'Вычисления не произведены!'; } } // class Model $controller = new Controller(); $controller->processData(); ?>
Работу данного кода можно увидеть в примере.
Итак, мы получили простейшую MVC-систему. Выделим положительные и отрицательные стороны:
К минусам можно отнести
Увеличение объема кода
Необходимость соблюдения заранее заданного интерфейса
Для поддержки разработки требуются более квалифицированные специалисты
Последнее требование к нашему примеру не относится, но для реальных систем оно весьма актуально.
К плюсам отнесём следующее:
Несомненно более гибкий код
Возможность повторного использования каждой из трёх составных частей MVC
Безболезненная замена модели (другие алгоритмы расчета, способа хранения данных и т.д.)
Достаточно просто перейти от одного представления, к другому (от HTML к XML или JSON)
Надо сказать, что код примера не идеален. В нём есть просторы для рефакторинга (несмотря на то, что он занимает чуть более ста строк). Скажем, в примере участвует всего лишь одна переменная, поступающая от пользователя (name), но что если их будет много? Как их хранить? Как сделать так, чтобы при изменении метода, не пришлось править код более чем в одном месте?
Всё это оставьте себе в качестве домашнего задания.
Разбор несколько более сложного примера, можно посмотреть в статье «Рейтинг с помощью MVC».
И напоследок: чаще мыслите не кодом, а схемами, шаблонами, абстракциями. Понятно, что это непросто, непривычно и сложно, но учиться этому нужно обязательно.
P.S. Отметим, что класс контроллера вдобавок построен по архитектурному шаблону Facade, суть которого состоит в сокрытии внутреннего устройства системы и предоставлении пользователю только функций самого высшего уровня. Эту суть убедительно доказывают две последние строки кода.
UPD0: На самом деле шаблон MVC куда более общий, мы рассмотрели, скорее, его вариант в виде MVP - Model View Presenter, где контроллер есть "медиатор" между представлением и моделью. Более того, в случае реализации MVC в GUI приложениях используют не так называемый Flow-подход (контроллер сам обращается к модели и представлению), а событийный подход с использованием шаблона Observer.
UPD1: Фреймворк CakePHP использует шаблон MVC, запрос к фреймворку изображён на этой схеме. Схема очень близка к той, что обсуждалась выше.
UPD2: В контроллере, как правило, лучше использовать динамическое связывание или абстрактную фабрику. Обычно это выглядит следующим образом:
<? $model = ModelAbstractFactory::getFactory(); $controller = new Controller($model); View::display($controller); ?>
Есть способ выделить контроллер не в виде класса, а в виде модуля:
<? $model = ModelAbstractFactory::getFactory(); $model->calculate(); View::display($model); ?>
В таком случае, схема MVC становится более похожей на классическую, но тогда модель должна представлять специальный интерфейс для доступа к данным.
Отметим также, что некоторые фреймворки устроены так, что все три части MVC есть три отдельных файла, которые связываются по названию автоматически.