Шаблоны творческого проектирования в Python

Обзор Это первая статья из короткой серии, посвященной шаблонам проектирования в Python [/ design-patterns-in-python]. Шаблоны творческого проектирования Шаблоны творческого проектирования, как следует из названия, имеют дело с созданием классов или объектов. Они служат для абстрагирования от специфики классов, чтобы мы меньше зависели от их точной реализации, или чтобы нам не приходилось иметь дело со сложной конструкцией всякий раз, когда они нам нужны, или чтобы мы обеспечивали некоторые особые свойства создания экземпляров.

Обзор

Это первая статья из небольшой серии, посвященной шаблонам проектирования в 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 - проблемы, которые они решают, и способы их решения.

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

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

comments powered by Disqus