4 must-have паттерна проектирования в Python

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

4 must-have паттерна проектирования на Python

Абстрактная фабрика

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

4 must-have паттерна проектирования на Python

Модуль стандартной Python библиотеки json иллюстрирует пример, когда требуется создание экземпляров объектов от имени вызывающей стороны. Рассмотрите строку JSON:

По умолчанию модуль json создаёт unicode объекты для строк типа «Americano» , float – для 9.61 , list – для последовательности элементов и dict – для ключей и значений объекта.

Но некоторым эти настройки по умолчанию не подходят. Например, бухгалтер против представления модулем json точной суммы «9 долларов 61 цент» в виде приближённого числа с плавающей запятой, и предпочёл бы вместо этого использовать экземпляр Decimal .

Это конкретный пример проблемы:

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

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

А вот загрузчик, что использует эту фабрику:

Во-вторых, отделите спецификацию от реализации путём создания абстрактного класса. Этот последний шаг оправдывает слово «абстрактный» в названии паттерна проектирования «Абстрактная фабрика». Ваш абстрактный класс гарантирует, что аргументом factory в load() будет класс, соответствующий требуемому интерфейсу:

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

Прототип

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

4 must-have паттерна проектирования на Python

Проще, если бы ни один класс в меню не нуждался в аргументах в __init__() :

Вместо этого, в дело вступает паттерн «Прототип», когда требуется создание экземпляров классов с заранее заданными списками аргументов:

Питонические решения

Питоническим подходом будет спроектировать классы исключительно с позиционными аргументами, без именованных. Затем легко хранить аргументы в виде кортежа, который предоставляется отдельно от самого класса. Это знакомый подход класса стандартной библиотеки Thread, который запрашивает вызываемый target= отдельно от передаваемых args=(. ) . Вот наши пункты меню:

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

Затем структура будет вызывать каждый объект с использованием некоторой вариации tup[0](*tup[1:]) .

Однако, возможно, классу потребуются не только позиционные аргументы, но и именованные. В ответ на это предоставьте простые вызываемые объекты, используя лямбда-выражения для классов, которые требуют аргументов:

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

Сам паттерн

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

К счастью, ситуация не такая мрачная. Если перечитаете фабричные классы выше, то заметите, что каждый из них удивительно похож на целевые классы, которые хотим создать. Так же, как и Note , NoteFactory сам хранит атрибут fraction . Стек фабрик выглядит, как минимум, списками атрибутов, как стек создаваемых целевых классов.

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

Результатом будет паттерн «Прототип», который напишем на Python с нуля. Все фабричные классы исчезают. Вместо этого у каждого объекта появляется метод clone() , на вызов которого он отвечает созданием нового экземпляра с полученными аргументами:

Хотя пример и так иллюстрирует паттерн проектирования, при желании усложните его. Например, добавьте в каждом методе clone() вызов type(self) вместо жёсткого кодирования имени собственного класса для случая вызова метода в подклассе.

Компоновщик

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

4 must-have паттерна проектирования на Python

Реализация: наследовать или нет?

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

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

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

Так как это программирование на Python, оба ограничения испаряются! Пишите код в предпочтительном для себя диапазоне безопасности и краткости. Хотите, пойдите классическим путём и добавьте общий суперкласс:

Или задайте объектам один и тот же интерфейс. И положитесь на тесты, которые помогут поддерживать симметрию между контейнерами и содержимым. (Где для простейших скриптов ваш «тест» может быть фактом выполнения кода.)

Или выберите другой подход из спектра дизайна между этими двумя крайностями. Вот что поддерживает Python:

  • Следуйте классической архитектуре с общим суперклассом, показанной в первом примере выше.
  • Сделайте суперкласс абстрактным базовым классом с помощью инструментов модуля стандартной библиотеки abc .
  • Объявите для двух классов совместно используемый интерфейс, наподобие поддерживаемых старым пакетом zope.interface .
  • Применяйте аннотации для получения жёстких гарантий того, что и контейнер, и содержимое реализуют требуемое поведение. Для этого понадобится установка Python библиотеки проверки типов, к примеру, MyPy .
  • Вы в праве использовать утиную типизацию и не просить ни разрешения, ни прощения!

Поскольку Python предлагает такой спектр подходов, не стоит определять паттерн «Компоновщик» классически, то есть как один конкретный механизм (суперкласс) для создания или гарантирования симметрии. Вместо этого определите его как создание симметрии любыми средствами в иерархии объектов.

Итератор

Как реализовать паттерн проектирования «Итератор» и подключиться к встроенным итерационным механизмам языка Python for , iter() и next() ?

4 must-have паттерна проектирования на Python

  • Добавьте в контейнер метод __iter__() , который возвращает объект итератора. Поддержка этого метода делает контейнер итерируемым.
  • Каждому итератору установите метод __next__() (в старом коде Python 2 next() записывали без двойного подчёркивания), который возвращает следующий элемент из контейнера при каждом вызове. Бросайте исключение StopIterator , когда больше нет элементов.
  • Помните, что некоторые пользователи передают в цикл for итераторы вместо основного контейнера? Чтобы обезопаситься в этом случае, каждому итератору также нужен метод __iter__() , который возвращает сам себя.

Посмотрите, как эти требования работают вместе, на примере нашего собственного итератора!

Обратите внимание, что не требуется, чтобы элементы, полученные в результате __next__() , сохранялись как постоянные значения внутри контейнера или даже присутствовали до вызова __next__() . Значит написать пример паттерна проектирования «Итератор» можно даже без реализации хранилища в контейнере:

Благодаря этим трём методам – ​​одному для объекта-контейнера и двум для его итератора – контейнер OddNumbers теперь полноправно участвует в богатой итерационной экосистеме языка программирования Python. Он будет работать без проблем с циклом for :

И также работает со встроенными методами iter() и next() .

Он дружит даже с генераторами списков и множеств!

Три простеньких метода – и вы разблокировали доступ к поддержке итераций на уровне синтаксиса Python.

Резюме

Теперь вы подружите Python с парой порождающих паттернов проектирования – абстрактной фабрикой и прототипом. Без труда реализуете структурный шаблон – прототип. По плечу вам имплементация поведенческого паттерна проектирования – итератора. Наверняка вас заинтересуют примеры других шаблонов проектирования на Python.