Как использовать фабричный метод при написании кода на Python

4
7042
Добавить в избранное

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

Знакомство с фабричным методом

Фабричный метод − это шаблон проектирования, используемый для создания общего интерфейса.

Например, приложению требуется объект с определенным интерфейсом для выполнения задач. Реализация интерфейса определяется некоторым параметром.

Вместо использования сложной структуры из условий if/elif/else для определения реализации, приложение делегирует это решение отдельному компоненту, который создает конкретный объект. При таком подходе код приложения упрощается, становится более удобным для повторного использования и поддержки.

Представьте себе приложение, которому необходимо преобразовать объект Song в String. Преобразование объекта называется сериализацией. Эти требования часто реализованы в одной функции или методе, которые содержат всю логику:

В приведенном выше примере есть базовый класс Song для представления песни и класс SongSerializer, который преобразовывает объект Song в его строковое представление в соответствии со значением параметра format.

Метод .serialize() поддерживает два разных формата: JSON и XML. Любой другой указанный формат не поддерживается, поэтому возникает исключение ValueError.

Воспользуемся интерактивной оболочкой Python, чтобы увидеть, как работает код:

Создаются объект Song и serializer, затем Song преобразуется в строковое представление с помощью метода .serialize(). Метод принимает в качестве параметров объект Song и строковое значение. Последний вызов использует YAML в качестве формата, который не поддерживает serializer, поэтому возникает исключение ValueError.

Это короткий и упрощенный пример, но в нем есть три логических ветви исполнения, которые зависят от значения параметра format и усложняют код.

Проблемы сложного кода с условиями

Как использовать фабричный метод при написании кода на Python

Пример выше раскрывает проблемы, с которыми вы столкнетесь в сложном логическом коде. Структуры if/elif/else используются для изменения поведения приложения, но они усложняют чтение, восприятие и поддержку.

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

Метод .serialize() в SongSerializer может изменяться по многим причинам. Такое поведение способно привести к появлению проблем. Давайте рассмотрим все возможные ситуации, когда придется вносить изменения в реализацию:

  • Представлен новый формат: нужно вносить изменения в метод, чтобы имплементировать сериализацию в данный формат.
  • Изменяется объект Song: добавление или удаление свойств класса Song потребует изменения реализации для размещения новой структуры.
  • Изменяется строковое представления формата (простой JSON и JSON API): необходимо изменять метод .serialize() вместе со строковым представлением формата, потому что представление жестко запрограммировано в реализации метода .serialize().

В идеале, любое изменение вносится без использования метода .serialize().

В поисках общего интерфейса

Если вы видите сложный код с условиями, определите общие цели каждого логического ответвления.

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

Нужно найти общий интерфейс, который можно использовать для замены каждой ветви. В приведенном выше примере требуется такой интерфейс, который принимает объект Song и возвращает строку.

Когда есть общий интерфейс, мы предоставляем отдельные реализации для каждого способа.

В примере выше мы сначала осуществляем сериализацию в JSON и XML, а затем предоставляем отдельный компонент, который решает, какую реализацию использовать на основе указанного формата. Этот компонент оценивает значение format и возвращает конкретную реализацию, определенную его значением.

В следующих разделах вы узнаете, как вносить изменения в существующий код без изменения поведения − проводить рефакторинг кода.

Рефакторинг кода в желаемый интерфейс

Желаемый интерфейс − это объект или функция, которая принимает объект Song и возвращает строковое представление.

Первым шагом является рефакторинг одного из логических ответвлений в этот интерфейс. Добавляем новый метод ._serialize_to_json() и перемещаем в него код сериализации JSON. Затем изменяем клиент для вызова, вместо реализации в теле оператора if:

После внесения этих изменений вы сможете убедиться, что поведение осталось прежним. Затем делаем то же самое для XML, представляя новый метод ._serialize_to_xml(), перемещая реализацию в него и изменяя ветку elif для вызова.

В следующем примере показан переработанный код:

Новая версия воспринимается проще, но ее можно улучшать дальше с помощью базовой реализации фабричного метода.

Базовая реализация фабричного метода

Главная идея фабричного метода заключается в том, чтобы предоставить отдельному компоненту ответственность за решение, какую реализацию следует использовать на основе определенного параметра. Этим параметром в нашем примере является format.

Чтобы завершить реализацию фабричного метода, вы добавляете новый метод ._get_serializer(), который принимает желаемый формат. Он оценивает значение format и возвращает соответствующую функцию сериализации:

Теперь можно изменить метод .serialize() в SongSerializer для использования ._get_serializer(), чтобы завершить реализацию фабричного метода. Вот так:

Окончательная реализация показывает различные компоненты фабричного метода.

Это − клиентский компонент шаблона. Определенный интерфейс называется компонентом-продуктом, в нашем случае продукт − это функция, которая принимает Song и возвращает строковое представление.

Как использовать фабричный метод при написании кода на Python

Методы ._serialize_to_json() и ._serialize_to_xml() являются конкретными реализациями продукта.

Наконец, метод ._get_serializer() является компонентом-создателем. Создатель решает, какую реализацию использовать.

Поскольку вы начали с уже существующего кода, все компоненты фабричного метода являются частями класса SongSerializer.

Обычно это не так, как видно, ни один из добавленных методов не использует параметр self. Это признак того, что они не должны быть методами класса SongSerializer, а могут стать внешними функциями:

Механика фабричного метода всегда одинакова. Клиент зависит от конкретной реализации интерфейса. Он запрашивает реализацию от компонента-создателя, используя какой-то идентификатор.

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

Вы можете выполнить тот же набор инструкций в интерактивном интерпретаторе Python, чтобы убедиться, что поведение приложения не изменилось:

Вы создаете Song и serializer и используете serializer для преобразования Song в строковое представление с указанием формата. Поскольку YAML не является поддерживаемым форматом, появляется ValueError.

Предпосылки для использования фабричного метода

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

Существует широкий спектр похожих задач, поэтому давайте рассмотрим примеры.

Замена сложного логического кода: сложные логические структуры if/elif/else трудно поддерживать, поскольку при изменении требований необходимы новые логические ветки.

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

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

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

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

Объединение похожих функций в общем интерфейсе. В соответствии с примером обработки изображений приложение должно применить фильтр. Необходимый фильтр может быть идентифицирован с помощью пользовательского ввода, а фабричный – предоставить реализацию фильтра.

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

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

Заключение

Фабричный метод − это широко используемый шаблон проектирования, который можно использовать в ситуациях с несколькими реализациями интерфейса.
Шаблон удаляет сложный логический код, который трудно поддерживать, и заменяет его на конструкцию, которую можно использовать повторно и расширять. Он предотвращает модификацию существующего кода для поддержки новых требований.
Это важно, потому что правка существующего кода может привести к изменениям в поведении и багам.

Что еще почитать

Design Patterns: Elements of Reusable Object-Oriented Software − набор распространённых шаблонов проектирования.

Heads First Design Patterns: A Brain-Friendly Guide − книга о принципах шаблонов проектирования.

Понравился материал о том, как применять фабричный метод? Другие материалы по теме:

Источник: Фабричный метод и как его применять при разработке на Python on Real Python

Интересуетесь программированием на Python?

Подпишитесь на нашу рассылку, чтобы получать больше интересных материалов:

И не беспокойтесь, мы тоже не любим спам. Отписаться можно в любое время.




Комментариев: 4

  1. Имхо лучше использовать методы под отдельную задачу serialized_json(file_path: str), serialized_xml(file_path: str), etc.
    Чем меньше пользователю нужно вводить параметров тем лучше.

  2. Фёдор Лянгузов

    Гораздо проще и удобнее использовать в таких случаях словарь. Достаточно заменить класс SongSerializer и функцию get_serializer на:

    SongSerializer = {
    ‘JSON’: _serialize_to_json,
    ‘XML’: _serialize_to_xml,
    }

    Использование:

    >>> song = Song(‘1’, ‘Water of Love’, ‘Dire Straits’)
    >>> SongSerializer[‘JSON’](song)
    ‘{«id»: «1», «title»: «Water of Love», «artist»: «Dire Straits»}’
    >>> SongSerializer[‘XML’](song)
    ‘Water of LoveDire Straits’
    >>> SongSerializer[‘YAML’](song)
    Traceback (most recent call last):
    File «», line 1, in
    SongSerializer[‘YAML’](song)
    KeyError: ‘YAML’

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

    Удобнее:
    1) Не надо создавать объект класса SongSerializer. Впрочем, здесь и так не стоило его создавать, достаточно отметить декоратором staticmethod метод serialize, или вообще не писать класс (а зачем?)
    2) Такой код не требуется изменять, только добавлять. Например, если появится новый формат, не требуется изменять ЭТОТ модуль, можно в новом модуле написать sd.SongSerializer[‘YAML’] = serialize_to_yaml
    3) Избавляемся от if/elif/else совсем. Порядок следования теперь даже теоретически не влияет на производительность.
    4) Легко можно обобщить на все классы в проекте, если в качестве ключей использовать кортежи (CLASS, FORMAT), например (Song, ‘YAML’)
    5) Исчезает проблема с наследованием: сейчас, если появится песня с несколькими исполнителями, необходимо будет создать новый класс-наследник Song, тогда придется писать и наследника класса SongSerializer, в этом наследнике будет опять написан if, а также super().serialize(). В моем коде достаточно добавить в словарь (SongMultiple, ‘JSON’): _serialize_multiple_to_json в любом модуле, одной (двумя с импортом) строчкой.
    6) Исчезает проблема с максимальным количеством функций на стеке: при вызове serialize у наследника на стек попадает и функция потомка, и функция родителя (потому что она по логике должна вызываться через super().serialize). Максимум стека по умолчанию — 1000 функций. От двух избавились

    1. Почему же функция предка должна вызываться через super().serialize? self.serialize() же.

  3. Если бы снова начали мусолить шаблоны проектирования на JS — ябпошутил, а так на пифоне — это даже не смешно…

Добавить комментарий