Обзор
Это первая статья из небольшой серии, посвященной шаблонам проектирования в Python .
Шаблоны творческого дизайна
Шаблоны Creational Design Patterns , как следует из названия, имеют дело с созданием классов или объектов.
Они служат для абстрагирования от специфики классов, чтобы мы были менее зависимы от их точной реализации, или чтобы нам не приходилось иметь дело со сложной конструкцией всякий раз, когда они нам нужны, или чтобы мы обеспечивали некоторые специальные свойства создания экземпляров. .
Они очень полезны для снижения уровня зависимости между нашими классами, а также для управления тем, как пользователь взаимодействует с ними.
В этой статье рассматриваются следующие шаблоны проектирования:
Фабрика
Проблема
Допустим, вы создаете программное обеспечение для страховой компании,
которая предлагает страхование людям, которые работают полный рабочий
день. Вы создали приложение, используя класс под названием Worker
.
Однако клиент решает расширить свой бизнес и теперь будет предоставлять свои услуги и безработным, хотя и с другими процедурами и условиями.
Теперь вам нужно создать совершенно новый класс для безработных, для которого потребуется совершенно другой конструктор! Но теперь вы не знаете, какой конструктор вызывать в общем случае, не говоря уже о том, какие аргументы ему передать.
В вашем коде могут быть уродливые условные выражения, где каждый вызов
конструктора окружен операторами if
, и вы используете некоторые,
возможно, дорогостоящие операции для проверки типа самого объекта.
Если во время инициализации возникают ошибки, они обнаруживаются, и код редактируется для этого в каждом из сотен мест, где используются конструкторы.
Не обращая на это внимания, вы прекрасно понимаете, что такой подход нежелателен, немасштабируем и полностью неустойчив.
В качестве альтернативы вы можете рассмотреть Factory Pattern .
Решение
Фабрики используются для инкапсуляции информации об используемых нами классах при их создании экземпляров на основе определенных параметров, которые мы им предоставляем.
Используя фабрику, мы можем заменить одну реализацию другой, просто изменив параметр, который изначально использовался для определения исходной реализации.
Это отделяет реализацию от использования таким образом, что мы можем легко масштабировать приложение, добавляя новые реализации и просто создавая их экземпляры через фабрику - с той же самой кодовой базой.
Если мы просто получаем в качестве параметра еще одну фабрику, нам даже не нужно знать, какой класс она производит. Нам просто нужно иметь единый фабричный метод, который возвращает класс, гарантированно имеющий определенный набор поведения. Давайте взглянем.
Для начала не забудьте включить абстрактные методы:
from abc import ABC, abstractmethod
Нам нужны наши созданные классы для реализации некоторого набора методов, которые позволят нам работать с ними единообразно. Для этого мы реализуем следующий интерфейс:
class Product(ABC):
@abstractmethod
def calculate_risk(self):
pass
И теперь мы наследуем от него через Worker
и Unemployed
:
class Worker(Product):
def __init__(self, name, age, hours):
self.name = name
self.age = age
self.hours = hours
def calculate_risk(self):
# Please imagine a more plausible implementation
return self.age + 100/self.hours
def __str__(self):
return self.name+" ["+str(self.age)+"] - "+str(self.hours)+"h/week"
class Unemployed(Product):
def __init__(self, name, age, able):
self.name = name
self.age = age
self.able = able
def calculate_risk(self):
# Please imagine a more plausible implementation
if self.able:
return self.age+10
else:
return self.age+30
def __str__(self):
if self.able:
return self.name+" ["+str(self.age)+"] - able to work"
else:
return self.name+" ["+str(self.age)+"] - unable to work"
Теперь, когда у нас есть люди, давайте создадим их фабрику:
class PersonFactory:
def get_person(self, type_of_person):
if type_of_person == "worker":
return Worker("Oliver", 22, 30)
if type_of_person == "unemployed":
return Unemployed("Sophie", 33, False)
Здесь мы жестко запрограммировали параметры для ясности, хотя обычно вы просто создаете экземпляр класса и заставляете его делать свое дело.
Чтобы проверить, как все это работает, давайте создадим экземпляр нашей фабрики и дадим ему возможность произвести пару человек:
factory = PersonFactory()
product = factory.get_person("worker")
print(product)
product2 = factory.get_person("unemployed")
print(product2)
Oliver [22] - 30h/week
Sophie [33] - unable to work
Абстрактная фабрика
Проблема
Вам нужно создать семью из разных предметов. Хотя они разные, они каким-то образом сгруппированы по определенному признаку.
Например, вам может потребоваться приготовить основное блюдо и десерт в итальянском и французском ресторанах, но вы не будете смешивать одну кухню с другой.
Решение
Идея очень похожа на обычный Factory Pattern, с той лишь разницей, что все фабрики имеют несколько отдельных методов для создания объектов, а тип фабрики определяет семейство объектов.
Абстрактная фабрика отвечает за создание целых групп объектов вместе с соответствующими фабриками, но не занимается конкретными реализациями этих объектов. Эта часть оставлена для соответствующих заводов:
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def cook(self):
pass
class FettuccineAlfredo(Product):
name = "Fettuccine Alfredo"
def cook(self):
print("Italian main course prepared: "+self.name)
class Tiramisu(Product):
name = "Tiramisu"
def cook(self):
print("Italian dessert prepared: "+self.name)
class DuckALOrange(Product):
name = "Duck À L'Orange"
def cook(self):
print("French main course prepared: "+self.name)
class CremeBrulee(Product):
name = "Crème brûlée"
def cook(self):
print("French dessert prepared: "+self.name)
class Factory(ABC):
@abstractmethod
def get_dish(type_of_meal):
pass
class ItalianDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return FettuccineAlfredo()
if type_of_meal == "dessert":
return Tiramisu()
def create_dessert(self):
return Tiramisu()
class FrenchDishesFactory(Factory):
def get_dish(type_of_meal):
if type_of_meal == "main":
return DuckALOrange()
if type_of_meal == "dessert":
return CremeBrulee()
class FactoryProducer:
def get_factory(self, type_of_factory):
if type_of_factory == "italian":
return ItalianDishesFactory
if type_of_factory == "french":
return FrenchDishesFactory
Мы можем проверить результаты, создав обе фабрики и вызвав
соответствующие cook()
для всех объектов:
fp = FactoryProducer()
fac = fp.get_factory("italian")
main = fac.get_dish("main")
main.cook()
dessert = fac.get_dish("dessert")
dessert.cook()
fac1 = fp.get_factory("french")
main = fac1.get_dish("main")
main.cook()
dessert = fac1.get_dish("dessert")
dessert.cook()
Italian main course prepared: Fettuccine Alfredo
Italian dessert prepared: Tiramisu
French main course prepared: Duck À L'Orange
French dessert prepared: Crème brûlée
Строитель
Проблема
Вам нужно изобразить робота со своей объектной структурой. Робот может быть гуманоидом с четырьмя конечностями и стоящим вверх, или он может быть похож на животное с хвостом, крыльями и т. Д.
Он может перемещаться с помощью колес или лопастей вертолета. Он может использовать камеры, модуль обнаружения инфракрасного излучения ... вы понимаете.
Представьте себе конструктор для этой штуки:
def __init__(self, left_leg, right_leg, left_arm, right_arm,
left_wing, right_wing, tail, blades, cameras,
infrared_module, #...
):
self.left_leg = left_leg
if left_leg == None:
bipedal = False
self.right_leg = right_leg
self.left_arm = left_arm
self.right_arm = right_arm
# ...
Создание экземпляра этого класса было бы чрезвычайно нечитаемым, было бы очень легко ошибиться в некоторых типах аргументов, поскольку мы работаем в Python, и с накоплением бесчисленных аргументов в конструкторе трудно справиться.
Кроме того, что, если мы не хотим, чтобы робот реализовал все поля в классе? Что, если мы хотим, чтобы у него были только ноги, а не обе ноги и колеса?
Python не поддерживает конструкторы перегрузки, которые помогли бы нам определить такие случаи (и даже если бы мы могли, это привело бы только к еще более беспорядочным конструкторам).
Решение
Мы можем создать класс Builder, который конструирует наш объект и добавляет соответствующие модули нашему роботу. Вместо запутанного конструктора мы можем создать экземпляр объекта и добавить необходимые компоненты с помощью функций.
Мы вызываем конструкцию каждого модуля отдельно после создания
экземпляра объекта. Давайте продолжим и определим Robot
с некоторыми
значениями по умолчанию:
class Robot:
def __init__(self):
self.bipedal = False
self.quadripedal = False
self.wheeled = False
self.flying = False
self.traversal = []
self.detection_systems = []
def __str__(self):
string = ""
if self.bipedal:
string += "BIPEDAL "
if self.quadripedal:
string += "QUADRIPEDAL "
if self.flying:
string += "FLYING ROBOT "
if self.wheeled:
string += "ROBOT ON WHEELS\n"
else:
string += "ROBOT\n"
if self.traversal:
string += "Traversal modules installed:\n"
for module in self.traversal:
string += "- " + str(module) + "\n"
if self.detection_systems:
string += "Detection systems installed:\n"
for system in self.detection_systems:
string += "- " + str(system) + "\n"
return string
class BipedalLegs:
def __str__(self):
return "two legs"
class QuadripedalLegs:
def __str__(self):
return "four legs"
class Arms:
def __str__(self):
return "four legs"
class Wings:
def __str__(self):
return "wings"
class Blades:
def __str__(self):
return "blades"
class FourWheels:
def __str__(self):
return "four wheels"
class TwoWheels:
def __str__(self):
return "two wheels"
class CameraDetectionSystem:
def __str__(self):
return "cameras"
class InfraredDetectionSystem:
def __str__(self):
return "infrared"
Обратите внимание, что мы пропустили определенные инициализации в конструкторе и использовали вместо них значения по умолчанию. Это потому, что мы будем использовать классы Builder для инициализации этих значений.
Сначала мы реализуем абстрактный Builder, который определяет наш интерфейс для сборки:
from abc import ABC, abstractmethod
class RobotBuilder(ABC):
@abstractmethod
def reset(self):
pass
@abstractmethod
def build_traversal(self):
pass
@abstractmethod
def build_detection_system(self):
pass
Теперь мы можем реализовать несколько видов Строителей, которые подчиняются этому интерфейсу, например, для Android и для автономного автомобиля:
class AndroidBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.bipedal = True
self.product.traversal.append(BipedalLegs())
self.product.traversal.append(Arms())
def build_detection_system(self):
self.product.detection_systems.append(CameraDetectionSystem())
class AutonomousCarBuilder(RobotBuilder):
def __init__(self):
self.product = Robot()
def reset(self):
self.product = Robot()
def get_product(self):
return self.product
def build_traversal(self):
self.product.wheeled = True
self.product.traversal.append(FourWheels())
def build_detection_system(self):
self.product.detection_systems.append(InfraredDetectionSystem())
Обратите внимание, как они реализуют одни и те же методы, но под ними внутренне различается структура объектов, и конечному пользователю не нужно иметь дело с деталями этой структуры?
Конечно, мы могли бы создать Robot
который может иметь как ноги, так и
колеса, и пользователь должен будет добавлять их по отдельности, но мы
также можем создать очень специфические конструкторы, которые добавляют
только один соответствующий модуль для каждой «части».
Давайте попробуем использовать AndroidBuilder
для создания Android:
builder = AndroidBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
Запуск этого кода даст:
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
А теперь давайте воспользуемся AutonomousCarBuilder
для сборки
автомобиля:
builder = AutonomousCarBuilder()
builder.build_traversal()
builder.build_detection_system()
print(builder.get_product())
Запуск этого кода даст:
ROBOT ON WHEELS
Traversal modules installed:
- four wheels
Detection systems installed:
- infrared
Инициализация намного более понятна и удобочитаема по сравнению с беспорядочным конструктором из прошлого, и у нас есть возможность добавлять модули, которые нам нужны .
Если в полях нашего продукта используются относительно стандартные конструкторы, мы даже можем создать так называемый Директор для управления конкретными конструкторами:
class Director:
def make_android(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
def make_autonomous_car(self, builder):
builder.build_traversal()
builder.build_detection_system()
return builder.get_product()
director = Director()
builder = AndroidBuilder()
print(director.make_android(builder))
Выполнение этого фрагмента кода даст:
BIPEDAL ROBOT
Traversal modules installed:
- two legs
- four legs
Detection systems installed:
- cameras
При этом шаблон Builder не имеет особого смысла для небольших простых классов, поскольку добавленная логика для их создания просто добавляет больше сложности.
Хотя, когда дело доходит до больших сложных классов с множеством полей, таких как многослойные нейронные сети, шаблон Builder спасает жизнь.
Опытный образец
Проблема
Нам нужно клонировать объект, но мы можем не знать его точный тип, параметры, не все они могут быть назначены через сам конструктор или могут зависеть от состояния системы в конкретный момент во время выполнения.
Если мы попытаемся сделать это напрямую, мы добавим в наш код много ветвлений зависимостей, и это может даже не сработать в конце.
Решение
Шаблон проектирования « Прототип» решает проблему копирования
объектов, делегируя его самим объектам. Все копируемые объекты должны
реализовывать метод под названием clone
и использовать его для
возврата точных копий самих себя.
Давайте продолжим и определим общую clone
для всех дочерних классов, а
затем унаследуем ее от родительского класса:
from abc import ABC, abstractmethod
class Prototype(ABC):
def clone(self):
pass
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
obj = MyObject(self.field1, field2)
obj.performed_operation = self.performed_operation
return obj
В качестве альтернативы вы можете использовать deepcopy
вместо
простого назначения полей, как в предыдущем примере:
class MyObject(Prototype):
def __init__(self, arg1, arg2):
self.field1 = arg1
self.field2 = arg2
def __operation__(self):
self.performed_operation = True
def clone(self):
return deepcopy(self)
Паттерн прототип может быть действительно полезен в крупномасштабных приложениях, которые создают множество объектов. Иногда копирование уже существующего объекта обходится дешевле, чем создание нового.
Синглтон
Проблема
Синглтон - это объект с двумя основными характеристиками:
- Может иметь не более одного экземпляра
- Он должен быть глобально доступен в программе
Оба эти свойства важны, хотя на практике вы часто услышите, как люди называют что-то синглтоном, даже если оно имеет только одно из этих свойств.
Наличие только одного экземпляра обычно является механизмом управления доступом к какому-либо общему ресурсу. Например, два потока могут работать с одним и тем же файлом, поэтому вместо того, чтобы открывать его по отдельности, синглтон может предоставить им обоим уникальную точку доступа.
Глобальная доступность важна, потому что после того, как ваш класс был создан один раз, вам нужно будет передать этот единственный экземпляр, чтобы с ним работать. Он не может быть создан снова. Вот почему легче убедиться, что всякий раз, когда вы снова пытаетесь создать экземпляр класса, вы просто получаете тот же экземпляр, который у вас уже был.
Решение
Давайте продолжим и реализуем шаблон Singleton , сделав объект глобально доступным и ограниченным одним экземпляром:
from typing import Optional
class MetaSingleton(type):
_instance : Optional[type] = None
def __call__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance
class BaseClass:
field = 5
class Singleton(BaseClass, metaclass=MetaSingleton):
pass
Optional
здесь тип данных, который может содержать либо класс,
указанный в []
либо None
.
Определение __call__
позволяет использовать экземпляры класса как
функции. Этот метод также вызывается во время инициализации, поэтому,
когда мы вызываем что-то вроде a = Singleton()
под капотом, он
__call__
метод __call__ своего базового класса.
В Python все является объектом. Это включает классы. Все обычные классы,
которые вы пишете, а также стандартные классы имеют type
качестве
своего типа объекта. Четный type
- это типовой type
.
Это означает, что type
является метаклассом - другие классы являются
экземплярами type
, точно так же, как объекты переменных являются
экземплярами этих классов. В нашем случае Singleton
является
экземпляром MetaSingleton
.
Все это означает, что наш __call__
будет вызываться всякий раз, когда
будет создан новый объект, и он предоставит новый экземпляр, если мы еще
не инициализировали его. Если да, он просто вернет уже
инициализированный экземпляр.
super(MetaSingleton, cls).__call__(*args, **kwargs)
вызывает
суперкласс ' __call__
. Наш суперкласс в этом случае - это type
,
который имеет __call__
которая будет выполнять инициализацию с
заданными аргументами.
Мы указали наш тип ( MetaSingleton
), значение, которое будет
присвоено полю _instance
cls
), и другие аргументы, которые мы можем
передавать.
Целью использования метакласса в этом случае, а не более простой реализации, является, по сути, возможность повторно использовать код.
В этом случае мы унаследовали от него один класс, но если бы нам понадобился другой синглтон для другой цели, мы могли бы просто получить тот же метакласс вместо того, чтобы реализовывать по сути то же самое.
Теперь мы можем попробовать его использовать:
a = Singleton()
b = Singleton()
a == b
True
Из-за его глобальной точки доступа разумно интегрировать
потокобезопасность в Singleton . К счастью, для этого нам не нужно
слишком много его редактировать. Мы можем просто немного MetaSingleton
def __call__(cls, *args, **kwargs):
with cls._lock:
if not cls._instance:
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
Таким образом, если два потока одновременно начнут создавать экземпляр
Singleton , один остановится на блокировке. Когда диспетчер
контекста снимает блокировку, другой войдет
в if
и увидит, что экземпляр действительно уже был создан другим
потоком.
Пул объектов
Проблема
В нашем проекте есть класс, назовем его MyClass
. MyClass
очень
полезен и часто используется на протяжении всего проекта, хотя и на
короткие периоды времени.
Однако его создание и инициализация очень дороги, и наша программа работает очень медленно, потому что ей постоянно нужно создавать новые экземпляры только для того, чтобы использовать их для нескольких операций.
Решение
Мы создадим пул объектов, экземпляры которых будут созданы при создании
самого пула. Всякий раз, когда нам нужно использовать объект типа
MyClass
, мы получаем его из пула, используем, а затем возвращаем в
пул для повторного использования.
Если объект имеет какое-то начальное состояние по умолчанию, при освобождении он всегда будет перезапущен. Если пул останется пустым, мы инициализируем новый объект для пользователя, но когда пользователь закончит с ним, он вернет его обратно в пул для повторного использования.
Давайте продолжим и сначала определим MyClass
:
class MyClass:
# Return the resource to default setting
def reset(self):
self.setting = 0
class ObjectPool:
def __init__(self, size):
self.objects = [MyClass() for _ in range(size)]
def acquire(self):
if self.objects:
return self.objects.pop()
else:
self.objects.append(MyClass())
return self.objects.pop()
def release(self, reusable):
reusable.reset()
self.objects.append(reusable)
И чтобы проверить это:
pool = ObjectPool(10)
reusable = pool.acquire()
pool.release(reusable)
Обратите внимание, что это простая реализация и что на практике этот шаблон можно использовать вместе с синглтоном для создания единого глобально доступного пула.
Обратите внимание, что полезность этого шаблона оспаривается в языках, которые используют сборщик мусора .
Выделение объектов, которые занимают только память (то есть отсутствие внешних ресурсов), как правило, относительно недорого в таких языках, в то время как множество «живых» ссылок на объекты может замедлить сборку мусора, поскольку сборщик мусора просматривает все ссылки.
Заключение
Здесь мы рассмотрели наиболее важные шаблоны творческого проектирования в Python - проблемы, которые они решают, и способы их решения.
Знакомство с шаблонами проектирования - чрезвычайно удобный набор навыков для всех разработчиков, поскольку они предоставляют решения общих проблем, возникающих при программировании.
Зная как мотивы, так и решения, вы также можете избежать случайного создания антипаттерна, пытаясь решить проблему.