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

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

В этой статье рассматривается шаблон проектирования Factory Method и его реализация в Python. Паттерны проектирования стали популярной темой в конце 90-х после того, как так называемая «Банда четырех» (GoF: Gamma, Helm, Johson и Vlissides) опубликовала свою книгуDesign Patterns: Elements of Reusable Object-Oriented Software.

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

Книга GoF описывает фабричный метод как шаблон творческого проектирования. Шаблоны проектирования для творчества связаны с созданием объектов, а Factory Method - это шаблон проектирования, который создает объекты с общим интерфейсом.

Это повторяющаяся проблемаmakes Factory Method one of the most widely used design patterns, и очень важно понимать ее и знать, как ее применять.

By the end of this article, you will:

  • Понять компоненты фабричного метода

  • Узнайте о возможностях использования Factory Method в ваших приложениях

  • Научитесь изменять существующий код и улучшать его дизайн, используя шаблон

  • Научитесь определять возможности, где Factory Method является подходящим шаблоном проектирования

  • Выберите подходящую реализацию Factory Method

  • Знать, как реализовать многоразовое решение общего назначения Factory Method

Free Bonus:5 Thoughts On Python Mastery, бесплатный курс для разработчиков Python, который показывает вам план действий и образ мышления, который вам понадобится, чтобы вывести свои навыки Python на новый уровень.

Представляем фабричный метод

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

Он отделяет процесс создания объекта от кода, который зависит от интерфейса объекта.

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

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

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

# In serializer_demo.py

import json
import xml.etree.ElementTree as et

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist


class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            song_info = {
                'id': song.song_id,
                'title': song.title,
                'artist': song.artist
            }
            return json.dumps(song_info)
        elif format == 'XML':
            song_info = et.Element('song', attrib={'id': song.song_id})
            title = et.SubElement(song_info, 'title')
            title.text = song.title
            artist = et.SubElement(song_info, 'artist')
            artist.text = song.artist
            return et.tostring(song_info, encoding='unicode')
        else:
            raise ValueError(format)

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

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

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

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 30, in serialize
    raise ValueError(format)
ValueError: YAML

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

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

Проблемы со сложным условным кодом

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

Код выше может показаться сложным для чтения или понимания, но подождите, пока не увидите окончательный код в этом разделе!

Тем не менее, приведенный выше код трудно поддерживать, потому что он делает слишком много. single responsibility principle указывает, что модуль, класс или даже метод должны иметь единственную четко определенную ответственность. Это должно сделать только одну вещь и иметь только одну причину, чтобы измениться.

Метод.serialize() вSongSerializer потребует изменений по разным причинам. Это увеличивает риск появления новых дефектов или нарушения существующих функций при внесении изменений. Давайте посмотрим на все ситуации, которые потребуют изменений в реализации:

  • When a new format is introduced: Метод необходимо изменить, чтобы реализовать сериализацию в этом формате.

  • When the Song object changes: Добавление или удаление свойств к классуSong потребует изменения реализации, чтобы приспособиться к новой структуре.

  • When the string representation for a format changes (plain JSON vs JSON API): Метод.serialize() должен измениться, если желаемое строковое представление формата изменится, потому что это представление жестко запрограммировано в реализации метода.serialize().

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

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

Первый шаг, когда вы видите сложный условный код в приложении, - это определение общей цели каждого из путей выполнения (или логических путей).

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

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

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

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

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

Мартин Фаулер в своей книгеRefactoring: Improving the Design of Existing Code определяет рефакторинг как «процесс изменения программной системы таким образом, чтобы не изменять внешнее поведение кода, но улучшать его внутреннюю структуру».

Давайте начнем рефакторинг кода, чтобы получить желаемую структуру, которая использует шаблон проектирования Factory Method.

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

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

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

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        # The rest of the code remains the same

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

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

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

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        elif format == 'XML':
            return self._serialize_to_xml(song)
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

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

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

Основная идея Factory Method - предоставить отдельному компоненту ответственность за решение о том, какую конкретную реализацию следует использовать на основе определенного параметра. В нашем примере этот параметр -format.

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

class SongSerializer:
    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

Note: Метод._get_serializer() не вызывает конкретную реализацию, а просто возвращает сам объект функции.

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

class SongSerializer:
    def serialize(self, song, format):
        serializer = self._get_serializer(format)
        return serializer(song)

    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

Окончательная реализация показывает различные компоненты Factory Method. Метод.serialize() - это код приложения, выполнение задачи которого зависит от интерфейса.

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

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

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

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

class SongSerializer:
    def serialize(self, song, format):
        serializer = get_serializer(format)
        return serializer(song)


def get_serializer(format):
    if format == 'JSON':
        return _serialize_to_json
    elif format == 'XML':
        return _serialize_to_xml
    else:
        raise ValueError(format)


def _serialize_to_json(song):
    payload = {
        'id': song.song_id,
        'title': song.title,
        'artist': song.artist
    }
    return json.dumps(payload)


def _serialize_to_xml(song):
    song_element = et.Element('song', attrib={'id': song.song_id})
    title = et.SubElement(song_element, 'title')
    title.text = song.title
    artist = et.SubElement(song_element, 'artist')
    artist.text = song.artist
    return et.tostring(song_element, encoding='unicode')

Note: Метод.serialize() вSongSerializer не использует параметрself.

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

Если вы удалитеSongSerializer и измените метод.serialize() на функцию, то вам придется изменить все местоположения в приложении, которые используютSongSerializer, и заменить вызовы новой функции.

Если у вас не очень высокий процент покрытия кода вашими модульными тестами, это не то изменение, которое вам следует делать.

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

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

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

>>>

>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.SongSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializer_demo.py", line 13, in serialize
    serializer = get_serializer(format)
  File "./serializer_demo.py", line 23, in get_serializer
    raise ValueError(format)
ValueError: YAML

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

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

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

Существует широкий спектр проблем, которые соответствуют этому описанию, поэтому давайте взглянем на некоторые конкретные примеры.

Replacing complex logical code: Сложные логические структуры в форматеif/elif/else трудно поддерживать, потому что по мере изменения требований требуются новые логические пути.

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

Параметр, оцениваемый в условиях, становится параметром для идентификации конкретной реализации. Пример выше представляет эту ситуацию.

Constructing related objects from external data: Представьте себе приложение, которому необходимо получить информацию о сотрудниках из базы данных или другого внешнего источника.

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

Supporting multiple implementations of the same feature: Приложению обработки изображений необходимо преобразовать спутниковое изображение из одной системы координат в другую, но для выполнения преобразования существует несколько алгоритмов с разными уровнями точности.

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

Combining similar features under a common interface: Следуя примеру обработки изображения, приложению необходимо применить фильтр к изображению. Конкретный фильтр для использования может быть идентифицирован с помощью некоторого пользовательского ввода, а Factory Method может предоставить конкретную реализацию фильтра.

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

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

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

Пример сериализации объекта

Основные требования для приведенного выше примера заключаются в том, что вы хотите сериализовать объектыSong в их представлениеstring. Похоже, что приложение предоставляет функции, связанные с музыкой, поэтому вполне вероятно, что приложению потребуется сериализовать другие типы объектов, напримерPlaylist илиAlbum.

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

Реализация интерфейса может выглядеть примерно так:

# In serializers.py

import json
import xml.etree.ElementTree as et

class JsonSerializer:
    def __init__(self):
        self._current_object = None

    def start_object(self, object_name, object_id):
        self._current_object = {
            'id': object_id
        }

    def add_property(self, name, value):
        self._current_object[name] = value

    def to_str(self):
        return json.dumps(self._current_object)


class XmlSerializer:
    def __init__(self):
        self._element = None

    def start_object(self, object_name, object_id):
        self._element = et.Element(object_name, attrib={'id': object_id})

    def add_property(self, name, value):
        prop = et.SubElement(self._element, name)
        prop.text = value

    def to_str(self):
        return et.tostring(self._element, encoding='unicode')

Note: В приведенном выше примере не реализован полный интерфейсSerializer, но он должен быть достаточно хорош для наших целей и для демонстрации фабричного метода.

ИнтерфейсSerializer является абстрактным понятием из-за динамической природы языкаPython. Статические языки, такие как Java или C #, требуют явного определения интерфейсов. В Python любой объект, который предоставляет требуемые методы или функции, как говорят, реализует интерфейс. В примере интерфейсSerializer определяется как объект, реализующий следующие методы или функции:

  • .start_object(object_name, object_id)

  • .add_property(name, value)

  • .to_str()

Этот интерфейс реализован конкретными классамиJsonSerializer иXmlSerializer.

В исходном примере использовался классSongSerializer. Для нового приложения вы реализуете что-то более общее, напримерObjectSerializer:

# In serializers.py

class ObjectSerializer:
    def serialize(self, serializable, format):
        serializer = factory.get_serializer(format)
        serializable.serialize(serializer)
        return serializer.to_str()

РеализацияObjectSerializer является полностью общей, и в качестве параметров упоминаются толькоserializable иformat.

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

Давайте посмотрим на конкретную реализацию интерфейсаserializable в классеSong:

# In songs.py

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist

    def serialize(self, serializer):
        serializer.start_object('song', self.song_id)
        serializer.add_property('title', self.title)
        serializer.add_property('artist', self.artist)

КлассSong реализует интерфейсSerializable, предоставляя метод.serialize(serializer). В этом методе классSong использует объектserializer для записи собственной информации без какого-либо знания формата.

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

До сих пор мы видели реализацию клиента (ObjectSerializer) и продукта (serializer). Настало время завершить реализацию Factory Method и предоставить создателю. Создателем в примере является переменнаяfactory вObjectSerializer.serialize().

Фабричный метод как объект Фабрика

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

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

Вы можете увидеть базовый интерфейсSerializerFactory в реализацииObjectSerializer.serialize(). Метод используетfactory.get_serializer(format) для полученияserializer из фабрики объектов.

Теперь вы реализуетеSerializerFactory для соответствия этому интерфейсу:

# In serializers.py

class SerializerFactory:
    def get_serializer(self, format):
        if format == 'JSON':
            return JsonSerializer()
        elif format == 'XML':
            return XmlSerializer()
        else:
            raise ValueError(format)


factory = SerializerFactory()

Текущая реализация.get_serializer() такая же, как и в исходном примере. Метод оценивает значениеformat и решает, какую конкретную реализацию создать и вернуть. Это относительно простое решение, которое позволяет нам проверить функциональность всех компонентов Factory Method.

Давайте посмотрим на интерактивный интерпретатор Python и посмотрим, как он работает:

>>>

>>> import songs
>>> import serializers
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'

>>> serializer.serialize(song, 'XML')
'Water of LoveDire Straits'

>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "", line 1, in 
  File "./serializers.py", line 39, in serialize
    serializer = factory.get_serializer(format)
  File "./serializers.py", line 52, in get_serializer
    raise ValueError(format)
ValueError: YAML

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

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

Поддержка дополнительных форматов

Текущая реализацияSerializerFactory должна быть изменена при вводе нового формата. Ваше приложение может никогда не нуждаться в поддержке каких-либо дополнительных форматов, но вы никогда не узнаете.

Вы хотите, чтобы ваши проекты были гибкими, и, как вы увидите, поддержка дополнительных форматов без измененияSerializerFactory относительно проста.

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

# In serializers.py

class SerializerFactory:

    def __init__(self):
        self._creators = {}

    def register_format(self, format, creator):
        self._creators[format] = creator

    def get_serializer(self, format):
        creator = self._creators.get(format)
        if not creator:
            raise ValueError(format)
        return creator()


factory = SerializerFactory()
factory.register_format('JSON', JsonSerializer)
factory.register_format('XML', XmlSerializer)

Метод.register_format(format, creator) позволяет регистрировать новые форматы, задав значениеformat, используемое для идентификации формата, и объектcreator. Объект-создатель является именем класса конкретногоSerializer. Это возможно, потому что все классыSerializer предоставляют значение.__init__() по умолчанию для инициализации экземпляров.

Регистрационная информация хранится в словаре_creators. Метод.get_serializer() получает зарегистрированного создателя и создает желаемый объект. Если запрошенныйformat не был зарегистрирован, то вызываетсяValueError.

Теперь вы можете проверить гибкость дизайна, реализовавYamlSerializer и избавившись от раздражающихValueError, которые вы видели ранее:

# In yaml_serializer.py

import yaml
import serializers

class YamlSerializer(serializers.JsonSerializer):
    def to_str(self):
        return yaml.dump(self._current_object)


serializers.factory.register_format('YAML', YamlSerializer)

Note: Чтобы реализовать пример, вам нужно установитьPyYAML в вашей среде, используяpip install PyYAML.

JSON и YAML - очень похожие форматы, поэтому вы можете повторно использовать большую часть реализацииJsonSerializer и перезаписать.to_str() для завершения реализации. Затем формат регистрируется в объектеfactory, чтобы сделать его доступным.

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

>>>

>>> import serializers
>>> import songs
>>> import yaml_serializer
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> print(serializer.serialize(song, 'JSON'))
{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}

>>> print(serializer.serialize(song, 'XML'))
Water of LoveDire Straits

>>> print(serializer.serialize(song, 'YAML'))
{artist: Dire Straits, id: '1', title: Water of Love}

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

Фабрика Объектов Общего Назначения

РеализацияSerializerFactory - огромное улучшение по сравнению с исходным примером. Это обеспечивает большую гибкость для поддержки новых форматов и позволяет избежать изменения существующего кода.

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

Фабричный метод может быть использован для решения широкого круга задач. Фабрика объектов дает дополнительную гибкость дизайну при изменении требований. В идеале вам понадобится реализация Object Factory, которую можно повторно использовать в любой ситуации без репликации реализации.

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

Не все объекты могут быть созданы равными

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

Не все ситуации позволяют нам использовать.__init__() по умолчанию для создания и инициализации объектов. Важно, чтобы создатель, в этом случае Object Factory, возвращал полностью инициализированные объекты.

Это важно, потому что если это не так, то клиенту придется завершить инициализацию и использовать сложный условный код для полной инициализации предоставленных объектов. Это противоречит цели шаблона проектирования Factory Method.

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

Note: Требования, которые я определяю для этого примера, приведены в целях иллюстрации и не отражают реальных требований, которые вам придется реализовать для интеграции с такими службами, какPandora илиSpotify.

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

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

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

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

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

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

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

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

Создание отдельного объекта для предоставления общего интерфейса

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

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

Давайте начнем с рассмотрения конфигурации приложения:

# In program.py

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

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

Давайте посмотрим на реализациюSpotifyService иSpotifyServiceBuilder:

# In music.py

class SpotifyService:
    def __init__(self, access_code):
        self._access_code = access_code

    def test_connection(self):
        print(f'Accessing Spotify with {self._access_code}')


class SpotifyServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, spotify_client_key, spotify_client_secret, **_ignored):
        if not self._instance:
            access_code = self.authorize(
                spotify_client_key, spotify_client_secret)
            self._instance = SpotifyService(access_code)
        return self._instance

    def authorize(self, key, secret):
        return 'SPOTIFY_ACCESS_CODE'

Note: Интерфейс музыкального сервиса определяет метод.test_connection(), которого должно быть достаточно для демонстрационных целей.

В примере показанSpotifyServiceBuilder, реализующий.__call__(spotify_client_key, spotify_client_secret, **_ignored).

Этот метод используется для создания и инициализации конкретногоSpotifyService. Он определяет обязательные параметры и игнорирует любые дополнительные параметры, предоставленные через**_ignored. После полученияaccess_code он создает и возвращает экземплярSpotifyService.

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

Давайте сделаем то же самое для Пандоры:

# In music.py

class PandoraService:
    def __init__(self, consumer_key, consumer_secret):
        self._key = consumer_key
        self._secret = consumer_secret

    def test_connection(self):
        print(f'Accessing Pandora with {self._key} and {self._secret}')


class PandoraServiceBuilder:
    def __init__(self):
        self._instance = None

    def __call__(self, pandora_client_key, pandora_client_secret, **_ignored):
        if not self._instance:
            consumer_key, consumer_secret = self.authorize(
                pandora_client_key, pandora_client_secret)
            self._instance = PandoraService(consumer_key, consumer_secret)
        return self._instance

    def authorize(self, key, secret):
        return 'PANDORA_CONSUMER_KEY', 'PANDORA_CONSUMER_SECRET'

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

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

# In music.py

class LocalService:
    def __init__(self, location):
        self._location = location

    def test_connection(self):
        print(f'Accessing Local music at {self._location}')


def create_local_music_service(local_music_location, **_ignored):
    return LocalService(local_music_location)

LocalService просто требует места, где хранится коллекция, для инициализацииLocalService.

Новый экземпляр создается каждый раз, когда запрашивается служба, поскольку нет медленной процедуры авторизации. Требования проще, поэтому вам не нужен классBuilder. Вместо этого используется функция, возвращающая инициализированныйLocalService. Эта функция соответствует интерфейсу методов.__call__(), реализованных в классах построителя.

Общий интерфейс к фабрике объектов

Фабрика объектов общего назначения (ObjectFactory) может использовать общий интерфейсBuilder для создания всех видов объектов. Он предоставляет метод для регистрацииBuilder на основе значенияkey и метод для создания конкретных экземпляров объекта на основеkey.

Давайте посмотрим на реализацию нашего общегоObjectFactory:

# In object_factory.py

class ObjectFactory:
    def __init__(self):
        self._builders = {}

    def register_builder(self, key, builder):
        self._builders[key] = builder

    def create(self, key, **kwargs):
        builder = self._builders.get(key)
        if not builder:
            raise ValueError(key)
        return builder(**kwargs)

Структура реализацииObjectFactory такая же, как уSerializerFactory.

Разница в том, что интерфейс поддерживает создание объектов любого типа. Параметром построителя может быть любой объект, реализующий интерфейсcallable. Это означает, чтоBuilder может быть функцией, классом или объектом, реализующим.__call__().

Метод.create() требует, чтобы дополнительные аргументы были указаны как аргументы ключевого слова. Это позволяет объектамBuilder указывать нужные им параметры и игнорировать остальные в произвольном порядке. Например, вы можете видеть, чтоcreate_local_music_service() указывает параметрlocal_music_location и игнорирует остальные.

Давайте создадим экземпляр фабрики и зарегистрируем сборщики для сервисов, которые вы хотите поддерживать:

# In music.py
import object_factory

# Omitting other implementation classes shown above

factory = object_factory.ObjectFactory()
factory.register_builder('SPOTIFY', SpotifyServiceBuilder())
factory.register_builder('PANDORA', PandoraServiceBuilder())
factory.register_builder('LOCAL', create_local_music_service)

Модульmusic предоставляет экземплярObjectFactory через атрибутfactory. Затем строители регистрируются в экземпляре. Для Spotify и Pandora вы регистрируете экземпляр их соответствующего компоновщика, но для локального сервиса вы просто передаете функцию.

Давайте напишем небольшую программу, которая демонстрирует функциональность:

# In program.py
import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.factory.create('PANDORA', **config)
pandora.test_connection()

spotify = music.factory.create('SPOTIFY', **config)
spotify.test_connection()

local = music.factory.create('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

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

Теперь вы можете запустить нашу программу, чтобы увидеть, как она работает:

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

Вы можете видеть, что правильный экземпляр создается в зависимости от указанного типа сервиса. Вы также можете видеть, что запрос сервиса Pandora или Spotify всегда возвращает один и тот же экземпляр.

Специализированная фабрика объектов для улучшения читабельности кода

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

В приведенном выше примере показано, что для доступа к музыкальной службе вызываетсяmusic.factory.create(). Это может привести к путанице. Другие разработчики могут полагать, что каждый раз создается новый экземпляр, и решают, что им следует хранить экземпляр службы, чтобы избежать медленного процесса инициализации.

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

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

В следующем примере показано, как специализироватьObjectFactory, предоставляя явный интерфейс для контекста приложения:

# In music.py

class MusicServiceProvider(object_factory.ObjectFactory):
    def get(self, service_id, **kwargs):
        return self.create(service_id, **kwargs)


services = MusicServiceProvider()
services.register_builder('SPOTIFY', SpotifyServiceBuilder())
services.register_builder('PANDORA', PandoraServiceBuilder())
services.register_builder('LOCAL', create_local_music_service)

Вы выводитеMusicServiceProvider изObjectFactory и предоставляете новый метод.get(service_id, **kwargs).

Этот метод вызывает общий.create(key, **kwargs), поэтому поведение остается прежним, но код лучше читается в контексте нашего приложения. Вы также переименовали предыдущую переменнуюfactory вservices и инициализировали ее какMusicServiceProvider.

Как видите, обновленный код приложения теперь выглядит намного лучше:

import music

config = {
    'spotify_client_key': 'THE_SPOTIFY_CLIENT_KEY',
    'spotify_client_secret': 'THE_SPOTIFY_CLIENT_SECRET',
    'pandora_client_key': 'THE_PANDORA_CLIENT_KEY',
    'pandora_client_secret': 'THE_PANDORA_CLIENT_SECRET',
    'local_music_location': '/usr/data/music'
}

pandora = music.services.get('PANDORA', **config)
pandora.test_connection()
spotify = music.services.get('SPOTIFY', **config)
spotify.test_connection()
local = music.services.get('LOCAL', **config)
local.test_connection()

pandora2 = music.services.get('PANDORA', **config)
print(f'id(pandora) == id(pandora2): {id(pandora) == id(pandora2)}')

spotify2 = music.services.get('SPOTIFY', **config)
print(f'id(spotify) == id(spotify2): {id(spotify) == id(spotify2)}')

Запуск программы показывает, что поведение не изменилось:

$ python program.py
Accessing Pandora with PANDORA_CONSUMER_KEY and PANDORA_CONSUMER_SECRET
Accessing Spotify with SPOTIFY_ACCESS_CODE
Accessing Local music at /usr/data/music
id(pandora) == id(pandora2): True
id(spotify) == id(spotify2): True

Заключение

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

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

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

В этой статье вы узнали:

  • Что такое шаблон проектирования Factory Method и каковы его компоненты

  • Как реорганизовать существующий код, чтобы использовать Factory Method

  • Ситуации, в которых должен использоваться фабричный метод

  • Как объектные фабрики обеспечивают большую гибкость для реализации фабричного метода

  • Как реализовать объектную фабрику общего назначения и ее задачи

  • Как специализировать общее решение, чтобы обеспечить лучший контекст

Дальнейшее чтение

Если вы хотите узнать больше о фабричном методе и других шаблонах проектирования, я рекомендуюDesign Patterns: Elements of Reusable Object-Oriented Software от GoF, который является отличным справочником для широко распространенных шаблонов проектирования.

Кроме того,Heads First Design Patterns: A Brain-Friendly Guide Эрика Фримена и Элизабет Робсон дает забавное, легкое для чтения объяснение шаблонов проектирования.

В Википедии есть хороший каталогdesign patterns со ссылками на страницы с наиболее распространенными и полезными шаблонами.