Markdown — упрощенный язык разметки, который удобен при работе с текстом (в отличие от HTML). Браузеры не умеют отображать Markdown напрямую, поэтому он транслируется в HTML и уже затем показывается. Трансляция Markdown в HTML описывается чистой функцией. Она не зависит от внешнего окружения, детерминирована и не порождает побочных эффектов.
html = markdownToHtml(markdown);
На входе текст (в формате Markdown), на выходе — тоже текст (в формате HTML). Если нужно изменить поведение трансляции, то достаточно передать вторым параметром объект опций.
// Вторым параметром передаем опции трансляции
// `sanitize` — флаг, отвечающий за включение безопасного рендеринга.
// Если его выключить, то теги `<script>` вставленные в Markdown отобразятся как есть.
const html = markdownToHtml(markdown, { sanitize: false });
Теперь давайте вообразим объектно-ориентированную версию этого кода. Перед тем, как двигаться дальше, попробуйте отвлечься от чтения и подумайте над следующими вопросами:
- Что мы вообще хотим получить такого от ООП, чего не дает нам чистая функция?
- Как будет выглядеть получившийся интерфейс?
Как вы помните, классы позволяют реализовать абстракцию. Можно ли сказать, что в процессе преобразования Markdown в HTML есть абстракция? Нет. Абстракция подразумевает наличие некоторого понятия (типа), значения которого обладают временем жизни. Это значит, что она создается и затем многократно и по-разному используется. Например, невозможно представить работу с пользователем в виде одной функции. Если говорить о Markdown, то конкретный текст этого формата не интересует нас сам по себе, мы не определяем над ним некоторый набор операций и не собираемся им активно пользоваться. Все, что мы хотим, прямо здесь и сейчас (в том коде) - получить HTML и забыть про Markdown.
Если бы мы хотели построить вокруг текста абстракцию, то код выглядел бы так:
// Объект md описывает собой переданный текст markdown и позволяет им манипулировать
const md = new Markdown(markdown);
const html = md.render();
В примере выше тип Markdown представляет собой абстракцию над текстом в формате Markdown. Смысла в таком коде мало, а вот проблем он доставит. Эти две строчки начнут неразрывно встречаться в каждом месте, в котором требуется получить HTML. Объект md
становится сразу не нужен, как только получен HTML, у него нет времени жизни. Такой антипаттерн особенно часто встречается у новичков. Загвоздка здесь именно в том, чтобы разобраться, где у нас абстракция данных, а где нет.
// Типичный избыточный код в том месте, где абстракцию сделали, но она не нужна
const md1 = new Markdown(markdown1);
const html1 = md1.render();
// Еще раз для закрепления
const md2 = new Markdown(markdown2);
const html2 = md2.render();
Существует формальное правило, позволяющее это определить. Если создание объекта и вызов метода можно заменить на обычную функцию, то ни о какой абстракции речи не идет, и правильный подход, в данной ситуации, сводится к переносу данных из конструктора в сам метод.
const md = new Markdown();
// очень важно, чтобы render оставался чистой функцией и не сохранял markdown внутри объекта
const html1 = md.render(markdown1);
const html2 = md.render(markdown2);
В этом коде класс Markdown — тип, относящийся к транслятору, а не к тексту. У такого объекта жизненный цикл шире, чем ожидание однократного вызова функции render()
(как в предыдущем случае). Он может (и должен) переиспользоваться столько раз, сколько потребуется. Для этого важно оставить функцию render()
чистой и не менять состояние объекта между вызовами.
Тогда становится непонятно, зачем здесь вообще объект. И на это есть 2 причины.
- Полиморфизм подтипов. Разберем в последующих курсах.
- Вторая и главная причина (для данного случая) — Конфигурация.
Разберем последний пункт подробнее. Представьте что Markdown на проекте используется повсеместно (на Хекслете очень часто) и код генерации HTML выглядит так:
// В одном месте
const html1 = markdownToHtml(markdown1, { sanitize: true });
// Где-то в другом месте
const html2 = markdownToHtml(markdown2, { sanitize: true });
Чем больше возникает таких мест, тем больше дублируется передача опций. Изменение поведения потребует переписывания всех мест вызова этой функции. Логичным шагом было бы задать опции в одном месте и затем их переиспользовать.
// В одном месте
const html1 = markdownToHtml(markdown1, options);
// Где-то в другом месте
const html2 = markdownToHtml(markdown2, options);
Использование объекта позволяет убрать явную передачу (про которую легко забыть). Суть этого паттерна заключается в конфигурировании. То есть объект в данном случае выступает в роли контейнера, содержащего опции для Markdown, которые применяются при рендеринге, что позволяет их не передавать каждый раз.
const md = new Markdown({ sanitize: true });
const html1 = md.render(markdown1);
const html2 = md.render(markdown2);
Под конфигурированием всегда понимается передача опций (различных настроек, необходимых данной библиотеке) в конструктор во время создания объекта. Особенно полезной такая конфигурация становится тогда, когда объект создается в одном месте программы (на этапе инициализации приложения), а используется в других местах. Возможность конфигурации не навязывает саму конфигурацию. Как правило, подобные объекты можно создавать и без указания чего-либо, тогда поведение остается "дефолтным", но смысл от этого не меняется.
const md = new Markdown();
const html = md.render(markdown);
Другой пример — это популярная библиотека для генерации различных данных @faker-js/faker. Она позволяет создать объект, который сохранит базовую конфигурацию:
import { Faker, ru } from '@faker-js/faker';
const faker = new Faker({ locale: ru });
console.log(`Привет, ${faker.person.fullName()}!`); // => Привет, Элеонора Авдеева!
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.