Skip to content

Latest commit

 

History

History
executable file
·
251 lines (178 loc) · 23 KB

mvc.md

File metadata and controls

executable file
·
251 lines (178 loc) · 23 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

Начнем с Модели, так как она является по сути ядром приложения. Для того, чтобы представить Объявление в виде объекта, удобно использовать класс 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;
    }

    /**
     * Возвращает все имеющиеся объявления
     * @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/post-list.phtml';

Осталось написать только Представление, которое будет отображать список объявлений в виде HTML страницы. Создадим файл view/post.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 уязвимости (урок про XSS).

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

php -S localhost:8001

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

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

  • FrontController
  • роутер

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

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

Вариации MVC

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

Ссылки