Шаблон проектирования "Фабрика", используется для создания конкретных реализаций общего интерфейса. Он отделяет процесс создания объекта от кода, который зависит от интерфейса объекта.
Например, приложению требуется объект с определенным интерфейсом для выполнения своих задач. Конкретная реализация интерфейса определяется некоторым параметром.
Вместо использования сложной условной структуры if/elif/else
для определения конкретной реализации приложение делегирует это решение отдельному компоненту, который создает конкретный объект. При таком подходе код приложения упрощается, что делает его более многоразовым и простым в обслуживании.
Представьте приложение, которому необходимо преобразовать объект Song
в его строковое представление, используя указанный формат. Преобразование объекта в другое представление часто называют сериализацией. В пользовательском коде эти требования часто реализуются в одной функции или методе, который содержит всю логику и реализацию. Например:
# test.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
, который может преобразовывать объект песни в его строковое представление в соответствии со значением аргумента format
.
Метод SongSerializer.serialize()
поддерживает два формата: JSON и XML, при использовании любого другого формата возникает исключение ValueError
.
Используем интерактивный режим Python, для просмотра работы кода:
# $ python3 -i test.py >>> song = Song('1', 'Water of Love', 'Dire Straits') >>> serializer = SongSerializer() >>> serializer.serialize(song, 'JSON') # '{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}' >>> serializer.serialize(song, 'XML') # '<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>' >>> serializer.serialize(song, 'YAML') # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # File "./test.py", line 30, in serialize # raise ValueError(format) # ValueError: YAML
Создается объекты песни и сериализатора. Сериализатор преобразует песню в ее строковое представление с помощью метода SongSerializer.serialize()
. Метод принимает объект песни в качестве параметра, а также строковое значение, представляющее нужный формат. Последний вызов использует 'YAML'
в качестве формата, который не поддерживается сериализатором, поэтому возникает исключение ValueError
.
Этот пример короткий и упрощенный, но все же в нем много сложностей. В коде существует три логических пути развития событий в зависимости от значения аргумента format
и это может показаться не таким уж важным, но приведенный выше пример довольно сложно поддерживать.
Приведенный выше пример демонстрирует все проблемы, которые можно встретить в сложном логическом коде при использовании операторов ветвления, применяемых для изменения поведения приложения. Использование условных структур if/elif/else
в совокупности с большими блоками кода в каждом из условий усложняет чтение, понимание и сопровождение кода.
Приведенный выше код может показаться несложным для чтения и понимания, тем не менее, его трудно поддерживать, т.к. он берет на себя слишком много задач. "Принцип единой ответственности" гласит, что модуль, класс или даже метод должны иметь единую четко определенную ответственность. Он должен делать только одну вещь и иметь только одну причину для изменения.
При внесении изменений в метод SongSerializer.serialize()
увеличивается риск появления новых ошибок или нарушения существующей функциональности. Рассмотрим все ситуации, которые могут потребовать модификации:
Song
. Добавление или удаление свойств класса Song
потребует изменения реализации, чтобы приспособиться к новой структуре.SongSerializer.serialize()
тоже должен измениться, т.к. преобразование в JSON жестко закодировано в реализации метода.Идеальная ситуация, это когда при изменений требований, можно эти требования реализовать без изменения метода SongSerializer.serialize()
.
Первый шаг, когда в приложении есть сложное ветвление кода - это определить общую цель каждого из путей выполнения.
Код, использующий if/elif/else
, обычно имеет общую цель, которая реализуется по-разному в каждом логическом пути. Приведенный выше код преобразует объект песни в его строковое представление, используя разные форматы в каждом логическом пути.
Исходя из цели, необходимо найти общий интерфейс, которым можно заменить каждый из путей. В примере выше требуется интерфейс, который принимает объект Song
и возвращает строку.
Когда есть общий интерфейс, то далее необходимо предоставить отдельные реализации для каждого логического пути. В примере выше предоставлена реализация для сериализации в JSON и еще одну для XML.
Затем предоставляется отдельный компонент, который определяет конкретную реализацию для использования на основе указанного формата. Этот компонент оценивает значение формата и возвращает конкретную реализацию, идентифицированную его значением.
Начнем рефакторинг кода для достижения желаемой структуры шаблона Factory
. Рефакторинг - это процесс изменения программы таким образом, чтобы не изменяя поведение кода, улучшить его внутреннюю структуру.
Интерфейс - это объект или функция, которая принимает объект Song
и возвращает строковое представление.
На первом шаге, преобразуем один из логических путей в интерфейс. То есть добавим новый метод SongSerializer._serialize_to_json()
и переместим в него код сериализации JSON. Затем изменим код для блока if format == 'JSON':
, так, чтобы он вызывал новый метод:
# пример рефакторинга class SongSerializer: def serialize(self, song, format): if format == 'JSON': return self._serialize_to_json(song) # Остальная часть кода остается прежней def _serialize_to_json(self, song): payload = { 'id': song.song_id, 'title': song.title, 'artist': song.artist } return json.dumps(payload)
После внесения этого изменения - поведение не изменилось. Теперь сделаем то же самое для параметра XML, вводя новый метод SongSerializer._serialize_to_xml()
, перемещая в него реализацию и изменяя вызов для блока elif format == 'XML':
.
В следующем примере показан рефакторинг кода:
# изменяем класс в test.py class SongSerializer: def serialize(self, song, format): """Интерфейс - принимает объект `Song` и возвращает строковое представление """ 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" состоит в том, чтобы предоставить отдельный компонент, ответственный за принятие решения о том, какую конкретную реализацию следует использовать на основе определенного параметра. В рассматриваемом примере, этим параметром является формат.
Чтобы завершить реализацию шаблона "Factory", добавим новый метод SongSerializer._get_serializer()
, который принимает нужный формат. Этот метод будет оценивать значение формата и возвращать соответствующую функцию сериализации:
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)
Примечание. Метод SongSerializer._get_serializer()
не вызывает конкретную реализацию, а просто возвращает сам объект метода.
Теперь, для завершения реализации изменим метод SongSerializer.serialize()
. В следующем примере показан полный код:
# изменяем класс в test.py class SongSerializer: def serialize(self, song, format): # Так как метод `_get_serializer()` возвращает объекты # методов, то переменной `serializer` присваивается один # их этих объектов, в зависимости от аргумента `format` serializer = self._get_serializer(format) # здесь происходит вызов объекта метода # с передачей в него аргумента `song` return serializer(song) def _get_serializer(self, format): if format == 'JSON': # возвращает объект метода `_serialize_to_json()` return self._serialize_to_json elif format == 'XML': # возвращает объект метода `_serialize_to_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". Метод SongSerializer.serializer()
- это код приложения, который зависит от интерфейса для выполнения своей задачи. Он называется клиентским компонентом шаблона. Интерфейс, который был определен - называется компонентом продукта. В нашем случае продукт - это функция, которая принимает Song
и возвращает строковое представление.
Методы SongSerializer._serialize_to_json()
и SongSerializer._serialize_to_xml()
являются конкретными реализациями продукта. Наконец, метод SongSerializer._get_serializer()
является компонентом-создателем. Создатель решает, какую конкретную реализацию использовать.
"Шаблон проектирования Factory" может быть реализован по другому. Обычно шаблон Factory, не использует аргумент self
ни в одном из добавленных методов. Это хороший признак того, что методы могут быть вынесены за пределы класса SongSerializer
, и, например, быть внешними функциями:
Примечание. Метод SongSerializer.serialize()
в не использует аргумент self
.
# изменяем класс в test.py 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')
Механика "шаблона проектирования Factory" всегда одинакова. Клиент SongSerializer.serialize()
зависит от конкретной реализации интерфейса. Он запрашивает реализацию у компонента-создателя get_serializer()
, используя какой-то идентификатор format
.
Создатель возвращает клиенту конкретную реализацию в соответствии со значением параметра, и клиент использует предоставленный объект для выполнения своей задачи.
Убедимся, что поведение приложения не изменилось:
# $ python3 -i test.py >>> song = Song('1', 'Water of Love', 'Dire Straits') >>> serializer = SongSerializer() >>> serializer.serialize(song, 'JSON') # '{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}' >>> serializer.serialize(song, 'XML') # '<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>' >>> serializer.serialize(song, 'YAML') # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # File "./test.py", line 13, in serialize # serializer = get_serializer(format) # File "./test.py", line 23, in get_serializer # raise ValueError(format) # ValueError: YAML
Шаблон проектирования "Фабрика" следует использовать в каждой ситуации, когда приложение (клиент) зависит от интерфейса (продукта) для выполнения задачи и существует несколько конкретных реализаций этого интерфейса. В этом случае необходимо предоставить параметр, который может идентифицировать конкретную реализацию, и использовать его в создателе для выбора конкретной реализации.
Существует множество ситуаций подходящих под это описание, поэтому рассмотрим несколько конкретных примеров.
Замена сложного логического кода. Сложные логические структуры в формате if/elif/else
трудно поддерживать, поскольку по мере изменения, требуются новые логические пути.
Шаблон "Фабрика" является хорошей заменой, т.к. можно поместить тело каждого логического пути в отдельные функции или классы с общим интерфейсом, а создатель может предоставить конкретную реализацию.
Параметр, оцениваемый в условиях, становится параметром для идентификации конкретной реализации. Приведенный выше пример реализуют эту ситуацию.
Создание связанных объектов из внешних данных. Представьте приложение, которому необходимо получить информацию о сотрудниках из базы данных или другого внешнего источника.
Записи представляют сотрудников с разными ролями или типами: менеджеры, офисные служащие, продавцы и т. д. Приложение может сохранить идентификатор, представляющий тип сотрудника в записи, а затем использовать "Фабрику" для создания каждого конкретного объекта Employee
из остальной информации в записи.
Поддержка нескольких реализаций одной и той же функции: приложению для обработки изображений необходимо преобразовать спутниковое изображение из одной системы координат в другую, но для выполнения преобразования существует несколько алгоритмов с разным уровнем точности.
Приложение может предоставить пользователю выбор опции, которая идентифицирует конкретный алгоритм. "Фабрика" может обеспечить конкретную реализацию алгоритма на основе этой опции.
Объединение схожих функций в общем интерфейсе. Следуя примеру обработки изображения, приложению необходимо применить фильтр к изображению. Конкретный фильтр может быть выбран пользовательским вводом, и "Фабрика" может предоставить конкретную реализацию фильтра.
Интеграция связанных внешних сервисов: например, приложение музыкального проигрывателя необходимо интегрировать с несколькими внешними сервисами и пользователям предоставить выбор, откуда нужно брать музыку. В этом случае, приложение может определить общий интерфейс для музыкального сервиса и использовать шаблон "Фабрика" для создания правильной интеграции на основе предпочтений пользователя.
Все эти ситуации определяют клиента, который зависит от общего интерфейса, известного как продукт. Все они предоставляют средства для определения конкретной реализации продукта, поэтому все они могут использовать шаблон проектирования "Фабрика" в своем дизайне.
Основные требования для разбираемого примера - это необходимость сериализовать объекты Song
в их строковое представление. Приложение предоставляет функции, связанные с музыкой, следовательно можно предположить, что в будущем ему может потребоваться сериализовать другие типы объектов, например, список воспроизведения или альбом.
В идеале проект должен поддерживать добавление сериализации для новых объектов путем реализации новых классов без необходимости внесения изменений в существующую реализацию. Цель разбираемого приложения, сериализация объектов в несколько форматов, таких как JSON и XML, поэтому кажется естественным определить интерфейс сериализатора, который может иметь несколько реализаций, по одной для каждого формата.
Реализация интерфейса может выглядеть примерно так:
# создаем `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')
Примечание. В приведенном примере реализован не полный интерфейс Serializer
.
Интерфейс Serializer
- это абстрактное понятие из-за динамической природы языка Python. Статические языки, такие как Java или C#, требуют явного определения интерфейсов. В Python говорят, что любой объект, предоставляющий нужные методы или функции, реализует интерфейс. В примере интерфейс Serializer
определяется как объект, реализующий следующие методы или функции:
.start_object(object_name, object_id)
.add_property(name, value)
.to_str()
Этот интерфейс реализуется конкретными классами JsonSerializer
и XmlSerializer
.
В исходном примере использовался класс SongSerializer
. Для нового приложения нужно реализовывать нечто более общее, например ObjectSerializer
:
# добавляем 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
и разрешается объектом фабрики. Аргумент serializable
относится к другому абстрактному интерфейсу, который должен быть реализован для любого типа объекта, который необходимо сериализовать.
Конкретная реализация сериализуемого интерфейса в классе Song
:
# создаем 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 - это объект сериализатора serializer.start_object('song', self.song_id) serializer.add_property('title', self.title) serializer.add_property('artist', self.artist)
Класс Song
реализует интерфейс serializable
, предоставляя метод Song.serialize(serializer)
. В этом методе, класс Song
, использует объект сериализатора serializer
для записи в него собственной информации, не зная формата.
Класс Song
даже не знает, что целью является преобразование данных в строку. Это важно, т.к. в дальнейшем можно использовать этот интерфейс для предоставления другого типа сериализатора, который при необходимости преобразует информацию о песне в совершенно другое представление. Например, приложению в будущем может потребоваться преобразовать объект Song
в двоичный формат.
В завершенном виде "шаблон Factory" и предоставляет "создателя", которым является фабрика переменных в методе ObjectSerializer.serialize()
.
В исходном примере, "создатель" реализовывался как функция. Функции хороши для очень простых примеров, но они не обеспечивают слишком большой гибкости при изменении требований.
Классы могут предоставлять дополнительные интерфейсы для добавления функциональности, и их можно создавать для настройки поведения. Если нужен очень простой "создатель", который не нужно менять в будущем, то необходимо реализовать его как класс, а не как функцию. Классы такого типа называются фабриками объектов.
Можно увидеть базовый интерфейс SerializerFactory
в реализации ObjectSerializer.serialize()
. Метод использует factory.get_serializer(format)
для извлечения сериализатора из фабрики объектов.
Теперь реализуем SerializerFactory
для соответствия этому интерфейсу:
# добавляем 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()
Текущая реализация SerializerFactory.get_serializer()
такая же, как и в исходном примере. Метод оценивает значение формата и возвращает конкретную реализацию. Это относительно простое решение, позволяющее выполнять проверку всех компонентов Factory.
Смотри как он работает:
>>> import songs, 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') # '<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>' >>> serializer.serialize(song, 'YAML') # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # 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 позволяет приложению вводить новые функции, добавляя новые классы, а не изменяя существующие. Можно сериализовать другие объекты, реализуя на них интерфейс Serializable
. Также можно поддерживать новые форматы, реализуя интерфейс Serializer
в другом классе.
Недостающая часть заключается в том, что для включения поддержки новых форматов необходимо изменить SerializerFactory
. Эта проблема легко решается с помощью нового дизайна, так как SerializerFactory
- это класс.
Текущую реализацию SerializerFactory
необходимо менять при введении нового формата. Приложению может никогда не потребоваться поддержка каких-либо дополнительных форматов, но этого, как обычно бывает, никто на 100% не знает.
Идея состоит в том, чтобы предоставить в SerializerFactory
метод, который регистрирует новую реализацию сериализатора serializer
(который передается в метод Song.serialize()
) для формата, который необходимо поддерживать:
# меняем класс в `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)
Метод SerializerFactory.register_format(format, creator)
позволяет регистрировать новые форматы format
, которое используется для идентификации объекта-создателя. Объект-создатель - это имя класса конкретного сериализатора. Регистрация классов сериализаторов (JsonSerializer
и XmlSerializer
) возможна, благодаря методу __init__()
в этих классах, для инициализации экземпляров.
Регистрируемые объекты хранятся в словаре _creators
. Метод SerializerFactory.get_serializer()
извлекает зарегистрированного создателя и создает нужный объект. Если запрошенный формат не был зарегистрирован, то возникает ValueError
.
При создании сериализатора YamlSerializer
можно увидеть гибкость дизайна. Класс YamlSerializer
создадим в отдельном файле (это не обязательно, его можно дописать в конец файл serializers.py
). Форматы JSON и YAML очень похожи, поэтому можно повторно использовать большую часть реализации класса JsonSerializer
, переопределив только метод .to_str()
. И чтобы сделать доступным новый формат, необходимо зарегистрировать созданный класс в фабрике объектов:
# файл yaml_serializers.py # импортируем основной файл `serializers.ру` import serializers import yaml # наследуемся от `serializers.JsonSerializer` class YamlSerializer(serializers.JsonSerializer): # переопределяем метод `to_str()` def to_str(self): return yaml.dumps(self._current_object) # регистрируем класс `YamlSerializer` в фабрике объектов serializers.factory.register_format('YAML', YamlSerializer)
Примечание. Чтобы пример работал, необходимо установить модуль pyyaml
, например, в виртуальное окружение.
Смотрим как это работает:
>>> import serializers, songs >>> import yaml_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') # <song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song> >>> serializer.serialize(song, 'YAML') # "artist: Dire Straits\nid: '1'\ntitle: Water of Love\n"
Реализуя шаблон Factory с помощью фабрики объектов и предоставляя интерфейс регистрации, можно поддерживать новые форматы, не изменяя какой-либо существующий код приложения. Это сводит к минимуму риск нарушения существующих функций или внесения неявных ошибок.
Реализация SerializerFactory
- это огромное улучшение по сравнению с первоначальным примером. Он обеспечивает большую гибкость для поддержки новых форматов и позволяет избежать изменения существующего кода.
Тем не менее, последняя реализация специально нацелена на проблему сериализации, описанную выше, и ее нельзя повторно использовать в других контекстах.
Шаблон проектирования "Factory" может быть использован для решения широкого круга задач. Фабрика объектов обеспечивает дополнительную гибкость конструкции при изменении требований. В идеале нужна реализация ObjectFactory
, которую можно повторно использовать в любой ситуации без дублирования реализации.
Самая большая проблема при реализации "Фабрики общего назначения" заключается в том, что не все объекты создаются одинаково.
Не во всех ситуациях можно использовать метод .__init__()
по умолчанию для создания и инициализации объектов. Важно, чтобы создатель, в данном случае "Фабрика объектов", возвращал полностью инициализированные объекты.
Это важно, потому что если это не так, то клиенту придется завершить инициализацию и использовать сложный условный код для полной инициализации предоставленных объектов. Это противоречит цели "шаблона проектирования Factory".
Чтобы понять сложность решения, рассмотрим другую проблему. Допустим, приложение хочет интегрироваться с различными музыкальными сервисами. Эти сервисы могут быть внешними по отношению к приложению или внутренними для поддержки локальной музыкальной коллекции. Каждая из услуг имеет свой набор требований.
Представьте, что приложению нужно интегрироваться с услугой, предоставляемой определенным музыкальном сервисом. Для этой службы требуется процесс авторизации, при котором предоставляются клиентский ключ key
и secret
.
Служба возвращает код доступа, который следует использовать при любом дальнейшем обмене данными. Процесс авторизации очень медленный, и его следует выполнять только один раз, следовательно приложению необходимо сохранить инициализированный объект службы и использовать его каждый раз, когда ему необходимо связаться музыкальном сервисом.
В то же время другие пользователи хотят интегрироваться с другим музыкальным сервисом, который может использовать совершенно другой процесс авторизации. Он также требует key
и secret
клиента, но при этом возвращает key
и secret
потребителя, которые следует использовать для других коммуникаций. Как и в случае с первым сервисом, процесс авторизации медленный, и его следует выполнять только один раз.
Наконец, приложение реализует концепцию локального музыкального сервиса, где музыкальная коллекция хранится локально. Службе нужно знать местоположение музыкальной коллекции в локальной системе. Создание нового экземпляра службы выполняется очень быстро, поэтому новый экземпляр можно создавать каждый раз, когда пользователь хочет получить доступ к музыкальной коллекции.
Этот пример имеет несколько проблем. Каждая служба инициализируется с различным набором параметров. Кроме того, первые два музыкальных сервиса требуют авторизации перед созданием экземпляра службы. Также необходимо повторно использовать экземпляр, чтобы избежать многократной авторизации приложения. Локальный сервис проще, но он не соответствует интерфейсу инициализации других.
Создание каждого конкретного музыкального сервиса имеет свой набор требований. Это означает, что общий интерфейс инициализации для каждой реализации службы невозможен или не рекомендуется.
Наилучший подход - это определить новый тип объекта, который предоставляет общий интерфейс и отвечает за создание конкретной службы. Этот новый тип объекта будет называться Builder
. Объект Builder
имеет всю логику для создания и инициализации экземпляра службы. Реализуем объект Builder
для каждой из поддерживаемых служб.
Начнем с конфигурации приложения:
# файл `program.py` config = { 'music1_client_key': 'MUSIC1_CLIENT_KEY', 'music1_client_secret': 'MUSIC1_CLIENT_SECRET', 'music2_client_key': 'MUSIC2_CLIENT_KEY', 'music2_client_secret': 'MUSIC2_CLIENT_SECRET', 'local_music_location': '/usr/data/music' }
Словарь конфигурации содержит все значения, необходимые для инициализации каждой из служб. Следующим шагом является определение интерфейса, который будет использовать эти значения для создания конкретной реализации музыкального сервиса. Этот интерфейс будет реализован в Builder
.
Смотрим на реализацию Music1Service
и Music1ServiceBuilder
первого музыкального сервиса:
# файл `music.py` class Music1Service: def __init__(self, access_code): self._access_code = access_code def test_connection(self): print(f'Доступ к сервису Music1 с {self._access_code}') class Music1ServiceBuilder: def __init__(self): self._instance = None def __call__(self, music1_client_key, music1_client_secret, **_ignored): if not self._instance: access_code = self.authorize( music1_client_key, music1_client_secret) self._instance = Music1Service(access_code) return self._instance def authorize(self, key, secret): return 'ACCESS_CODE'
Примечание. Интерфейс музыкального сервиса определяет метод Music1Service.test_connection()
, которого должно быть достаточно для демонстрационных целей.
В примере показан Music1ServiceBuilder
, реализующий .__call__(spotify_client_key, spotify_client_secret, **_ignored)
. Этот метод используется для создания и инициализации конкретного Music1Service. Он указывает обязательные параметры и игнорирует любые дополнительные параметры, предоставленные через **_ignored
. После получения кода доступа он создает и возвращает экземпляр Music1Service
.
Обратите внимание, что Music1ServiceBuilder
сохраняет экземпляр службы и создает новый только при первом запросе службы. Это позволяет избежать многократного прохождения процесса авторизации, как указано в требованиях.
Сделаем то же самое для второго музыкального сервиса:
# добавляем в файл `music.py` class Music2Service: def __init__(self, consumer_key, consumer_secret): self._key = consumer_key self._secret = consumer_secret def test_connection(self): print(f'Доступ к сервису Music2 с {self._key} и {self._secret}') class Music2ServiceBuilder: def __init__(self): self._instance = None def __call__(self, music2_client_key, music2_client_secret, **_ignored): if not self._instance: consumer_key, consumer_secret = self.authorize( music2_client_key, music2_client_secret) self._instance = Music2Service(consumer_key, consumer_secret) return self._instance def authorize(self, key, secret): return 'CONSUMER_KEY', 'CONSUMER_SECRET'
Music2ServiceBuilder
реализует тот же интерфейс, но использует другие параметры и процессы для создания и инициализации Music2Service
. Он также сохраняет экземпляр службы, поэтому авторизация происходит только один раз.
Реализация локального сервиса:
# добавляем в файл `music.py` class LocalService: def __init__(self, location): self._location = location def test_connection(self): print(f'Прослушивание музыки в папке {self._location}') def create_local_music_service(local_music_location, **_ignored): return LocalService(local_music_location)
Для инициализации LocalService
просто нужно указать местоположение, где хранится коллекция.
Новый экземпляр создается каждый раз, когда запрашивается служба, т.к. нет медленного процесса авторизации. Требования проще, поэтому не нужен класс Builder
. Вместо него используется функция, возвращающая инициализированный LocalService
. Эта функция соответствует интерфейсу методов .__call__()
, реализованных в классах строителей *Builder
.
Фабрика объекта общего назначения ObjectFactory
может использовать интерфейс универсальный интерфейс для создания всех видов объектов. Он предоставляет метод для регистрации строителя *Builder
на основе ключевого значения и способа для создания инстанций конкретных объектов на основе ключа.
Смотрим на реализацию универсальной ObjectFactory
:
# файл `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
. Разница заключается в интерфейсе, который поддерживает создание любого типа объекта. Аргумент builder
может быть любым объектом, реализующим вызываемый интерфейс. Это означает, что он может быть функцией, классом или объектом, реализующим метод .__call__()
(т.е. любой вызываемый объект).
Метод ObjectFactory.create()
требует, чтобы дополнительные аргументы были указаны как ключевые. Это позволяет передаваемым объектам builder
указывать нужные им параметры и игнорировать остальные в произвольном порядке. Например, create_local_music_service()
задает параметр local_music_location
и игнорирует остальные.
Создадим экземпляр фабрики и зарегистрируем в там сервисы:
# добавляем в `music.py` # импортируем файл `object_factory.py` import object_factory # создаем экземпляр фабрики объектов factory = object_factory.ObjectFactory() # регистрируем объекты сервисов factory.register_builder('MUSIC1', Music1ServiceBuilder()) factory.register_builder('MUSIC2', Music2ServiceBuilder()) factory.register_builder('LOCAL', create_local_music_service)
В файле music.py
предоставляется экземпляр ObjectFactory
через атрибут factory
. Затем в экземпляре регистрируются строители. Для онлайн музыкальных сервисов регистрируются экземпляры соответствующих компоновщиков, для локальной службы - передается простая функция.
Итоговая программа, демонстрирующая функциональность:
# файл `program.py` import music config = { 'music1_client_key': 'MUSIC1_CLIENT_KEY', 'music1_client_secret': 'MUSIC1_CLIENT_SECRET', 'music2_client_key': 'MUSIC2_CLIENT_KEY', 'music2_client_secret': 'MUSIC2_CLIENT_SECRET', 'local_music_location': '/usr/data/music' } # создаем экземпляры сервисов music1 = music.factory.create('MUSIC1', **config) music1.test_connection() music2 = music.factory.create('MUSIC2', **config) music2.test_connection() local = music.factory.create('LOCAL', **config) local.test_connection() # создаем вторые экземпляры ОНЛАЙН сервисов music_1 = music.factory.create('MUSIC1', **config) music_2 = music.factory.create('MUSIC2', **config) # убедимся, что это одни и те же экземпляры # то есть, не производится авторизация дважды print(f'id(music1) == id(music_1): {id(music1) == id(music_1)}') print(f'id(music2) == id(music_2): {id(music2) == id(music_2)}')
Приложение определяет словарь c конфигурацией приложения. Конфигурация используется в качестве ключевых аргументов для фабрики независимо от службы, к которой надо получить доступ. Фабрика создает конкретную реализацию музыкального сервиса на основе указанного ключевого аргумента.
Теперь можно посмотреть как работает программа:
$ python3 program.py Доступ к сервису Music1 с ACCESS_CODE Доступ к сервису Music2 с CONSUMER_KEY и CONSUMER_SECRET Прослушивание музыки в папке /usr/data/music id(music1) == id(music_1): True id(music2) == id(music_2): True
Общие решения допускают повторное использование и позволяют избежать дублирования кода. К сожалению, они также могут скрывать код и делать его менее читаемым.
В приведенном выше примере показано, что для доступа к музыкальному сервису вызывается функция music.factory.create()
. Это может привести к путанице. Другие разработчики могут подумать, что каждый раз создается новый экземпляр, и решат сохранить экземпляр службы, чтобы избежать медленного процесса инициализации.
Но этого не происходит, так как что классы *Builder
для онлайн сервисов сохраняет инициализированный экземпляр и возвращает его для последующих вызовов, но это не ясно из кода.
Хорошим тоном является специализация реализации общего назначения для предоставления интерфейса, который является конкретным для контекста приложения.
В следующем примере показано, как специализировать ObjectFactory
, предоставляя явный интерфейс для контекста приложения:
# добавляем в `music.py` после `import object_factory` class ServiceProvider(object_factory.ObjectFactory): def get(self, service_id, **kwargs): return self.create(service_id, **kwargs) # заменяем создание и работу с экземпляром `factory` provider = ServiceProvider() provider.register_builder('MUSIC1', Music1ServiceBuilder()) provider.register_builder('MUSIC2', Music2ServiceBuilder()) provider.register_builder('LOCAL', create_local_music_service)
Создаем класс ServiceProvider
, наследуя его от ObjectFactory
и добавляем новый метод ServiceProvider.get(service_id, **kwargs)
.
Этот метод вызывает общий .create(key, **kwargs)
, и следовательно поведение остается прежним, но код в контексте приложения читается лучше.
Теперь код самого приложения читается лучше:
# файл `program.py` import music config = { 'music1_client_key': 'MUSIC1_CLIENT_KEY', 'music1_client_secret': 'MUSIC1_CLIENT_SECRET', 'music2_client_key': 'MUSIC2_CLIENT_KEY', 'music2_client_secret': 'MUSIC2_CLIENT_SECRET', 'local_music_location': '/usr/data/music' } # создаем экземпляры сервисов music1 = music.provider.get('MUSIC1', **config) music1.test_connection() music2 = music.provider.get('MUSIC2', **config) music2.test_connection() local = music.provider.get('LOCAL', **config) local.test_connection() # создаем вторые экземпляры ОНЛАЙН сервисов music_1 = music.provider.get('MUSIC1', **config) music_2 = music.provider.get('MUSIC2', **config) # убедимся, что это одни и те же экземпляры # то есть, не производится авторизация дважды print(f'id(music1) == id(music_1): {id(music1) == id(music_1)}') print(f'id(music2) == id(music_2): {id(music2) == id(music_2)}')
Снова запустите файл program.py
и посмотрите как работает программа.
Если хотите знать о других шаблонах проектирования: