Паттерны для новичков: MVC vs MVP vs MVVM. Введение в MVC онлайн урок

По всему интернет-миру разбросаны миллионы веб-приложений. Есть совсем простые, есть такие, что сам «архитектор матрицы ногу сломит». Но их объединяет одно — MVC .

Самый популярный архитектурный паттерн в мире среди веб-приложений — модель-представление-контроллер (Model View Controller или просто MVC). Впервые, он был использован ещё в конце 70-х двадцатого века, в приложениях на языке Smalltalk . А затем, его приютили программисты Java и расшарили для всего мира и всех языков программирования. PHP не стал исключением. Сегодня, только малая часть программистов, коллекционирующих раритетный PHP-код может себе позволить не смотреть в сторону MVC.

Таким популярным он стал неспроста. Он просто рождён для создания гибких и масштабируемых приложений, которые легко сопровождать и достраивать.
Цель нашего тьюториала — показать на простом примере, как работает паттерн MVC.

Чтобы выполнить задания, вам потребуются следующие программы:

Примечания:

Паттерн MVC

Теперь обо всём по порядку. Сначала раскроем великую тайну аббревиатуры, в которой, очевидно, отражается тот факт, что приложение будет представлять собой три взаимодействующие части:

  • Модель отвечает за управление данными, она сохраняет и извлекает сущности, используемые приложением, как правило, из базы данных и содержит логику, реализованную в приложении.
  • Представление несет ответственность за отображение данных, которые даёт контроллер. С представлением тесно связано понятие шаблона, который позволяет менять внешний вид показываемой информации. В веб-приложении представление часто реализуется в виде HTML-страницы.
  • Контроллер связывает модель и представление. Он получает запрос от клиента, анализирует его параметры и обращается к модели для выполнения операций над данными запроса. От модели поступают уже скомпонованные объекты. Затем они перенаправляются в представление, которое передаёт сформированную страницу контроллеру, а он, в свою очередь, отправляет её клиенту.

Схематично потоки данных в этой модели можно представить так:

Вход в реальность

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

Мы не будем сейчас рассматривать архитектуру всей социальной сети. Мы возьмём только маленькую подзадачку, представим всю её серьёзность и применим к ней паттерн MVC.

Как только мы начинаем его использовать, то сразу задумываемся — а как бы нам расположить скрипты нашего решения так, что бы всё было под рукой? Для этого, разместим каждый из трёх разделов нашей MVC-системы по отдельным папкам и, таким образом, получим простую структуру каталогов, в которой легко найти то, что нам нужно. Кроме того, эти три папки поместим в каталог lib, и вынесем его выше корневого веб-каталога www:

/lib --/controller ---- FrendCnt.php --/model ---- Frend.php ---- FrendList.php --/view ---- frendlist.php ---- frendone.php /www -- index.php -- .htaccess

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

Контроллер

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

В нашем простом примере, контроллер будет сконцентрирован в одном классе FrendCnt . Подробнее его опишем позже.А сейчас немного о точке входа в веб-приложение — это, конечно, будет файл index.php . В нём, мы определим точку отсчёта для подключения наших скриптов. Создадим экземпляр контроллера, и вызовем у него метод, который начнёт обрабатывать HTTP-запрос и определит что делать дальше.

Листинг №1 (файл index.php):

$baseDir = dirname(__FILE__) . "/.."; include_once($baseDir . "/lib/controller/FriendCnt.php"); $controller = new FriendCnt(); $controller->invoke();

Теперь о контроллере. У нас — это класс FriendCnt . Вы уже заметили, что экземпляр этого класса создаётся в index.php . Он имеет только один метод invoke() , который вызывается сразу после создания экземпляра. В конструкторе контроллера, создаётся объект на основе класса модели — FrendList (список друзей) для оперирования с данными.

В функции invoke() , на основе пришедшего HTTP-запроса, принимается решение: какие данные потребуются от модели. Затем происходит вызов метода извлекающего данные. Далее происходит подключение шаблонов для отображения, которым передаются данные из контроллера. Обратите внимание, что контроллер ничего не знает о базе данных или о том, как страница генерится.

Листинг №2 (файл контроллера FriendCnt.php):

Require_once($baseDir . "/lib/model/FriendList.php"); class FriendCnt { public $oFriendList; public function __construct() { $this->oFriendList = new FriendList(); } public function invoke() { global $baseDir; $oFriendList = $this->oFriendList; if(isset($_GET["key"])) { $oFriendList->setKey($_GET["key"]); $oFriend = $oFriendList->fetch(); include $baseDir . "/lib/view/friendone.php"; }else { $aFriend = $oFriendList->fetch(); include $baseDir . "/lib/view/friendlist.php"; } } }

Модель и сущности

Модель — это образ реальности, из которой взято только то, что нужно для решения задачи. Модель концентрируется на логике решения основной задачи. Многие называют это бизнес-логикой, на ней лежит большая ответственность:

  • Сохранение, удаление, обновление данных приложения. Это реализуется через операции с базой данных или через вызов внешних веб-сервисов.
  • Инкапсуляция всей логики приложения. Абсолютно вся логика приложения без исключений должна быть сконцентрирована в модели. Не нужно какую-то часть бизнес-логики выносить в контроллер или представление.

У нас к модели относятся два скрипта, в каждом из которых определён свой класс. Центральный класс FriendList и класс-сущность Friend . В центральном классе, происходит манипуляция с данными: получение данных от контроллера и их обработка. Класс-сущность служит контейнером для переноса данных между моделью и представлением, а также определяет их формат. При хорошей реализации паттерна MVC, классы сущности не должны упоминаться в контроллере, и они не должны содержать какую-либо бизнес-логику. Их цель - только хранение данных.
В классе FriendList , работающем со списком друзей, мы создали функцию, которая моделирует взаимодействие этого класса с базой данных. Метод getFriendList() возвращает массив из объектов, созданных на основе класса Friend . Для обеспечения удобства работы с данными, также была создана функция, индексирующая массив объектов. Контроллеру оказались доступны только два метода: setKey() — устанавливает поле ключа, по которому возвращаются детальные данные о друге; fetch() — возвращает или конкретный объект или весь список друзей.

Листинг №3 (файл модели FriendList.php):

Require_once($baseDir . "/lib/model/Friend.php"); class FriendList { private $oneKey; private function getFriendList() { return array(new Friend("Александр", "1985", "[email protected]"), new Friend("Юрий", "1987", "[email protected]"), new Friend("Алексей", "1989", "[email protected]"),); } private function getIndexedList() { $list = array(); foreach($this->getFriendList() as $val) { $list[$val->getKey()] = $val; } return $list; } public function setKey($key) { $this->oneKey = $key; } public function fetch() { $aFriend = $this->getIndexedList(); return ($this->oneKey) ? $aFriend[$this->oneKey] : $aFriend; } }

В зависимости от реализации объектов Сущности, данные о ней, могут быть оформлены в виде XML-документа или JSON-объекта.

Листинг №4 (файл сущности Friend.php):

Class Friend { private $key; private $name; private $yearOfBirth; private $email; public function __construct($name, $yearOfBirth, $email) { $this->key = md5($name . $yearOfBirth . $email); $this->name = $name; $this->yearOfBirth = $yearOfBirth; $this->email = $email; } public function getKey() { return $this->key; } public function getName() { return $this->name; } public function getYearOfBirth() { return $this->yearOfBirth; } public function getEmail() { return $this->email; } }

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

Теперь нам нужно представить данные в наилучшем свете для пользователя.

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

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

В нашем примере представление содержит только два файла: для отображения детальной информации о друге и для отображения списка друзей.

Листинг №5 (файл для вывода списка друзей friendlist.php):

Мои друзья

Имя Год рождения

У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:

Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.

Чтобы направить все запросы на главную страницу, мы воспользуемся mod_rewrite и установим в.htaccess директиву RewriteRule. Вставим следующий код в файл.htaccess и сохраним его в той же директории, что и index.php:

RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?route=$1

Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.

Если у Вас нет возможности использовать.htaccess или mod_rewrite, то Вам придётся вручную адресовать все запросы к index.php. Другими словами, все ссылки должны будут иметь вид «index.php?route=[здесь-идёт-запрос]». Например, «index.php?route=chat/index».

Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php: