Skip to content

Latest commit

 

History

History
executable file
·
255 lines (180 loc) · 24.6 KB

mvc.md

File metadata and controls

executable file
·
255 lines (180 loc) · 24.6 KB

Архитектура MVC

Архитектура MVC позволяет нам разделить код приложения на 3 части: Модель (Model), Вид или Представление (View) и Контроллер (Controller). Впервые она была описана в 1978 году, и предназначалась для приложений с графическим интерфейсом (окошками и кнопками), но позже была адаптирована и для веб-приложений.

Разделение на части позволяет упростить большой по объему код. Если код писать одним длинным скриптом, в нем становится тяжело разобраться, и тяжело вносить изменения, не допустив ошибку.

MVC не привязана к какому-то конкретному языку программирования, и не требует использования объектно-ориентированного программирования или какой-то другой парадигмы.

Разделение на части здесь не значит, что в коде должно быть ровно 3 файла (или 3 папки с файлами, или 3 класса) с названиями model, view и controller. MVC ничего не говорит нам по поводу того, как организовывать файлы с кодом. На практике модель часто занимает основной объем приложения, и представлена в виде большого числа разнотипных классов - сущностей, сервисов, классов работы с БД, и для каждого вида классов делают отдельные папки.

MVC применима к разным видам приложений - и к серверным веб-приложениям, и к десктопным (клиентским) приложениям. Разница между ними в том, что в веб-приложении программа получает один запрос от пользователя, обрабатывает его, выводит результат (обычно это веб-страница) и завершается. Если придет еще один запрос, будет запущена новая, независимая копия программы для его обработки. В отличие от веб-приложений, десктопные, мобильные приложения (а также написанные на яваскрипте приложения, которые работают на странице браузера) долгоживущие. Они обрабатывают много запросов от пользователя и обновляют информацию на экране, не завершаясь.

Компоненты MVC приложения

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

Например, в программе на PHP Модель не должна обращаться к внешним переменным вроде $_GET/$_POST/$_SESSION/$_COOKIE и не должна ничего выводить через echo. Все необходимые данные она получает через аргументы функций, и возвращает результат через return. А в программе на JS Модель не должна пытаться взаимодействовать с объектами вроде document и любыми DOM-элементами (DOM - это часть Представления).

Модель не должна никак зависеть и не должна ничего знать о Контроллерах и Видах.

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

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

В PHP оно не должно обращаться к внешним переменным ($_GET и другие), его задача просто отобразить те данные, которые ему передали.

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

Контроллер отвечает за выполнение запросов, пришедших от пользователя. В веб-приложении обычно контроллер разбирает параметры HTTP-запроса из $_POST/$_GET, обращается к модели, чтобы получить или изменить какие-то данные, и в конце вызывает Представление, чтобы отобразить результат выполнения запроса. Число контроллеров определяется числом разделов или страниц сайта. В десктопных приложениях Контроллер отвечает за обработку нажатий на кнопки и других воздействий от пользователя.

Один Контроллер может работать с несколькими Моделями, и наоборот, одна Модель может использоваться в нескольких Контроллерах.

В веб-приложении обычно Контроллеры - это набор однотипных классов, каждому разделу на сайте соответствует свой класс, и в нем делаются методы (их называют "действия", "action") для отдельных страниц (например: для раздела новостей - класс NewsController, в нем методы latestAction - вывод страницы последних новостей, archiveAction - страница архива новостей, viewAction - страница просмотра одной новости). Тут создается некоторая путаница, от того, что класс называется NewsController ("контроллер новостей"), но фактически содержит в себе не один, а несколько методов-контроллеров для отдельных страниц. Иногда делают и по-другому - на каждую страницу свой класс с одним действием, но на практике бывает удобнее группировать действия вместе.

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

Взаимодействие компонентов MVC

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

В серверных приложениях обычно используется "пассивная" модель, а в десктопных приложениях - "активная". Активная модель, в отличие от пассивной, позволяет подписываться и получать уведомления об изменении в ней. В серверных приложениях это не требуется. Вот схема, изображающая взаимодействие компонентов:

Взаимодействие компонентов MVC

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

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

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

Вооружившись этими знаниями, попробуем написать простейшее веб-приложение с использованием MVC.

Пример MVC приложения

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

Вот дерево файлов, из которых состоит приложение:

|-- public/                # публичная папка веб-сервера
|   `-- list.php           # контроллер вывода списка объявлений
|-- view/                  # папка для шаблонов страниц
|   `-- list.phtml         # шаблон для вывода списка объявлений
|-- bootstrap.php          # скрипт инициализации
|-- Post.php               # класс, представляющий одно Объявление
`-- PostService.php        # сервис для управления списком объявлений

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

Начнем с Модели, так как она является по сути ядром приложения. Для того, чтобы представить Объявление в виде объекта, удобно использовать класс Post, а для того, чтобы хранить и управлять списком объявлений, мы сделаем сервис PostService. Для начала сделаем модель Объявления (которая будет являться частью Модели из MVC) и сохраним код в файл Post.php:

<?php

/**
 * Модель объявления
 */
class Post 
{
    // Заголовок объявления
    public $title; 

    // Номер телефона 
    public $phoneNumber;

    // Текст объявления
    public $text;
}

Теперь напишем сервис, который позволит нам получить список объявлений, добавлять или удалять их. Мы не будем усложнять код и добавлять постраничную выборку, сортировку, поиск, и т.д. Так как мы не используем базу данных, то изменения будут сохраняться только до завершения программы. Вот текст файла PostService.php:

<?php

/**
 * Сервис для управления списком объявлений
 */
class PostService
{
    /** 
     * @var Post[] Список объявлений
     */
    private $posts = [];

    public function __construct()
    {
        // Список объявлений, который у нас жестко заложен в коде
        $this->posts[] = $this->createPost(
            'Продам слона',
            '+79990000001',
            'Продается пока еще небольшой дрессировнный африканский слон.'
        );

        $this->posts[] = $this->createPost(
            'Сдам 8-к квартиру около метро недорого',
            '+79990000002',
            'Сдается квартира, евроремонт, без хозяев, только серьезным людям.'
        );
        // .. при желании можно добавить еще
    }

    private function createPost($title, $phoneNumber, $text)
    {
        $c = new Post;
        $c->title = $title;
        $c->phoneNumber = $phoneNumber;
        $c->text = $text;

        return $c;
    }

    /**
     * Возвращает все имеющиеся объявления в виде масссива объектов Post
     * @return Post[]
     */
    public function getAllPosts()
    {
        return $this->posts;
    }

    /**
     * Удаляет одно объявление 
     */
    public function deletePost(Post $post)
    {
        $key = array_search($this->posts, $post, true);
        if ($key === null) {
            throw new \Exception("Post is not in list, cannot delete");
        }

        unset($this->posts[$key]);
    }

    /**
     * Добавляет новое объявление в список
     */
    public function addPost(Post $post)
    {
        // Проверим, что объявления еще нет в списке
        if (null !== array_search($this->posts, $post, true)) {
            throw new \Exception("Post already added");
        }

        // Для простоты мы не будем проверять, заполнены ли все нужные 
        // поля у объявления, хотя в реальном приложении такая проверка
        // необходима.
        $this->posts[] = $post;
    }
}

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

$service = new PostService;
$posts = $service->getAllPosts();

Напишем также скрипт инициализации bootstrap.php, который будет инициализировать наше приложение, подключать нужные классы и создавать экземпляр сервиса.

<?php

require_once __DIR__ . '/Post.php';
require_once __DIR__ . '/PostService.php';

$postService = new PostService;

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

Сначала напишем Контроллер, который будет при обращении к нему выводить список объявлений. Он будет запрашивать этот список у Модели и вызывать Вид, чтобы представить список в виде HTML страницы, которую отобразит браузер пользователя. Не будем использовать здесь функций или классов и напишем контроллер в виде простого скрипта public/list.php:

<?php 

// Инициализируем наше приложение
require __DIR__ . '/../bootstrap.php';

// Получаем список объявлений
$posts = $postService->getAllPosts();

// Вызываем вид, чтобы отобразить их
require __DIR__ . '/../view/list.phtml';

Осталось написать только Представление, которое будет отображать список объявлений в виде HTML страницы. Создадим файл view/list.phtml. Расширение phtml указывает, что это PHP-шаблон (урок про шаблонизацию):

<!doctype html>
<meta charset="utf-8">
<?php if (!$posts): ?>
    <p>Объявлений пока нет.</p>
<?php else: ?>
    <?php foreach ($posts as $post): ?>
        <article>
            <h2><?= htmlspecialchars($post->title) ?></h2>
            <div class="body"><?= htmlspecialchars($post->text) ?></div>
            <p>Телефон: <?= htmlspecialchars($post->phoneNumber) ?></p>
        </article>
    <?php endforeach ?>
<?php endif ?>

Функция htmlspecialchars() нужна для корректного вывода спецсимволов вроде & или < в тексте или заголовке объявления и для предотвращения XSS уязвимости.

Протестируем написанное приложение. Для этого достаточно запустить встроенный в php веб-сервер, открыв командную строку, перейдя в папку public и набрав команду:

php -S localhost:8001

После этого, набрав в браузере http://localhost:8001/list.php, мы должны увидеть список объявлений. При желании наше приложение можно доработать. Можно добавить сохранение объявлений в базу данных, сделать форму добавления объявления, кнопку удаления объявления, проверку правильности заполнения всех полей. Оставим это как домашнее задание читателю.

Другие классы, используемые в MVC приложениях

  • FrontController
  • роутер

Антипаттерн: толстые контроллеры

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

Вариации MVC

За долгое время было придумано несколько похожих архитектур с небольшими изменениями (MVP, MVVM ит.д.). Почитать про различия между ними можно например тут: https://habrahabr.ru/company/mobileup/blog/313538/ . Они обычно заточены под использование с каким-то фреймворком в какой-то специфической ситуации.

Ссылки