Рубрики
Без рубрики

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

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

Автор оригинала: Darinka Zobenica.

Обзор

Это первая статья в короткой серии, посвященной шаблонам проектирования в Python .

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

Creational Design Patterns , как следует из названия, имеет дело с созданием классов или объектов.

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

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

В этой статье рассматриваются следующие шаблоны проектирования:

  • Фабрика
  • Абстрактная фабрика
  • Строитель
  • Прототип
  • Синглтон
  • Пул объектов

Фабрика

Проблема

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

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

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

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

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

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

В качестве альтернативы вы можете рассмотреть Заводской шаблон .

Решение

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

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

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

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

Для начала не забудьте включить абстрактные методы:

from abc import ABC, abstractmethod

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

class Product(ABC):

    @abstractmethod
    def calculate_risk(self):
        pass

И теперь мы наследуем от него через Рабочего и Безработного :

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

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

Проблема

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

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

Решение

Идея очень похожа на обычный Фабричный паттерн, с той лишь разницей, что все фабрики имеют несколько отдельных методов создания объектов, а тип фабрики-это то, что определяет семейство объектов.

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

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

Теперь мы можем реализовать несколько видов Builders , которые подчиняются этому интерфейсу, например, для 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())

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

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

Давайте попробуем использовать Android Builder для создания 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

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

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 спасает жизнь.

Прототип

Проблема

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

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

Решение

Шаблон проектирования Prototype решает проблему копирования объектов, делегируя ее самим объектам. Все объекты, которые можно скопировать, должны реализовать метод с именем 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)

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

Синглтон

Проблема

A Singleton – это объект с двумя основными характеристиками:

  • Он может иметь не более одного экземпляра
  • Он должен быть глобально доступен в программе

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

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

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

Решение

Давайте продолжим и реализуем шаблон 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

Необязательный вот тип данных, который может содержать либо класс, указанный в [] , либо None .

Определение метода __call__ позволяет использовать экземпляры класса в качестве функций. Метод также вызывается во время инициализации, поэтому, когда мы вызываем что-то вроде a() под капотом, он вызовет свой базовый класс’ __call__ |/method.

В Python все является объектом. Это включает в себя занятия. Все обычные классы, которые вы пишете, а также стандартные классы имеют type в качестве типа объекта. Даже тип имеет тип тип .

Это означает , что type является метаклассом – другие классы являются экземплярами type , точно так же, как переменные объекты являются экземплярами этих классов. В нашем случае Singleton является экземпляром MetaSingleton .

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

супер(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 одновременно, один остановится на блокировке. Когда context manager освободит блокировку, другой войдет в оператор if и увидит, что экземпляр действительно уже создан другим потоком.

Пул объектов

Проблема

В нашем проекте есть класс, назовем его MyClass . Мой класс очень полезен и часто используется на протяжении всего проекта, хотя и в течение коротких периодов времени.

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

Решение

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

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

Давайте пойдем дальше и сначала определим 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)

Обратите внимание, что это голая реализация и что на практике этот шаблон может использоваться вместе с Singleton для обеспечения единого глобально доступного пула.

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

Выделение объектов, которые занимают только память (то есть никаких внешних ресурсов), как правило, относительно недорого в таких языках, в то время как большое количество “живых” ссылок на объекты может замедлить сборку мусора, потому что GC проходит через все ссылки.

Вывод

Таким образом, мы рассмотрели наиболее важные Шаблоны творческого проектирования в Python – проблемы, которые они решают и как они их решают.

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

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