Pull to refresh

MVC + Scenario против Толстых Контроллеров

Website developmentPHPLaravel
Sandbox

MVC + Scenario Против Толстых Контроллеров


Современные PHP фреймворки (Symphony, Laravel, далее везде) убедительно показывают, что реализовать паттерн Model-View-Controller не так уж просто. Все реализации почему-то склонны к Толстым Контроллерам (fat controllers), осуждаемыми всеми, и разработчиками, и самими фреймворками.


Почему все так? И можно ли с этим как-то справиться? Давайте разбираться.


Терминология


  • Model — модель (формирователь запрошенных данных)
  • View — представление (оформитель данных модели)
  • Controller — контроллер (координатор модель-представление согласно запросу)
  • Template — шаблон представления
  • Rendering — рендеринг (формирование, оформление образа представления)
  • Renderer — рендерер (формирователь, оформитель образа представления)

Толстый контроллер


Вот типичный Толстый Контроллер:


class UserController
{
    /**
     * Действие контроллера
     * Возвращает приветствие юзеру с заданным ID
     */
    public function actionUserHello($userId)
    {
        // Получаем имя и фамилию юзера из модели юзера (База Данных)
        $user = UserModel::find($userId);

        // Шаблону представления нужно полное имя юзера - делаем его
        $name = $user->firstName.' '.$user->lastName;

        // Создаем представление с нужным шаблоном и полным именем
        $view = new View('hello', ['name' => $name]);

        // Рендерим (создаем образ) представление и возвращаем приветствие
        return $view->render();
    }
}

Что мы видим? Мы видим винегрет! В контроллере намешано все, что можно — и модель, и представление, и, собственно, сам контроллер!


Мы видим имена модели и шаблона, намертво зашитые в контроллер. Это не гуд. Мы видим манипуляции с данными модели в контроллере — формирование полного имени из имени и фамилии. И это не гуд.


И еще: мы не видим этого примере явно, но неявно оно есть. А именно: есть только один способ рендеринга (формирования образа)! Только один: по шаблону в php файле! А если я хочу pdf? А если я хочу не в файле, а в php строке? У меня были проекты с вычурным дизайном на сотне маленьких шаблончиков. Приходилось самому ляпать рендерер для строковых шаблонов. Не перегревался конечно, но дело ведь в принципе.


Краткое резюме:


Современные фреймворки имеют общие для всех недостатки в реализации MVC:
  1. Узкая трактовка MVC-представления (View) только как "Представление с шаблоном в PHP файле" вместо "Представление с любым рендерером".
  2. Узкая трактовка MVC-модели только как как "Модель домена Базы Данных" вместо "Любой компилятор данных для представления".
  3. Провоцируют использование так называемых "Толстых Контроллеров" содержащих одновременно все логики: бизнеса, представления и взаимодействия. Это полностью разрушает основную цель MVC — разделение ответственностей между компонентами триады.

Для устранения этих недостатков неплохо бы взглянуть на компоненты MVC повнимательнее.


Представление — это рендерер


Глянем на первый недостаток:


  1. Узкая трактовка MVC-представления (View) только как "Представление с шаблоном в PHP файле" вместо "Представление с любым рендерером".

Здесь все довольно просто — решение проблемы уже указано в самой формулировке проблемы. Мы должны просто сказать, что представление может использовать любой рендерер. Для реализации этого достаточно просто добавить новое свойство renderer к классу View:


class View {
    public $template, $data, $renderer;

    public function __costruct($template, $data, $renderer = NULL) {}
}

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


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


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


$name = 'дядя Ваня';
return "Hello, {$name}!";

или даже:


$return json_encode($name); // Ajax response

то мы реально определяем тот View, который в MVC, не трогая при этом никаких тех View, которые в фреймворках!


А вот теперь все на самом деле просто: те View, которые в фреймворках — это подмножество тех View, которые в MVC. Причем очень узкое подмножество, а именно, это только шаблонизаторы на основе PHP файлов.


Резюме: именно рендерер, т.е. любой оформитель образа данных и является тем View, который в MVC. А те View, которые в фреймворках, это только разновидность рендереров.


Модель домена / Модель представления (ViewModel / DomainModel)


А теперь глянем на второй недостаток:


  1. Узкая трактовка MVC-модели только как как "Модель домена Базы Данных" вместо "Любой компилятор данных для представления".

Всем очевидно, что MVC-модель — это сложная штука, которая состоит из других штук. В сообществе есть согласие по декомпозиции модели на две компоненты: доменная модель (DomainModel) и модель представления (ViewModel).


Доменная модель — это то, что хранится в базах данных, т.е. нормализованные данные модели. Типа, 'имя' и 'фамилия' в разных полях. Фреймворки заняты именно этой частью модели просто потому, что хранение данных — это своя вселенная, неплохо изученная.


Однако приложению нужны агрегированные, а не нормализованные данные. Доменные данные надо компилировать в образы типа: 'Привет, Иван!', или 'Дорогой Иван Петров!', или даже 'Для Ивана Петрова!'. Вот эти преобразованные данные и относят к другой модели — модели представления. Так вот именно эта часть модели пока игнорируется современными фреймворками. Игнорируется потому, что нет согласия как с ней быть. А если фреймворки решения не дают, то программисты идут по самому простому пути — кидают модель представления в контроллер. И получают ненавистные, но неизбежные Толстые Контроллеры!


Итого: чтобы реализовать MVC необходимо реализовать модель представления. Других опций нет. Учитывая, что представления и их данные могут быть любыми, констатируем, что имеем проблему.


Сценарий против Толстых Контроллеров


Остался последний недостаток фреймворков:


  1. Провоцируют использование так называемых "Толстых Контроллеров" содержащих одновременно все логики: бизнеса, представления и взаимодействия. Это полностью разрушает основную цель MVC — разделение ответственностей между компонентами триады.

Здесь мы подобрались к основам MVC. Давайте наведем ясность. Итак, MVC предполагает такое распределение ответственностей между компонентами триады:


  • Контроллер — логика взаимодействия, т.е. взаимодействия с как с внешним миром (запрос — ответ), так и с внутренним (Модель — Представление),
  • Модель — бизнес-логика, т.е. формирование данных для конкретного запроса,
  • Представление — логика представления, т.е. декорация данных, сформированных Моделью.

Идем дальше. Ясно просматриваются два уровня ответственностей:


  • Организационный уровень — это Контроллер,
  • Исполнительный уровень — это Модель и Представление.

Говоря по-простому, Контроллер рулит, Модель и Представление пашут. Это если по-простому. А если не по-простому, а поконкретнее? Как именно рулит Контроллер? И как именно пашут Модель и Представление?


Контроллер рулит так:


  • Получает запрос от приложения,
  • Решает, какую Модель и какое Представление использовать для этого запроса,
  • Вызывает выбранную Модель и получает от нее данные,
  • Вызывает выбранное Представление с полученными от Модели данными,
  • Возвращает декорированные Представлением данные обратно приложению.

Вот как-то так. Существенным в этой схеме является то, что Модель и Представление оказываются звеньями цепи исполнения запроса. Причем последовательными звеньями: сначала Модель преобразует запрос в некоторые данные, затем эти данные Модели преобразуются Представлением в ответ, декорированный так, как надо конкретному запросу. Типа, запрос гуманоида декорируется визуально шаблонизаторами, запрос андроида декорируется кодировщиками JSON.


Теперь попробуем разобраться, как конкретно пашут исполнители — Модель и Представление. Выше мы говорили, что есть консенсус насчет декомпозиции Модели на две под-компоненты: Модель домена и Модель представления. Это значит, что исполнителей может быть больше — не два, а три. Вместо цепочки исполнения


Модель >> Представление

вполне может быть цепочка


Модель домена >> Модель представления >> Представление

Сам собой напрашивается вопрос: а почему только два или три? А если надо больше? Естественный ответ — да ради бога, сколько надо, столько и берите!


Сходу просматриваются иные полезные исполнители: валидаторы, редиректоры, разнообразные рендереры и вообще все, что непредсказуемо, но угодно.


Давайте резюмируем:


  • Исполнительный уровень MVC (МодельПредставление) может быть реализован как цепочка звеньев, где каждое звено преобразует выход предшествующего звена во вход для последующего.
  • Входом первого звена является запрос приложения.
  • Выход последнего звена является ответом приложения на запрос.

Я назвал эту цепочку Сценарием (Scenario), а для звеньев цепочки с названием пока не определился. Текущие варианты — сцена (как часть сценария), фильтр (как преобразователь данных), действие сценария. Вообще говоря, название для звена не столь важно, есть более существенная вещь.


Существенным является последствия появления Сценария. А именно: Сценарий взял на себя основную ответственность Контроллера — определять нужные для запроса Модель и Представление и запускать их. Тем самым у контроллера остаются только две ответственности: взаимодействие с внешним миром (запрос-ответ) и запуск сценария. И это хорошо в том плане, что все компоненты триады MVC последовательно декомпозируются и становятся более конкретными и управляемыми. И еще хорошо в другом плане — контроллер MVCS становится чисто внутренним неизменяемым классом, и поэтому даже в принципе не может стать толстым.


Использование Сценариев приводит к очередной вариации паттерна MVC, эту вариацию я обозвал как MVCSModel-View-Controller-Scenario.


И еще пару строк насчет декомпозиции MVC. Современные фреймворки, где все типовые функции декомпозированы до предела, вполне себе естественным образом забрали у концептуальной MVC часть ответственностей по взаимодействию с внешним миром. Так, обработкой запроса пользователя занимаются специально обученные классы типа HTTP запрос и Роутер. В результате Контроллер получает не исходный запрос пользователя, а некоторое рафинированное действие, и это позволяет изолировать контроллер от специфики запроса. Аналогичным образом делается изоляция от специфики HTTP ответа, позволяя модулю MVC определять свой собственный тип ответа. Кроме того, фреймворки полностью реализовали две компоненты MVC — Модель домена и Шаблонизатор представления, впрочем, это мы уже обсуждали. Я все это к тому, что уточнение и конкретизация MVC идет постоянно и непрерывно, и это гуд.


Пример использования MVCS


А теперь давайте посмотрим, как пример "Толстого Кортроллера" в начале этой статьи может быть реализован в MVCS.


Начинаем с создания контроллера MVCS:


$mvcs = new MvcsController();

Контроллер MVCS получает запрос от внешнего роутера. Пусть роутер преобразует URI вида 'user/hello/XXX' в такие действие и параметры запроса:


$requestAction = 'user/hello';  // Действие запроса
$requestParams = ['XXX'];   // Параметры действия - ИД юзера

Учитывая, что контроллер MVCS принимает не URI, а сценарии, нам надо сопоставить действию запроса некоторый сценарий. Лучше всего это делать в контейнере MVCS:


// Определяем сценарий MVCS для URI запроса
$mvcs->set('scenarios', [
    'user/hello' => 'UserModel > UserViewModel > view, hello',
    ...,
]);

Давайте глянем на этот сценарий повнимательнее. Это цепь из трех преобразователей данных, разделенных знаком '>':


  • 'UserModel' — это имя Модели домена 'User', входом модели будут параметры запроса, выходом — собственно данные модели,
  • 'UserViewModel' — это имя Модели Представления, которая преобразует доменные данные в данные представления,
  • 'view, hello' — это системный шаблонизатор 'view' для PHP шаблона с именем 'hello'.

Теперь нам осталось только добавить в контейнер MVCS два задействованных в сценарии преобразователя как функции-замыкания:


// Модель домена UserModel
$mvcs->set('UserModel', function($id) {
    $users = [
        1 => ['first' => 'Иван', 'last' => 'Петров'],
        2 => ['first' => 'Петр', 'last' => 'Иванов'],
    ];
    return isset($users[$id]) ? $users[$id] : NULL;
});
// Модель представления UserViewModel
$mvcs->set('UserViewModel', function($user) {
    // Слепить данные для PHP шаблона типа: 'echo "Hello, $name!"';
    return ['name' => $user['first'].' '.$user['last']];
});

И это все! Для каждого запроса надо определить соответствующий сценарий и все его сцены (кроме системных, таких, как 'view'). И ничего более.


А теперь мы готовы протестировать MVCS для разных запросов:


// Получить из контейнера сценарий для текущего запроса
$scenarios = $mvcs->get('scenarios');
$scenario = $scenarios[$requestAction];

// Исполнить сценарий с параметрами текущего запроса...

// Для запроса 'user/hello/1' получим 'Иван Петров' декорированный шаблоном 'hello'
$requestParams = ['1'];
$response = $mvcs->play($scenario, $requestParams);

// Для запроса 'user/hello/2' получим 'Петр Иванов' декорированный шаблоном 'hello'
$requestParams = ['2'];
$response = $mvcs->play($scenario, $requestParams);

PHP реализация MVCS размещена на github.com.
Этот пример находится в директории example MVCS.
Tags:mvcmodelviewcontrollerframeworkphp
Hubs: Website development PHP Laravel
Total votes 11: ↑7 and ↓4 +3
Views6.2K

Popular right now

Комплексное обучение PHP
August 16, 202120,000 ₽Loftschool
Основы вёрстки сайтов
June 28, 202120,000 ₽Loftschool
Node.js: серверный JavaScript
June 28, 202127,000 ₽Loftschool
Веб-дизайнер
June 28, 202183,000 ₽GeekBrains
SMM-менеджер
June 28, 202196,900 ₽Нетология

Top of the last 24 hours