К сожалению, PHP славится спагетти-кодом, но примеры логики запутывания являются продуктом программиста, а не языка.
Одним из антишаблонов, которых мы научились избегать, является запутывание логики предметной области и логики представления. Логика домена состоит из моделей данных и поведения, таких как классы User, Group и ForumPost и базы данных для их сохранения; Логика представления касается создания вывода в форме HTML, JSON или любого другого формата. Более того, анализ HTTP-запроса и его заголовков также является частью верхнего уровня приложения, который должен объединять всю логику представления.
Рефакторинг « Отдельный домен от презентации» пытается отвлечь вас от подхода «SQL-и-HTML-в-том-же-сценарии» для нацеливания на архитектурный шаблон, такой как многоуровневая структура или MVC2.
Но я использую фреймворк, он «разделяет проблемы» для меня
Даже при использовании инфраструктуры MVC (иногда называемой MVC2 в его веб-версии), не обязательно, что логика домена и представления фактически разделена.
Фреймворки обычно вынуждают вас писать контроллеры и представления, и этот шаг вынуждает отделить шаблонную форму ответа (создание HTML) от выполнения запроса (изменение или получение состояния приложения, обычно с сеансом или базой данных в качестве серверной части).
Однако платформы не учитывают разделение моделей от логики контроллера, продиктованной MVC. Причина, вероятно, заключается в том, что выбор между возможными моделями и их постоянством настолько различен, что среда предоставляет базовые классы для контроллеров и представлений, но не для моделей. Результатом обычно является гигантский класс контроллера, содержащий всю логику приложения: это решение объединяет проблемы HTTP и уровня приложения, такие как параметры и аутентификация, с логикой, специфичной для домена, например, как вычисляются самые последние публикации или какую информацию должен предоставить пользователь для того, чтобы зарегистрироваться.
Так почему разделение?
Первая причина — это большая сплоченность ваших классов и пространств имен: конструкции, которые изменяются вместе, должны оставаться вместе в классе или папке. В современном apporach, когда вы меняете способ выбора сообщения для запроса, вы меняете только класс сообщений; когда вы меняете разметку, вы меняете только шаблон.
Это не означает, что нет эволюций, которые даже лучше разделяют проблемы по вертикали (между объектами и полями), а не только по горизонтали (между слоями).
Но хотя бы отделяя разметку от всего остального, если первый шаг.
Вторая причина — тестируемость . Если вы можете изолировать части вашего дизайна, вы можете проверить их сами; Модульные тесты проще в настройке из-за уменьшенной области видимости и более детерминированы (следовательно, автоматизируемы), поскольку у вас нет зависимости от глобальных переменных, таких как дата или случайные числа. Представьте, что вы устанавливаете часы на нужное время, проверьте, что в некоторых ситуациях код верен …
Результат разделения интересов означает, что вы можете:
- проверить контроллер или действие без Apache, но только с PHPUnit.
- проверить постоянство модели с поддельной базой данных, заполнив таблицы SQLite.
- протестируйте шаблон (если хотите), не заполняя таблицы.
меры
- Определите назначение запутанной страницы и создайте для нее класс домена . Различные операции могут быть смоделированы как разные методы для одного и того же объекта (например, Репозиторий, удовлетворяющий каждому запросу относительно пользователей), если у вас уже есть много классов домена.
- Переместите логику из текущего места в класс домена .
- Извлеките файл шаблона или View Helper для генерации HTML (или JSON или из чего состоит ваш вывод).
- После второго движения логика внутри исходной страницы или действия должна быть сосредоточена вокруг HTTP-запроса и в соединении других объектов. Это элементарный шаблон MVC для Интернета.
пример
Мы работаем над сценарием .php, для простоты; цель состоит в том, чтобы разделить его на модель, представление и контроллер. Тот же пример можно применить к действию контроллера, встроенному в Symfony или Zend Framework.
Этот скрипт печатает первый не прочитанный пост в ветке, помеченной администраторами как липкая; он возвращает его как фрагмент HTML, который может быть включен клиентами.
Существует необязательный параметр date_visit date: если он присутствует, это означает, что сообщения до этой даты не должны рассматриваться. Если этот параметр отсутствует, это означает, что информация о текущем пользователе отсутствует, поэтому следует выбрать последнее сообщение.
Этот MySQL содержит данные о сообщениях:
mysql> SELECT * FROM posts; +----+-----------+---------+----------------+------------+ | id | id_thread | author | text | date | +----+-----------+---------+----------------+------------+ | 1 | 23 | giorgio | My new post... | 2012-02-13 | | 2 | 23 | giorgio | My old post... | 2012-01-01 | +----+-----------+---------+----------------+------------+ 2 rows in set (0.00 sec)
<?php $dsn = 'mysql:host=localhost;dbname=practical_php_refactoring'; $username = 'root'; $password = ''; $dbh = new PDO($dsn, $username, $password); $stmt = $dbh->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date"); $stmt->bindValue(':id_thread', (int) $_GET['id_thread']); if (!isset($_GET['last_visit'])) { $_GET['last_visit'] = date('Y-m-d'); } $stmt->bindValue(':date', $_GET['last_visit']); $stmt->execute(); $post = $stmt->fetch(); ?> <div class="post"> <div class="author"><?php echo $post['author']; ?></div> <div class="date"><?php echo $post['date']; ?></div> <div class="text"><?php echo $post['text']; ?></div> </div>
Мы извлекаем модель для постов, но поскольку каждая из них сама по себе имеет очень мало логики (пост не имеет состояний в этой модели или проверки при создании / обновлении), речь идет в основном об инкапсуляции запросов (шаблон репозитория).
<?php class Posts { public function __construct(PDO $connection) { $this->connection = $connection; } /** * @param int $threadId * @param string $lastVisit Y-m-d format * @return array fields for the selected post */ public function lastPost($threadId, $lastVisit) { $stmt = $this->connection->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date"); $stmt->bindValue(':id_thread', (int) $_GET['id_thread']); $stmt->bindValue(':date', $lastVisit); $stmt->execute(); return $stmt->fetch(); } } $dsn = 'mysql:host=localhost;dbname=practical_php_refactoring'; $username = 'root'; $password = ''; $posts = new Posts(new PDO($dsn, $username, $password)); if (!isset($_GET['last_visit'])) { $_GET['last_visit'] = date('Y-m-d'); } $post = $posts->lastPost($_GET['id_thread'], $_GET['last_visit']); ?> <div class="post"> <div class="author"><?php echo $post['author']; ?></div> <div class="date"><?php echo $post['date']; ?></div> <div class="text"><?php echo $post['text']; ?></div> </div>
В реальной жизни вы, вероятно, создадите EntityManager или другой Фасад для ORM вместо прямого доступа к PDO. Обратите внимание, что классы, показанные здесь, хранятся в исходном файле ради отдельного примера, но они должны быть перемещены в собственный исходный файл и загружены автоматически.
Теперь мы извлекаем контроллер, но на самом деле это всего лишь одно действие возможного контроллера. Мы назовем класс Action, чтобы отразить этот факт.
<?php class Posts { public function __construct(PDO $connection) { $this->connection = $connection; } /** * @param int $threadId * @param string $lastVisit Y-m-d format * @return array fields for the selected post */ public function lastPost($threadId, $lastVisit) { $stmt = $this->connection->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date"); $stmt->bindValue(':id_thread', (int) $_GET['id_thread']); $stmt->bindValue(':date', $lastVisit); $stmt->execute(); return $stmt->fetch(); } } class LastPostAction { public function __construct(Posts $posts, $template) { $this->posts = $posts; $this->template = $template; } public function execute(array $getParameters) { if (!isset($getParameters['last_visit'])) { $getParameters['last_visit'] = date('Y-m-d'); } $post = $this->posts->lastPost($getParameters['id_thread'], $getParameters['last_visit']); require $this->template; } } $dsn = 'mysql:host=localhost;dbname=practical_php_refactoring'; $username = 'root'; $password = ''; $action = new LastPostAction( new Posts(new PDO($dsn, $username, $password)), 'last_post.php' ); $action->execute($_GET); ?>
Вместе с контроллером мы также извлекли шаблон, отделив презентацию от всего остального. Поскольку PHP уже является языком шаблонов, результат довольно чистый.
<div class="post"> <div class="author"><?php echo $post['author']; ?></div> <div class="date"><?php echo $post['date']; ?></div> <div class="text"><?php echo $post['text']; ?></div> </div>