В объектно-ориентированном программировании одна из ключевых проблем — это управление конфигурацией. Допустим, у вас есть функция, которая выполняет задачу и принимает различные параметры или опции. Если эта функция вызывается в нескольких местах приложения, то может возникнуть необходимость изменить ее поведение. Именно эту проблему мы разберем в данном уроке.
Трансляция Markdown в HTML
Рассмотрим простую функцию, которая переводит текст в формате Markdown в HTML.
Markdown — упрощенный язык разметки, который удобен при работе с текстом в отличие от HTML. Браузеры не умеют отображать Markdown напрямую, поэтому он транслируется в HTML и уже затем показывается.
Функция перевода Markdown в HTML не зависит от внешнего окружения, детерминирована и не создает побочных эффектов. На входе текст в формате Markdown, на выходе — тоже текст, но в формате HTML. Если нужно изменить поведение трансляции, то достаточно передать дополнительные опции.
Вот пример такой функции, которая преобразует текст из формата Markdown в HTML:
from markdown import markdown
html = markdown(markdown_text)
В этом примере кода мы используем функцию markdown
из библиотеки markdown
, чтобы преобразовать текст в формате Markdown в HTML.
Если мы хотим настроить поведение трансляции, мы можем передать дополнительные опции функции:
html = markdown(markdown_text, output_format="html5")
Здесь мы передаем дополнительный параметр output_format
в функцию markdown
, чтобы изменить формат вывода.
Теперь представим, что было бы, если бы мы решили применить объектно-ориентированный подход к этой проблеме.
Применение ООП
Перед тем, как двигаться дальше, подумаем над следующими вопросами:
- Что мы хотим получить от ООП, чего не дает чистая функция?
- Как будет выглядеть получившийся интерфейс?
Классы позволяют реализовать абстракцию. Это является ключевым преимуществом ООП перед функциональным подходом. С другой стороны, абстракция может быть сложной и привести к избыточности кода.
Рассмотрим пример класса, который используется для конвертации текста из формата Markdown в HTML, чтобы понять, как это работает и как будет выглядеть получившийся интерфейс.
Например, невозможно представить работу с пользователем в виде одной функции. Если говорить о Markdown, то конкретный текст этого формата не интересует нас сам по себе. Мы не определяем над ним некоторый набор операций и не собираемся им активно пользоваться. Мы хотим получить HTML и забыть про Markdown.
Если бы мы хотели построить вокруг текста абстракцию, то код выглядел бы так:
class MarkdownToHTML:
def __init__(self, markdown_text):
self.markdown_text = markdown_text
def render(self):
return markdown(self.markdown_text)
md = MarkdownToHTML(markdown_text)
html = md.render()
В этом примере мы создали класс MarkdownToHTML
, который принимает текст в формате Markdown и имеет метод render()
, транслирующий этот текст в HTML.
Использование объектов может привести к проблемам, когда код становится избыточным:
md1 = MarkdownToHTML(markdown1)
html1 = md1.render()
md2 = MarkdownToHTML(markdown2)
html2 = md2.render()
Здесь нам приходится для каждого блока Markdown, который мы хотим преобразовать в HTML, создавать новый объект MarkdownToHTML
. Это может быть избыточно и приведет к дублированию кода.
Но существует формальное правило, позволяющее это определить. Если создание объекта и вызов метода можно заменить на обычную функцию, то ни о какой абстракции речи не идет. Правильный подход в этом случае сводится к переносу данных из конструктора в сам метод:
md = MarkdownToHTML()
# Важно, чтобы render оставался чистой функцией и не сохранял markdown внутри объекта
html1 = md.render(markdown1)
html2 = md.render(markdown2)
Здесь мы преобразовали класс MarkdownToHTML
так, чтобы он представлял собой абстракцию не над текстом Markdown, а над транслятором Markdown в HTML. Это значит, что жизненный цикл такого объекта стал шире, чем ожидание однократного вызова функции render()
.
В нашем примере объект md
должен переиспользоваться столько раз, сколько потребуется. Для этого важно оставить функцию render()
«чистой» — она не должна менять состояние объекта между вызовами.
Стоит отметить, что мы столкнулись здесь с двумя важными концепциями:
- Полиморфизм подтипов, который мы рассмотрим в последующих курсах
- Конфигурация, которая является ключевым моментом для данного случая
Представим, что в вашем проекте Markdown используется повсеместно, и код для генерации HTML в разных местах выглядит примерно так:
# В одном месте
html1 = markdown_to_html(markdown1, sanitize=True)
# В другом месте
html2 = markdown_to_html(markdown2, sanitize=True)
При таком подходе в каждом месте вызова функции markdown_to_html
мы дублируем передачу параметра sanitize
. Если бы нам потребовалось изменить поведение этой функции, мы были бы вынуждены переписать все места, где она вызывается. Более логичным было бы задать параметры один раз и затем их переиспользовать.
Использование объекта позволяет убрать явную передачу (про которую легко забыть). Суть этого паттерна заключается в конфигурировании. То есть объект в данном случае выступает в роли контейнера, содержащего опции для Markdown, которые применяются при рендеринге, что позволяет их не передавать каждый раз.
options = {'sanitize': True}
md = MarkdownToHTML(**options)
html1 = md.render(markdown1)
html2 = md.render(markdown2)
Здесь мы создали объект класса MarkdownToHTML
, передав в его конструктор параметр sanitize
. Этот объект затем можно использовать для преобразования текста Markdown в HTML с заданными параметрами.
Под конфигурированием мы понимаем передачу опций — различных настроек, необходимых данной библиотеке — в конструктор во время создания объекта. Эта возможность особенно полезна, когда объект создается в одном месте программы, например, на этапе инициализации приложения, а используется в других местах.
Возможность конфигурации не обязывает ее использовать. Мы всегда можем создать объект и без указания каких-либо параметров. В таком случае его поведение будет по умолчанию:
md = MarkdownToHTML()
html = md.render(markdown)
Так использование объектов вместо функций дает нам больше гибкости и контроля над настройками и поведением кода.
Выводы
В этом уроке мы увидели, как использование объектно-ориентированного подхода может помочь нам управлять конфигурацией в приложениях. Показанные принципы применимы к любому объектно-ориентированному языку. Основной идеей является инициализация объектов с опциями или настройками, которые затем можно использовать при вызове методов этих объектов. Это позволяет уменьшить дублирование кода и сделать приложение более гибким и настраиваемым.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.