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

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


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

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

#serializer_demo.py

import json

import xml.etree.ElementTree aset

classSong:

def __init__(self,song_id,title,artist):

self.song_id=song_id

self.title=title

self.artist=artist

classSongSerializer:

def serialize(self,song,format):

ifformat=='JSON':

song_info={

'id':song.song_id,

'title':song.title,

'artist':song.artist

}

returnjson.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

returnet.tostring(song_info,encoding='unicode')

else:

raise ValueError(format)

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

>>>import serializer_demo assd

>>>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')

'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'

>>>serializer.serialize(song,'YAML')

Traceback(most recent call last):

File"<stdin>",line1,in<module>

File"./serializer_demo.py",line30,inserialize

raise ValueError(format)

ValueError:YAML

Создаются объект 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:

1

2

3

4

5

6

7

8

9

10

11

12

13

classSongSerializer:

def serialize(self,song,format):

ifformat=='JSON':

returnself._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

}

returnjson.dumps(payload)

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

classSongSerializer:

def serialize(self,song,format):

ifformat=='JSON':

returnself._serialize_to_json(song)

elif format=='XML':

returnself._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

}

returnjson.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

returnet.tostring(song_element,encoding='unicode')

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

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

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

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

1

2

3

4

5

6

7

8

classSongSerializer:

def _get_serializer(self,format):

ifformat=='JSON':

returnself._serialize_to_json

elif format=='XML':

returnself._serialize_to_xml

else:

raise ValueError(format)

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

classSongSerializer:

def serialize(self,song,format):

serializer=self._get_serializer(format)

returnserializer(song)

def _get_serializer(self,format):

ifformat=='JSON':

returnself._serialize_to_json

elif format=='XML':

returnself._serialize_to_xml

else:

raise ValueError(format)

def _serialize_to_json(self,song):

payload={

'id':song.song_id,

'title':song.title,

'artist':song.artist

}

returnjson.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

returnet.tostring(song_element,encoding='unicode')

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

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

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

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

classSongSerializer:

def serialize(self,song,format):

serializer=get_serializer(format)

returnserializer(song)

def get_serializer(format):

ifformat=='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

}

returnjson.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

returnet.tostring(song_element,encoding='unicode')

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

>>>import serializer_demo assd

>>>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')

'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'

>>>serializer.serialize(song,'YAML')

Traceback(most recent call last):

File"<stdin>",line1,in<module>

File"./serializer_demo.py",line13,inserialize

serializer=get_serializer(format)

File"./serializer_demo.py",line23,inget_serializer

raise ValueError(format)

ValueError:YAML

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

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

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

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

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

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

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

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

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

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

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

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


Источник: proglib.io

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