Зарегистрируйтесь, чтобы продолжить обучение

Конфигурация PHP: Объектно-ориентированный дизайн

Markdown — упрощённый язык разметки, который удобен при работе с текстом (в отличие от HTML). Браузеры не умеют отображать Markdown напрямую, поэтому он конвертируется в HTML и уже затем показывается. Конвертация Markdown в HTML описывается чистой функцией. Она не зависит от внешнего окружения, детерминирована и не порождает побочных эффектов.

<?php

$html = markdownToHtml($markdown);

На входе текст (в формате Markdown), на выходе — тоже текст (в формате HTML). Если нужно изменить поведение конвертации, то достаточно передать вторым параметром массив опций.

<?php

$html = markdownToHtml($markdown, ['sanitize' => false]);

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

  • Что мы вообще хотим получить такого от ООП, чего не даёт нам чистая функция?
  • Как будет выглядеть получившийся интерфейс?

Как вы помните, классы позволяют реализовать абстракцию. Можно ли сказать, что в процессе преобразования Markdown в HTML есть абстракция? Нет. Абстракция подразумевает наличие некоторого понятия (типа), значения которого обладают временем жизни. Это значит, что она создается и затем многократно и по-разному используется. Например, невозможно представить работу с пользователем в виде одной функции. Если говорить о Markdown, то конкретный текст этого формата не интересует нас сам по себе, мы не определяем над ним некоторый набор операций и не собираемся им активно пользоваться. Все, что мы хотим, прямо здесь и сейчас (в том коде) - получить HTML и забыть про Markdown.

Если бы мы хотели построить вокруг текста абстракцию, то код выглядел бы так:

<?php

// Объект md описывает собой переданный текст $markdown и позволяет им манипулировать
$md = new Markdown($markdown);
$html = $md->render();

В примере выше тип Markdown представляет собой абстракцию над текстом в формате Markdown. Смысла в таком коде мало, а вот проблем он доставит. Эти две строчки начнут неразрывно встречаться в каждом месте, в котором требуется получить HTML. Объект $md становится сразу не нужен, как только получен HTML, у него нет времени жизни. Такой антипаттерн особенно часто встречается у новичков. Загвоздка здесь именно в том, чтобы разобраться, где у нас абстракция данных, а где нет.

<?php

// Типичный избыточный код в том месте, где абстракцию сделали, но она не нужна
$md1 = new Markdown($markdown1);
$html1 = $md1->render();

// Еще раз для закрепления
$md2 = new Markdown($markdown2);
$html2 = $md2->render();

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

<?php

$md = new Markdown();
// очень важно, чтобы render оставался чистой функцией и не сохранял $markdown внутри объекта
$html1 = $md->render($markdown1);
$html2 = $md->render($markdown2);

В этом коде класс Markdown — тип, относящийся к транслятору, а не к тексту. У такого объекта жизненный цикл шире, чем ожидание однократного вызова функции render (как в предыдущем случае). Он может (и должен) переиспользоваться столько раз, сколько потребуется. Для этого важно оставить функцию render чистой и не менять состояние объекта между вызовами.

Тогда становится непонятно, зачем здесь вообще объект. И на это есть 3 причины.

  1. Идиоматика. В PHP, как и в Java, принято практически всё оформлять в виде классов. К тому же для них работает автозагрузка.
  2. Полиморфизм подтипов. Разберём в последующих курсах.
  3. Третья и главная причина (для данного случая) — Конфигурация.

Разберём последний пункт подробнее. Представьте что Markdown на проекте используется повсеместно (на Хекслете очень часто) и код генерации HTML выглядит так:

<?php

// В одном месте
$html1 = markdownToHtml($markdown1, ['sanitize' => true]);

// Где-то в другом месте
$html2 = markdownToHtml($markdown2, ['sanitize' => true]);

Чем больше возникает таких мест, тем больше дублируется передача опций. Изменение поведения потребует переписывания всех мест вызова этой функции. Логичным шагом было бы задать опции в одном месте и затем их переиспользовать.

<?php

// В одном месте
$html1 = markdownToHtml($markdown1, $options);

// Где-то в другом месте
$html2 = markdownToHtml($markdown2, $options);

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

<?php

$md = new Markdown(['sanitize' => true]);
$html1 = $md->render($markdown1);
$html2 = $md->render($markdown2);

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

<?php

$md = new Markdown();
$html = $md->render($markdown);

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

Попробуйте проверить себя. Выполнение HTTP-запроса это абстракция данных или нет?

<?php

$client = new \GuzzleHttp\Client();
$res = $client->get('https://api.github.com/repos/guzzle/guzzle');
echo $res->getStatusCode();

Данный приём не является прерогативой классов и объектов. В функциональных языках (и в JS) он крайне просто реализуется через замыкание


Дополнительные материалы

  1. Библиотека CommonMark

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Laravel
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»