Когда вы уже написали несколько своих небольших пет-проектов, вы начинаете понимать что чистый код, архитектура и другие паттерны программирования начинают иметь смысл. В масштабируемых, командный или коммерческих проектах это несет особую ценность. Изучив эти принципы, новички получат представление о построении надежных, гибких и легко тестируемых приложений, что позволит им сохранить ясность кодовой базы и возможность ее сопровождения по мере роста их проектов.
В этой статье мы изучим методологии программирования и паттерны проектирования на Python. Я бы даже сказал, что это компиляция полезных материалов, справочник, большая шпаргалка по всем паттернам.
Больше материала для тех кто любит и интересуется языком программирования Python (а также Data Science) я собрал в своем Telegram-канале.
Паттерны
Порождающие паттерны(Creational Patterns):
беспокоятся о гибком создании объектов без внесения в программу лишних зависимостей.
Абстрактная фабрика (Abstract Factory) Семейства связанных объектов.
Строитель (Builder) Cложные объекты пошагово. один код для разных объектов.
Фабричный метод (Factory Method) Общий интерфейс для подклассов изменет тип объектов.
Прототип (Prototype) Копируем объекты, не вдаваясь в подробности реализации.
Одиночка (Singleton) Класс имеет только один экземпляр, и глобальную точку доступа.
Моностатический синглтон (Borg)
Ленивая оценка (Lazy evaluation)
Пул объектов (Object pool)
Структурные паттерны (Structural Patterns):
показывают различные способы построения связей между объектами.
Адаптер (Adapter) Несовместимые интерфейсы
Компоновщик (Composite) Древовидная структуруа
Декоратор/оформитель (Decorator/Wrapper) Функциональность через «обёртки».
Фасад (Facade) Простой интерфейс к сложной структуре
Мост (Bridge) Абстракция + Реализация
Легковес/Приспособленец (Flyweight) Разделяя общее состояние объектов
Заместитель/прокси/суррогат (Proxy/surrogate) Подставляет объекты-заменители.
Трехзвенка (Three-Tier/3-tier)
Единая точка входа (Front controller)
Модель Отображенин Контроллер(MVC)
Поведенческие паттерны (Behavioral Patterns):
заботятся об эффективной коммуникации между объектами.
Команда/действие (Command/action) Передает запросы в объекты как аргументы.
Итератор/указатель (Iterator) Последовательный обход элементов составных объектов.
Наблюдатель/слушатель (Observer/Listener) Один объект следит за другим.
Стратегия (Strategy) Схожие алгоритмы в класс.
Посредник (Mediator) Перемещение связей в один класс-посредник.
Состояние (State) Меняет поведение в зависимости от состояния.
Шаблонный метод (Template Method) Перекладывает ответственность на подклассы не меняя его общей структуры.
Цепочка обязанностей (Chain of Responsibility) Запросы по цепочке обработчиков.
Снимок/Хранитель (Memento) Снимки состояния объектов.
Классная доска (Blackboard)(доска объявлений)
Посетитель (Visitor) Новые операции, не меняя классы объектов.
Одноразовый посетитель (Single-serving visitor)
Иерархический посетитель (Hierarchical visitor)
Каталог (Сatalog)
Цепь методов (Chaining method)
Издатель-подписчик (Publish subscribe/Pub-sub)
Спецификация/пределение (Specification)
Слуга (Servant)
Подчинение (Subsumption)
Разное
Внедрение зависимости (Dependency injection)
Отложенная инициализация (Lazy initialization)
Шаблон делегирования (Delegation pattern)
Поиск в графе (Graph search)
Машина состояний (Hierarchical State Machine/HSM)
Реестр/Журнал записей (Registry)
Наследование (Inheritance)
Нейтральный объект (Null)
Сокрытие (Closure)
Пул «одиночек» (Multiton)
Абстрактная фабрика
Абстрактная фабрика — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов, не привязываясь к конкретным классам создаваемых объектов.
Абстрактная фабрика (англ.Abstract factory) — порождающий шаблон проектирования, предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов. Шаблон реализуется созданием абстрактного класса Factory, который представляет собой интерфейс для создания компонентов системы (например, для оконного интерфейса он может создавать окна и кнопки). Затем пишутся классы, реализующие этот интерфейс.
from __future__ import annotations
from abc import ABC, abstractmethod
class AbstractFactory(ABC):
"""
Интерфейс Абстрактной Фабрики объявляет набор методов, которые возвращают
различные абстрактные продукты. Эти продукты называются семейством и связаны
темой или концепцией высокого уровня. Продукты одного семейства обычно могут
взаимодействовать между собой. Семейство продуктов может иметь несколько
вариаций, но продукты одной вариации несовместимы с продуктами другой.
"""
@abstractmethod
def create_product_a(self) -> AbstractProductA:
pass
@abstractmethod
def create_product_b(self) -> AbstractProductB:
pass
class ConcreteFactory1(AbstractFactory):
"""
Конкретная Фабрика производит семейство продуктов одной вариации. Фабрика
гарантирует совместимость полученных продуктов. Обратите внимание, что
сигнатуры методов Конкретной Фабрики возвращают абстрактный продукт, в то
время как внутри метода создается экземпляр конкретного продукта.
"""
def create_product_a(self) -> AbstractProductA:
return ConcreteProductA1()
def create_product_b(self) -> AbstractProductB:
return ConcreteProductB1()
class ConcreteFactory2(AbstractFactory):
"""
Каждая Конкретная Фабрика имеет соответствующую вариацию продукта.
"""
def create_product_a(self) -> AbstractProductA:
return ConcreteProductA2()
def create_product_b(self) -> AbstractProductB:
return ConcreteProductB2()
class AbstractProductA(ABC):
"""
Каждый отдельный продукт семейства продуктов должен иметь базовый интерфейс.
Все вариации продукта должны реализовывать этот интерфейс.
"""
@abstractmethod
def useful_function_a(self) -> str:
pass
"""
Конкретные продукты создаются соответствующими Конкретными Фабриками.
"""
class ConcreteProductA1(AbstractProductA):
def useful_function_a(self) -> str:
return "The result of the product A1."
class ConcreteProductA2(AbstractProductA):
def useful_function_a(self) -> str:
return "The result of the product A2."
class AbstractProductB(ABC):
"""
Базовый интерфейс другого продукта. Все продукты могут взаимодействовать
друг с другом, но правильное взаимодействие возможно только между продуктами
одной и той же конкретной вариации.
"""
@abstractmethod
def useful_function_b(self) -> None:
"""
Продукт B способен работать самостоятельно...
"""
pass
@abstractmethod
def another_useful_function_b(self, collaborator: AbstractProductA) -> None:
"""
...а также взаимодействовать с Продуктами A той же вариации.
Абстрактная Фабрика гарантирует, что все продукты, которые она создает,
имеют одинаковую вариацию и, следовательно, совместимы.
"""
pass
"""
Конкретные Продукты создаются соответствующими Конкретными Фабриками.
"""
class ConcreteProductB1(AbstractProductB):
def useful_function_b(self) -> str:
return "The result of the product B1."
"""
Продукт B1 может корректно работать только с Продуктом A1. Тем не менее, он
принимает любой экземпляр Абстрактного Продукта А в качестве аргумента.
"""
def another_useful_function_b(self, collaborator: AbstractProductA) -> str:
result = collaborator.useful_function_a()
return f"The result of the B1 collaborating with the ({result})"
class ConcreteProductB2(AbstractProductB):
def useful_function_b(self) -> str:
return "The result of the product B2."
def another_useful_function_b(self, collaborator: AbstractProductA):
"""
Продукт B2 может корректно работать только с Продуктом A2. Тем не менее,
он принимает любой экземпляр Абстрактного Продукта А в качестве
аргумента.
"""
result = collaborator.useful_function_a()
return f"The result of the B2 collaborating with the ({result})"
def client_code(factory: AbstractFactory) -> None:
"""
Клиентский код работает с фабриками и продуктами только через абстрактные
типы: Абстрактная Фабрика и Абстрактный Продукт. Это позволяет передавать
любой подкласс фабрики или продукта клиентскому коду, не нарушая его.
"""
product_a = factory.create_product_a()
product_b = factory.create_product_b()
print(f"{product_b.useful_function_b()}")
print(f"{product_b.another_useful_function_b(product_a)}", end="")
if __name__ == "__main__":
"""
Клиентский код может работать с любым конкретным классом фабрики.
"""
print("Client: Testing client code with the first factory type:")
client_code(ConcreteFactory1())
print("\n")
print("Client: Testing the same client code with the second factory type:")
client_code(ConcreteFactory2())
Или вот другой код попроще:
from abc import ABC, abstractmethod
# Abstract Products
class Laptop(ABC):
@abstractmethod
def display(self):
pass
class Smartphone(ABC):
@abstractmethod
def display(self):
pass
# Concrete Products
class DellLaptop(Laptop):
def display(self):
return "Dell Laptop"
class SamsungSmartphone(Smartphone):
def display(self):
return "Samsung Smartphone"
# Abstract Factory
class DeviceFactory(ABC):
@abstractmethod
def create_laptop(self):
pass
@abstractmethod
def create_smartphone(self):
pass
# Concrete Factories
class DellFactory(DeviceFactory):
def create_laptop(self):
return DellLaptop()
def create_smartphone(self):
return None # Dell doesn't make smartphones
class SamsungFactory(DeviceFactory):
def create_laptop(self):
return None # Samsung doesn't make laptops
def create_smartphone(self):
return SamsungSmartphone()
Строитель
Строитель — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. Строитель даёт возможность использовать один и тот же код строительства для получения разных представлений объектов.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any
class Builder(ABC):
"""
Интерфейс Строителя объявляет создающие методы для различных частей объектов
Продуктов.
"""
@property
@abstractmethod
def product(self) -> None:
pass
@abstractmethod
def produce_part_a(self) -> None:
pass
@abstractmethod
def produce_part_b(self) -> None:
pass
@abstractmethod
def produce_part_c(self) -> None:
pass
class ConcreteBuilder1(Builder):
"""
Классы Конкретного Строителя следуют интерфейсу Строителя и предоставляют
конкретные реализации шагов построения. Ваша программа может иметь несколько
вариантов Строителей, реализованных по-разному.
"""
def __init__(self) -> None:
"""
Новый экземпляр строителя должен содержать пустой объект продукта,
который используется в дальнейшей сборке.
"""
self.reset()
def reset(self) -> None:
self._product = Product1()
@property
def product(self) -> Product1:
"""
Конкретные Строители должны предоставить свои собственные методы
получения результатов. Это связано с тем, что различные типы строителей
могут создавать совершенно разные продукты с разными интерфейсами.
Поэтому такие методы не могут быть объявлены в базовом интерфейсе
Строителя (по крайней мере, в статически типизированном языке
программирования).
Как правило, после возвращения конечного результата клиенту, экземпляр
строителя должен быть готов к началу производства следующего продукта.
Поэтому обычной практикой является вызов метода сброса в конце тела
метода getProduct. Однако такое поведение не является обязательным, вы
можете заставить своих строителей ждать явного запроса на сброс из кода
клиента, прежде чем избавиться от предыдущего результата.
"""
product = self._product
self.reset()
return product
def produce_part_a(self) -> None:
self._product.add("PartA1")
def produce_part_b(self) -> None:
self._product.add("PartB1")
def produce_part_c(self) -> None:
self._product.add("PartC1")
class Product1():
"""
Имеет смысл использовать паттерн Строитель только тогда, когда ваши продукты
достаточно сложны и требуют обширной конфигурации.
В отличие от других порождающих паттернов, различные конкретные строители
могут производить несвязанные продукты. Другими словами, результаты
различных строителей могут не всегда следовать одному и тому же интерфейсу.
"""
def __init__(self) -> None:
self.parts = []
def add(self, part: Any) -> None:
self.parts.append(part)
def list_parts(self) -> None:
print(f"Product parts: {', '.join(self.parts)}", end="")
class Director:
"""
Директор отвечает только за выполнение шагов построения в определённой
последовательности. Это полезно при производстве продуктов в определённом
порядке или особой конфигурации. Строго говоря, класс Директор необязателен,
так как клиент может напрямую управлять строителями.
"""
def __init__(self) -> None:
self._builder = None
@property
def builder(self) -> Builder:
return self._builder
@builder.setter
def builder(self, builder: Builder) -> None:
"""
Директор работает с любым экземпляром строителя, который передаётся ему
клиентским кодом. Таким образом, клиентский код может изменить конечный
тип вновь собираемого продукта.
"""
self._builder = builder
"""
Директор может строить несколько вариаций продукта, используя одинаковые
шаги построения.
"""
def build_minimal_viable_product(self) -> None:
self.builder.produce_part_a()
def build_full_featured_product(self) -> None:
self.builder.produce_part_a()
self.builder.produce_part_b()
self.builder.produce_part_c()
if __name__ == "__main__":
"""
Клиентский код создаёт объект-строитель, передаёт его директору, а затем
инициирует процесс построения. Конечный результат извлекается из объекта-
строителя.
"""
director = Director()
builder = ConcreteBuilder1()
director.builder = builder
print("Standard basic product: ")
director.build_minimal_viable_product()
builder.product.list_parts()
print("\n")
print("Standard full featured product: ")
director.build_full_featured_product()
builder.product.list_parts()
print("\n")
# Помните, что паттерн Строитель можно использовать без класса Директор.
print("Custom product: ")
builder.produce_part_a()
builder.produce_part_b()
builder.product.list_parts()
Factory method
Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов в суперклассе, позволяя подклассам изменять тип создаваемых объектов.
Избавляет класс от привязки к конкретным классам продуктов.
Выделяет код производства продуктов в одно место, упрощая поддержку кода.
Упрощает добавление новых продуктов в программу.
Реализует принцип открытости/закрытости.
from __future__ import annotations
from abc import ABC, abstractmethod
class Creator(ABC):
"""
Класс Создатель объявляет фабричный метод, который должен возвращать объект
класса Продукт. Подклассы Создателя обычно предоставляют реализацию этого
метода.
"""
@abstractmethod
def factory_method(self):
"""
Обратите внимание, что Создатель может также обеспечить реализацию
фабричного метода по умолчанию.
"""
pass
def some_operation(self) -> str:
"""
Также заметьте, что, несмотря на название, основная обязанность
Создателя не заключается в создании продуктов. Обычно он содержит
некоторую базовую бизнес-логику, которая основана на объектах Продуктов,
возвращаемых фабричным методом. Подклассы могут косвенно изменять эту
бизнес-логику, переопределяя фабричный метод и возвращая из него другой
тип продукта.
"""
# Вызываем фабричный метод, чтобы получить объект-продукт.
product = self.factory_method()
# Далее, работаем с этим продуктом.
result = f"Creator: The same creator's code has just worked with {product.operation()}"
return result
"""
Конкретные Создатели переопределяют фабричный метод для того, чтобы изменить тип
результирующего продукта.
"""
class ConcreteCreator1(Creator):
"""
Обратите внимание, что сигнатура метода по-прежнему использует тип
абстрактного продукта, хотя фактически из метода возвращается конкретный
продукт. Таким образом, Создатель может оставаться независимым от конкретных
классов продуктов.
"""
def factory_method(self) -> Product:
return ConcreteProduct1()
class ConcreteCreator2(Creator):
def factory_method(self) -> Product:
return ConcreteProduct2()
class Product(ABC):
"""
Интерфейс Продукта объявляет операции, которые должны выполнять все
конкретные продукты.
"""
@abstractmethod
def operation(self) -> str:
pass
"""
Конкретные Продукты предоставляют различные реализации интерфейса Продукта.
"""
class ConcreteProduct1(Product):
def operation(self) -> str:
return "{Result of the ConcreteProduct1}"
class ConcreteProduct2(Product):
def operation(self) -> str:
return "{Result of the ConcreteProduct2}"
def client_code(creator: Creator) -> None:
"""
Клиентский код работает с экземпляром конкретного создателя, хотя и через
его базовый интерфейс. Пока клиент продолжает работать с создателем через
базовый интерфейс, вы можете передать ему любой подкласс создателя.
"""
print(f"Client: I'm not aware of the creator's class, but it still works.\n"
f"{creator.some_operation()}", end="")
if __name__ == "__main__":
print("App: Launched with the ConcreteCreator1.")
client_code(ConcreteCreator1())
print("\n")
print("App: Launched with the ConcreteCreator2.")
client_code(ConcreteCreator2())
Прототип
Прототип — это порождающий паттерн проектирования, который позволяет копировать объекты, не вдаваясь в подробности их реализации.
Позволяет клонировать объекты, не привязываясь к их конкретным классам.
Меньше повторяющегося кода инициализации объектов.
Ускоряет создание объектов.
Альтернатива созданию подклассов для конструирования сложных объектов.
import copy
class SelfReferencingEntity:
def __init__(self):
self.parent = None
def set_parent(self, parent):
self.parent = parent
class SomeComponent:
"""
Python provides its own interface of Prototype via `copy.copy` and
`copy.deepcopy` functions. And any class that wants to implement custom
implementations have to override `__copy__` and `__deepcopy__` member
functions.
"""
def __init__(self, some_int, some_list_of_objects, some_circular_ref):
self.some_int = some_int
self.some_list_of_objects = some_list_of_objects
self.some_circular_ref = some_circular_ref
def __copy__(self):
"""
Create a shallow copy. This method will be called whenever someone calls
`copy.copy` with this object and the returned value is returned as the
new shallow copy.
"""
# First, let's create copies of the nested objects.
some_list_of_objects = copy.copy(self.some_list_of_objects)
some_circular_ref = copy.copy(self.some_circular_ref)
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref
)
new.__dict__.update(self.__dict__)
return new
def __deepcopy__(self, memo=None):
"""
Create a deep copy. This method will be called whenever someone calls
`copy.deepcopy` with this object and the returned value is returned as
the new deep copy.
What is the use of the argument `memo`? Memo is the dictionary that is
used by the `deepcopy` library to prevent infinite recursive copies in
instances of circular references. Pass it to all the `deepcopy` calls
you make in the `__deepcopy__` implementation to prevent infinite
recursions.
"""
if memo is None:
memo = {}
# First, let's create copies of the nested objects.
some_list_of_objects = copy.deepcopy(self.some_list_of_objects, memo)
some_circular_ref = copy.deepcopy(self.some_circular_ref, memo)
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref
)
new.__dict__ = copy.deepcopy(self.__dict__, memo)
return new
if __name__ == "__main__":
list_of_objects = [1, {1, 2, 3}, [1, 2, 3]]
circular_ref = SelfReferencingEntity()
component = SomeComponent(23, list_of_objects, circular_ref)
circular_ref.set_parent(component)
shallow_copied_component = copy.copy(component)
# Let's change the list in shallow_copied_component and see if it changes in
# component.
shallow_copied_component.some_list_of_objects.append("another object")
if component.some_list_of_objects[-1] == "another object":
print(
"Adding elements to `shallow_copied_component`'s "
"some_list_of_objects adds it to `component`'s "
"some_list_of_objects."
)
else:
print(
"Adding elements to `shallow_copied_component`'s "
"some_list_of_objects doesn't add it to `component`'s "
"some_list_of_objects."
)
# Let's change the set in the list of objects.
component.some_list_of_objects[1].add(4)
if 4 in shallow_copied_component.some_list_of_objects[1]:
print(
"Changing objects in the `component`'s some_list_of_objects "
"changes that object in `shallow_copied_component`'s "
"some_list_of_objects."
)
else:
print(
"Changing objects in the `component`'s some_list_of_objects "
"doesn't change that object in `shallow_copied_component`'s "
"some_list_of_objects."
)
deep_copied_component = copy.deepcopy(component)
# Let's change the list in deep_copied_component and see if it changes in
# component.
deep_copied_component.some_list_of_objects.append("one more object")
if component.some_list_of_objects[-1] == "one more object":
print(
"Adding elements to `deep_copied_component`'s "
"some_list_of_objects adds it to `component`'s "
"some_list_of_objects."
)
else:
print(
"Adding elements to `deep_copied_component`'s "
"some_list_of_objects doesn't add it to `component`'s "
"some_list_of_objects."
)
# Let's change the set in the list of objects.
component.some_list_of_objects[1].add(10)
if 10 in deep_copied_component.some_list_of_objects[1]:
print(
"Changing objects in the `component`'s some_list_of_objects "
"changes that object in `deep_copied_component`'s "
"some_list_of_objects."
)
else:
print(
"Changing objects in the `component`'s some_list_of_objects "
"doesn't change that object in `deep_copied_component`'s "
"some_list_of_objects."
)
print(
f"id(deep_copied_component.some_circular_ref.parent): "
f"{id(deep_copied_component.some_circular_ref.parent)}"
)
print(
f"id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent): "
f"{id(deep_copied_component.some_circular_ref.parent.some_circular_ref.parent)}"
)
print(
"^^ This shows that deepcopied objects contain same reference, they "
"are not cloned repeatedly."
)
Одиночка \ Singleton \ Синглтон
Одиночка — это порождающий паттерн проектирования, который гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа.
Гарантирует наличие единственного экземпляра класса.
Предоставляет к нему глобальную точку доступа.
Реализует отложенную инициализацию объекта-одиночки.
class SingletonMeta(type):
"""
В Python класс Одиночка можно реализовать по-разному. Возможные способы
включают себя базовый класс, декоратор, метакласс. Мы воспользуемся
метаклассом, поскольку он лучше всего подходит для этой цели.
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Данная реализация не учитывает возможное изменение передаваемых
аргументов в `__init__`.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
def some_business_logic(self):
"""
Наконец, любой одиночка должен содержать некоторую бизнес-логику,
которая может быть выполнена на его экземпляре.
"""
# ...
if __name__ == "__main__":
# Клиентский код.
s1 = Singleton()
s2 = Singleton()
if id(s1) == id(s2):
print("Singleton works, both variables contain the same instance.")
else:
print("Singleton failed, variables contain different instances.")
Адаптер
Адаптер — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе. Отделяет и скрывает от клиента подробности преобразования различных интерфейсов. Усложняет код программы из-за введения дополнительных классов.
class Target:
"""
Целевой класс объявляет интерфейс, с которым может работать клиентский код.
"""
def request(self) -> str:
return "Target: The default target's behavior."
class Adaptee:
"""
Адаптируемый класс содержит некоторое полезное поведение, но его интерфейс
несовместим с существующим клиентским кодом. Адаптируемый класс нуждается в
некоторой доработке, прежде чем клиентский код сможет его использовать.
"""
def specific_request(self) -> str:
return ".eetpadA eht fo roivaheb laicepS"
class Adapter(Target, Adaptee):
"""
Адаптер делает интерфейс Адаптируемого класса совместимым с целевым
интерфейсом благодаря множественному наследованию.
"""
def request(self) -> str:
return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
def client_code(target: "Target") -> None:
"""
Клиентский код поддерживает все классы, использующие интерфейс Target.
"""
print(target.request(), end="")
if __name__ == "__main__":
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)
print("\n")
adaptee = Adaptee()
print("Client: The Adaptee class has a weird interface. "
"See, I don't understand it:")
print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")
print("Client: But I can work with it via the Adapter:")
adapter = Adapter()
client_code(adapter)
Компоновщик
Компоновщик — это структурный паттерн проектирования, который позволяет сгруппировать множество объектов в древовидную структуру, а затем работать с ней так, как будто это единичный объект.
Упрощает архитектуру клиента при работе со сложным деревом компонентов.
Облегчает добавление новых видов компонентов.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
class Component(ABC):
"""
Базовый класс Компонент объявляет общие операции как для простых, так и для
сложных объектов структуры.
"""
@property
def parent(self) -> Component:
return self._parent
@parent.setter
def parent(self, parent: Component):
"""
При необходимости базовый Компонент может объявить интерфейс для
установки и получения родителя компонента в древовидной структуре. Он
также может предоставить некоторую реализацию по умолчанию для этих
методов.
"""
self._parent = parent
"""
В некоторых случаях целесообразно определить операции управления потомками
прямо в базовом классе Компонент. Таким образом, вам не нужно будет
предоставлять конкретные классы компонентов клиентскому коду, даже во время
сборки дерева объектов. Недостаток такого подхода в том, что эти методы
будут пустыми для компонентов уровня листа.
"""
def add(self, component: Component) -> None:
pass
def remove(self, component: Component) -> None:
pass
def is_composite(self) -> bool:
"""
Вы можете предоставить метод, который позволит клиентскому коду понять,
может ли компонент иметь вложенные объекты.
"""
return False
@abstractmethod
def operation(self) -> str:
"""
Базовый Компонент может сам реализовать некоторое поведение по умолчанию
или поручить это конкретным классам, объявив метод, содержащий поведение
абстрактным.
"""
pass
class Leaf(Component):
"""
Класс Лист представляет собой конечные объекты структуры. Лист не может
иметь вложенных компонентов.
Обычно объекты Листьев выполняют фактическую работу, тогда как объекты
Контейнера лишь делегируют работу своим подкомпонентам.
"""
def operation(self) -> str:
return "Leaf"
class Composite(Component):
"""
Класс Контейнер содержит сложные компоненты, которые могут иметь вложенные
компоненты. Обычно объекты Контейнеры делегируют фактическую работу своим
детям, а затем «суммируют» результат.
"""
def __init__(self) -> None:
self._children: List[Component] = []
"""
Объект контейнера может как добавлять компоненты в свой список вложенных
компонентов, так и удалять их, как простые, так и сложные.
"""
def add(self, component: Component) -> None:
self._children.append(component)
component.parent = self
def remove(self, component: Component) -> None:
self._children.remove(component)
component.parent = None
def is_composite(self) -> bool:
return True
def operation(self) -> str:
"""
Контейнер выполняет свою основную логику особым образом. Он проходит
рекурсивно через всех своих детей, собирая и суммируя их результаты.
Поскольку потомки контейнера передают эти вызовы своим потомкам и так
далее, в результате обходится всё дерево объектов.
"""
results = []
for child in self._children:
results.append(child.operation())
return f"Branch({'+'.join(results)})"
def client_code(component: Component) -> None:
"""
Клиентский код работает со всеми компонентами через базовый интерфейс.
"""
print(f"RESULT: {component.operation()}", end="")
def client_code2(component1: Component, component2: Component) -> None:
"""
Благодаря тому, что операции управления потомками объявлены в базовом классе
Компонента, клиентский код может работать как с простыми, так и со сложными
компонентами, вне зависимости от их конкретных классов.
"""
if component1.is_composite():
component1.add(component2)
print(f"RESULT: {component1.operation()}", end="")
if __name__ == "__main__":
# Таким образом, клиентский код может поддерживать простые компоненты-
# листья...
simple = Leaf()
print("Client: I've got a simple component:")
client_code(simple)
print("\n")
# ...а также сложные контейнеры.
tree = Composite()
branch1 = Composite()
branch1.add(Leaf())
branch1.add(Leaf())
branch2 = Composite()
branch2.add(Leaf())
tree.add(branch1)
tree.add(branch2)
print("Client: Now I've got a composite tree:")
client_code(tree)
print("\n")
print("Client: I don't need to check the components classes even when managing the tree:")
client_code2(tree, simple)
Декоратор
Декоратор — это структурный паттерн проектирования, который позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки».
Большая гибкость, чем у наследования.
Позволяет добавлять обязанности на лету.
Можно добавлять несколько новых обязанностей сразу.
Позволяет иметь несколько мелких объектов вместо одного объекта на все случаи жизни.
class Component():
"""
Базовый интерфейс Компонента определяет поведение, которое изменяется
декораторами.
"""
def operation(self) -> str:
pass
class ConcreteComponent(Component):
"""
Конкретные Компоненты предоставляют реализации поведения по умолчанию. Может
быть несколько вариаций этих классов.
"""
def operation(self) -> str:
return "ConcreteComponent"
class Decorator(Component):
"""
Базовый класс Декоратора следует тому же интерфейсу, что и другие
компоненты. Основная цель этого класса - определить интерфейс обёртки для
всех конкретных декораторов. Реализация кода обёртки по умолчанию может
включать в себя поле для хранения завёрнутого компонента и средства его
инициализации.
"""
_component: Component = None
def __init__(self, component: Component) -> None:
self._component = component
@property
def component(self) -> Component:
"""
Декоратор делегирует всю работу обёрнутому компоненту.
"""
return self._component
def operation(self) -> str:
return self._component.operation()
class ConcreteDecoratorA(Decorator):
"""
Конкретные Декораторы вызывают обёрнутый объект и изменяют его результат
некоторым образом.
"""
def operation(self) -> str:
"""
Декораторы могут вызывать родительскую реализацию операции, вместо того,
чтобы вызвать обёрнутый объект напрямую. Такой подход упрощает
расширение классов декораторов.
"""
return f"ConcreteDecoratorA({self.component.operation()})"
class ConcreteDecoratorB(Decorator):
"""
Декораторы могут выполнять своё поведение до или после вызова обёрнутого
объекта.
"""
def operation(self) -> str:
return f"ConcreteDecoratorB({self.component.operation()})"
def client_code(component: Component) -> None:
"""
Клиентский код работает со всеми объектами, используя интерфейс Компонента.
Таким образом, он остаётся независимым от конкретных классов компонентов, с
которыми работает.
"""
# ...
print(f"RESULT: {component.operation()}", end="")
# ...
if __name__ == "__main__":
# Таким образом, клиентский код может поддерживать как простые компоненты...
simple = ConcreteComponent()
print("Client: I've got a simple component:")
client_code(simple)
print("\n")
# ...так и декорированные.
#
# Обратите внимание, что декораторы могут обёртывать не только простые
# компоненты, но и другие декораторы.
decorator1 = ConcreteDecoratorA(simple)
decorator2 = ConcreteDecoratorB(decorator1)
print("Client: Now I've got a decorated component:")
client_code(decorator2)
Facade/Фасад
Фасад — это структурный паттерн проектирования, который предоставляет простой интерфейс к сложной системе классов, библиотеке или фреймворку.
Изолирует клиентов от компонентов сложной подсистемы. Фасад рискует стать божественным объектом, привязанным ко всем классам программы.
from __future__ import annotations
class Facade:
"""
Класс Фасада предоставляет простой интерфейс для сложной логики одной или
нескольких подсистем. Фасад делегирует запросы клиентов соответствующим
объектам внутри подсистемы. Фасад также отвечает за управление их жизненным
циклом. Все это защищает клиента от нежелательной сложности подсистемы.
"""
def __init__(self, subsystem1: Subsystem1, subsystem2: Subsystem2) -> None:
"""
В зависимости от потребностей вашего приложения вы можете предоставить
Фасаду существующие объекты подсистемы или заставить Фасад создать их
самостоятельно.
"""
self._subsystem1 = subsystem1 or Subsystem1()
self._subsystem2 = subsystem2 or Subsystem2()
def operation(self) -> str:
"""
Методы Фасада удобны для быстрого доступа к сложной функциональности
подсистем. Однако клиенты получают только часть возможностей подсистемы.
"""
results = []
results.append("Facade initializes subsystems:")
results.append(self._subsystem1.operation1())
results.append(self._subsystem2.operation1())
results.append("Facade orders subsystems to perform the action:")
results.append(self._subsystem1.operation_n())
results.append(self._subsystem2.operation_z())
return "\n".join(results)
class Subsystem1:
"""
Подсистема может принимать запросы либо от фасада, либо от клиента напрямую.
В любом случае, для Подсистемы Фасад – это ещё один клиент, и он не является
частью Подсистемы.
"""
def operation1(self) -> str:
return "Subsystem1: Ready!"
# ...
def operation_n(self) -> str:
return "Subsystem1: Go!"
class Subsystem2:
"""
Некоторые фасады могут работать с разными подсистемами одновременно.
"""
def operation1(self) -> str:
return "Subsystem2: Get ready!"
# ...
def operation_z(self) -> str:
return "Subsystem2: Fire!"
def client_code(facade: Facade) -> None:
"""
Клиентский код работает со сложными подсистемами через простой интерфейс,
предоставляемый Фасадом. Когда фасад управляет жизненным циклом подсистемы,
клиент может даже не знать о существовании подсистемы. Такой подход
позволяет держать сложность под контролем.
"""
print(facade.operation(), end="")
if __name__ == "__main__":
# В клиентском коде могут быть уже созданы некоторые объекты подсистемы. В
# этом случае может оказаться целесообразным инициализировать Фасад с этими
# объектами вместо того, чтобы позволить Фасаду создавать новые экземпляры.
subsystem1 = Subsystem1()
subsystem2 = Subsystem2()
facade = Facade(subsystem1, subsystem2)
client_code(facade)
Мост
Мост — это структурный паттерн проектирования, который разделяет один или несколько классов на две отдельные иерархии — абстракцию и реализацию, позволяя изменять их независимо друг от друга.
Позволяет строить платформо-независимые программы.
Скрывает лишние или опасные детали реализации от клиентского кода.
Реализует принцип открытости/закрытости.
from __future__ import annotations
from abc import ABC, abstractmethod
class Abstraction:
"""
Абстракция устанавливает интерфейс для «управляющей» части двух иерархий
классов. Она содержит ссылку на объект из иерархии Реализации и делегирует
ему всю настоящую работу.
"""
def __init__(self, implementation: Implementation) -> None:
self.implementation = implementation
def operation(self) -> str:
return (f"Abstraction: Base operation with:\n"
f"{self.implementation.operation_implementation()}")
class ExtendedAbstraction(Abstraction):
"""
Можно расширить Абстракцию без изменения классов Реализации.
"""
def operation(self) -> str:
return (f"ExtendedAbstraction: Extended operation with:\n"
f"{self.implementation.operation_implementation()}")
class Implementation(ABC):
"""
Реализация устанавливает интерфейс для всех классов реализации. Он не должен
соответствовать интерфейсу Абстракции. На практике оба интерфейса могут быть
совершенно разными. Как правило, интерфейс Реализации предоставляет только
примитивные операции, в то время как Абстракция определяет операции более
высокого уровня, основанные на этих примитивах.
"""
@abstractmethod
def operation_implementation(self) -> str:
pass
"""
Каждая Конкретная Реализация соответствует определённой платформе и реализует
интерфейс Реализации с использованием API этой платформы.
"""
class ConcreteImplementationA(Implementation):
def operation_implementation(self) -> str:
return "ConcreteImplementationA: Here's the result on the platform A."
class ConcreteImplementationB(Implementation):
def operation_implementation(self) -> str:
return "ConcreteImplementationB: Here's the result on the platform B."
def client_code(abstraction: Abstraction) -> None:
"""
За исключением этапа инициализации, когда объект Абстракции связывается с
определённым объектом Реализации, клиентский код должен зависеть только от
класса Абстракции. Таким образом, клиентский код может поддерживать любую
комбинацию абстракции и реализации.
"""
# ...
print(abstraction.operation(), end="")
# ...
if __name__ == "__main__":
"""
Клиентский код должен работать с любой предварительно сконфигурированной
комбинацией абстракции и реализации.
"""
implementation = ConcreteImplementationA()
abstraction = Abstraction(implementation)
client_code(abstraction)
print("\n")
implementation = ConcreteImplementationB()
abstraction = ExtendedAbstraction(implementation)
client_code(abstraction)
Lightweight / легковес
Легковес — это структурный паттерн проектирования, который позволяет вместить бóльшее количество объектов в отведённую оперативную память. Легковес экономит память, разделяя общее состояние объектов между собой, вместо хранения одинаковых данных в каждом объекте.
Экономит оперативную память. Расходует процессорное время на поиск/вычисление контекста. Усложняет код программы из-за введения множества дополнительных классов.
import json
from typing import Dict
class Flyweight():
"""
Легковес хранит общую часть состояния (также называемую внутренним
состоянием), которая принадлежит нескольким реальным бизнес-объектам.
Легковес принимает оставшуюся часть состояния (внешнее состояние, уникальное
для каждого объекта) через его параметры метода.
"""
def __init__(self, shared_state: str) -> None:
self._shared_state = shared_state
def operation(self, unique_state: str) -> None:
s = json.dumps(self._shared_state)
u = json.dumps(unique_state)
print(f"Flyweight: Displaying shared ({s}) and unique ({u}) state.", end="")
class FlyweightFactory():
"""
Фабрика Легковесов создает объекты-Легковесы и управляет ими. Она
обеспечивает правильное разделение легковесов. Когда клиент запрашивает
легковес, фабрика либо возвращает существующий экземпляр, либо создает
новый, если он ещё не существует.
"""
_flyweights: Dict[str, Flyweight] = {}
def __init__(self, initial_flyweights: Dict) -> None:
for state in initial_flyweights:
self._flyweights[self.get_key(state)] = Flyweight(state)
def get_key(self, state: Dict) -> str:
"""
Возвращает хеш строки Легковеса для данного состояния.
"""
return "_".join(sorted(state))
def get_flyweight(self, shared_state: Dict) -> Flyweight:
"""
Возвращает существующий Легковес с заданным состоянием или создает
новый.
"""
key = self.get_key(shared_state)
if not self._flyweights.get(key):
print("FlyweightFactory: Can't find a flyweight, creating new one.")
self._flyweights[key] = Flyweight(shared_state)
else:
print("FlyweightFactory: Reusing existing flyweight.")
return self._flyweights[key]
def list_flyweights(self) -> None:
count = len(self._flyweights)
print(f"FlyweightFactory: I have {count} flyweights:")
print("\n".join(map(str, self._flyweights.keys())), end="")
def add_car_to_police_database(
factory: FlyweightFactory, plates: str, owner: str,
brand: str, model: str, color: str
) -> None:
print("\n\nClient: Adding a car to database.")
flyweight = factory.get_flyweight([brand, model, color])
# Клиентский код либо сохраняет, либо вычисляет внешнее состояние и передает
# его методам легковеса.
flyweight.operation([plates, owner])
if __name__ == "__main__":
"""
Клиентский код обычно создает кучу предварительно заполненных легковесов на
этапе инициализации приложения.
"""
factory = FlyweightFactory([
["Chevrolet", "Camaro2018", "pink"],
["Mercedes Benz", "C300", "black"],
["Mercedes Benz", "C500", "red"],
["BMW", "M5", "red"],
["BMW", "X6", "white"],
])
factory.list_flyweights()
add_car_to_police_database(
factory, "CL234IR", "James Doe", "BMW", "M5", "red")
add_car_to_police_database(
factory, "CL234IR", "James Doe", "BMW", "X1", "red")
print("\n")
factory.list_flyweights()
Прокси/заместитель/суррогат
Заместитель — это структурный паттерн проектирования, который позволяет подставлять вместо реальных объектов специальные объекты-заменители. Эти объекты перехватывают вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.
Позволяет контролировать сервисный объект незаметно для клиента. Может работать, даже если сервисный объект ещё не создан. Может контролировать жизненный цикл служебного объекта. Усложняет код программы из-за введения дополнительных классов. Увеличивает время отклика от сервиса.
from abc import ABC, abstractmethod
class Subject(ABC):
"""
Интерфейс Субъекта объявляет общие операции как для Реального Субъекта, так
и для Заместителя. Пока клиент работает с Реальным Субъектом, используя этот
интерфейс, вы сможете передать ему заместителя вместо реального субъекта.
"""
@abstractmethod
def request(self) -> None:
pass
class RealSubject(Subject):
"""
Реальный Субъект содержит некоторую базовую бизнес-логику. Как правило,
Реальные Субъекты способны выполнять некоторую полезную работу, которая к
тому же может быть очень медленной или точной – например, коррекция входных
данных. Заместитель может решить эти задачи без каких-либо изменений в коде
Реального Субъекта.
"""
def request(self) -> None:
print("RealSubject: Handling request.")
class Proxy(Subject):
"""
Интерфейс Заместителя идентичен интерфейсу Реального Субъекта.
"""
def __init__(self, real_subject: RealSubject) -> None:
self._real_subject = real_subject
def request(self) -> None:
"""
Наиболее распространёнными областями применения паттерна Заместитель
являются ленивая загрузка, кэширование, контроль доступа, ведение
журнала и т.д. Заместитель может выполнить одну из этих задач, а затем,
в зависимости от результата, передать выполнение одноимённому методу в
связанном объекте класса Реального Субъекта.
"""
if self.check_access():
self._real_subject.request()
self.log_access()
def check_access(self) -> bool:
print("Proxy: Checking access prior to firing a real request.")
return True
def log_access(self) -> None:
print("Proxy: Logging the time of request.", end="")
def client_code(subject: Subject) -> None:
"""
Клиентский код должен работать со всеми объектами (как с реальными, так и
заместителями) через интерфейс Субъекта, чтобы поддерживать как реальные
субъекты, так и заместителей. В реальной жизни, однако, клиенты в основном
работают с реальными субъектами напрямую. В этом случае, для более простой
реализации паттерна, можно расширить заместителя из класса реального
субъекта.
"""
# ...
subject.request()
# ...
if __name__ == "__main__":
print("Client: Executing the client code with a real subject:")
real_subject = RealSubject()
client_code(real_subject)
print("")
print("Client: Executing the same client code with a proxy:")
proxy = Proxy(real_subject)
client_code(proxy)
Команда
Команда — это поведенческий паттерн проектирования, который превращает запросы в объекты, позволяя передавать их как аргументы при вызове методов, ставить запросы в очередь, логировать их, а также поддерживать отмену операций.
Убирает прямую зависимость между объектами, вызывающими операции, и объектами, которые их непосредственно выполняют. Позволяет реализовать простую отмену и повтор операций. Позволяет реализовать отложенный запуск операций. Позволяет собирать сложные команды из простых. Реализует принцип открытости/закрытости.
from __future__ import annotations
from abc import ABC, abstractmethod
class Command(ABC):
"""
Интерфейс Команды объявляет метод для выполнения команд.
"""
@abstractmethod
def execute(self) -> None:
pass
class SimpleCommand(Command):
"""
Некоторые команды способны выполнять простые операции самостоятельно.
"""
def __init__(self, payload: str) -> None:
self._payload = payload
def execute(self) -> None:
print(f"SimpleCommand: See, I can do simple things like printing"
f"({self._payload})")
class ComplexCommand(Command):
"""
Но есть и команды, которые делегируют более сложные операции другим
объектам, называемым «получателями».
"""
def __init__(self, receiver: Receiver, a: str, b: str) -> None:
"""
Сложные команды могут принимать один или несколько объектов-получателей
вместе с любыми данными о контексте через конструктор.
"""
self._receiver = receiver
self._a = a
self._b = b
def execute(self) -> None:
"""
Команды могут делегировать выполнение любым методам получателя.
"""
print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)
class Receiver:
"""
Классы Получателей содержат некую важную бизнес-логику. Они умеют выполнять
все виды операций, связанных с выполнением запроса. Фактически, любой класс
может выступать Получателем.
"""
def do_something(self, a: str) -> None:
print(f"\nReceiver: Working on ({a}.)", end="")
def do_something_else(self, b: str) -> None:
print(f"\nReceiver: Also working on ({b}.)", end="")
class Invoker:
"""
Отправитель связан с одной или несколькими командами. Он отправляет запрос
команде.
"""
_on_start = None
_on_finish = None
"""
Инициализация команд.
"""
def set_on_start(self, command: Command):
self._on_start = command
def set_on_finish(self, command: Command):
self._on_finish = command
def do_something_important(self) -> None:
"""
Отправитель не зависит от классов конкретных команд и получателей.
Отправитель передаёт запрос получателю косвенно, выполняя команду.
"""
print("Invoker: Does anybody want something done before I begin?")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: ...doing something really important...")
print("Invoker: Does anybody want something done after I finish?")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
if __name__ == "__main__":
"""
Клиентский код может параметризовать отправителя любыми командами.
"""
invoker = Invoker()
invoker.set_on_start(SimpleCommand("Say Hi!"))
receiver = Receiver()
invoker.set_on_finish(ComplexCommand(
receiver, "Send email", "Save report"))
invoker.do_something_important()
Итератор
Итератор — это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления. Упрощает классы хранения данных. Позволяет реализовать различные способы обхода структуры данных. Позволяет одновременно перемещаться по структуре данных в разные стороны. Не оправдан, если можно использовать обычный цикл.
from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import Any, List
"""
Для создания итератора в Python есть два абстрактных класса из встроенного
модуля collections - Iterable, Iterator. Нужно реализовать метод __iter__() в
итерируемом объекте (списке), а метод __next__() в итераторе.
"""
class AlphabeticalOrderIterator(Iterator):
"""
Конкретные Итераторы реализуют различные алгоритмы обхода. Эти классы
постоянно хранят текущее положение обхода.
"""
"""
Атрибут _position хранит текущее положение обхода. У итератора может быть
множество других полей для хранения состояния итерации, особенно когда он
должен работать с определённым типом коллекции.
"""
_position: int = None
"""
Этот атрибут указывает направление обхода.
"""
_reverse: bool = False
def __init__(self, collection: WordsCollection, reverse: bool = False) -> None:
self._collection = collection
self._reverse = reverse
self._position = -1 if reverse else 0
def __next__(self):
"""
Метод __next __() должен вернуть следующий элемент в последовательности.
При достижении конца коллекции и в последующих вызовах должно вызываться
исключение StopIteration.
"""
try:
value = self._collection[self._position]
self._position += -1 if self._reverse else 1
except IndexError:
raise StopIteration()
return value
class WordsCollection(Iterable):
"""
Конкретные Коллекции предоставляют один или несколько методов для получения
новых экземпляров итератора, совместимых с классом коллекции.
"""
def __init__(self, collection: List[Any] = []) -> None:
self._collection = collection
def __iter__(self) -> AlphabeticalOrderIterator:
"""
Метод __iter__() возвращает объект итератора, по умолчанию мы возвращаем
итератор с сортировкой по возрастанию.
"""
return AlphabeticalOrderIterator(self._collection)
def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection, True)
def add_item(self, item: Any):
self._collection.append(item)
if __name__ == "__main__":
# Клиентский код может знать или не знать о Конкретном Итераторе или классах
# Коллекций, в зависимости от уровня косвенности, который вы хотите
# сохранить в своей программе.
collection = WordsCollection()
collection.add_item("First")
collection.add_item("Second")
collection.add_item("Third")
print("Straight traversal:")
print("\n".join(collection))
print("")
print("Reverse traversal:")
print("\n".join(collection.get_reverse_iterator()), end="")
Наблюдатель
Наблюдатель — это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.
from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List
class Subject(ABC):
"""
Интферфейс издателя объявляет набор методов для управлениями подписчиками.
"""
@abstractmethod
def attach(self, observer: Observer) -> None:
"""
Присоединяет наблюдателя к издателю.
"""
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
"""
Отсоединяет наблюдателя от издателя.
"""
pass
@abstractmethod
def notify(self) -> None:
"""
Уведомляет всех наблюдателей о событии.
"""
pass
class ConcreteSubject(Subject):
"""
Издатель владеет некоторым важным состоянием и оповещает наблюдателей о его
изменениях.
"""
_state: int = None
"""
Для удобства в этой переменной хранится состояние Издателя, необходимое всем
подписчикам.
"""
_observers: List[Observer] = []
"""
Список подписчиков. В реальной жизни список подписчиков может храниться в
более подробном виде (классифицируется по типу события и т.д.)
"""
def attach(self, observer: Observer) -> None:
print("Subject: Attached an observer.")
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
"""
Методы управления подпиской.
"""
def notify(self) -> None:
"""
Запуск обновления в каждом подписчике.
"""
print("Subject: Notifying observers...")
for observer in self._observers:
observer.update(self)
def some_business_logic(self) -> None:
"""
Обычно логика подписки – только часть того, что делает Издатель.
Издатели часто содержат некоторую важную бизнес-логику, которая
запускает метод уведомления всякий раз, когда должно произойти что-то
важное (или после этого).
"""
print("\nSubject: I'm doing something important.")
self._state = randrange(0, 10)
print(f"Subject: My state has just changed to: {self._state}")
self.notify()
class Observer(ABC):
"""
Интерфейс Наблюдателя объявляет метод уведомления, который издатели
используют для оповещения своих подписчиков.
"""
@abstractmethod
def update(self, subject: Subject) -> None:
"""
Получить обновление от субъекта.
"""
pass
"""
Конкретные Наблюдатели реагируют на обновления, выпущенные Издателем, к которому
они прикреплены.
"""
class ConcreteObserverA(Observer):
def update(self, subject: Subject) -> None:
if subject._state < 3:
print("ConcreteObserverA: Reacted to the event")
class ConcreteObserverB(Observer):
def update(self, subject: Subject) -> None:
if subject._state == 0 or subject._state >= 2:
print("ConcreteObserverB: Reacted to the event")
if __name__ == "__main__":
# Клиентский код.
subject = ConcreteSubject()
observer_a = ConcreteObserverA()
subject.attach(observer_a)
observer_b = ConcreteObserverB()
subject.attach(observer_b)
subject.some_business_logic()
subject.some_business_logic()
subject.detach(observer_a)
subject.some_business_logic()
Стратегия
Стратегия — это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
class Context():
"""
Контекст определяет интерфейс, представляющий интерес для клиентов.
"""
def __init__(self, strategy: Strategy) -> None:
"""
Обычно Контекст принимает стратегию через конструктор, а также
предоставляет сеттер для её изменения во время выполнения.
"""
self._strategy = strategy
@property
def strategy(self) -> Strategy:
"""
Контекст хранит ссылку на один из объектов Стратегии. Контекст не знает
конкретного класса стратегии. Он должен работать со всеми стратегиями
через интерфейс Стратегии.
"""
return self._strategy
@strategy.setter
def strategy(self, strategy: Strategy) -> None:
"""
Обычно Контекст позволяет заменить объект Стратегии во время выполнения.
"""
self._strategy = strategy
def do_some_business_logic(self) -> None:
"""
Вместо того, чтобы самостоятельно реализовывать множественные версии
алгоритма, Контекст делегирует некоторую работу объекту Стратегии.
"""
# ...
print("Context: Sorting data using the strategy (not sure how it'll do it)")
result = self._strategy.do_algorithm(["a", "b", "c", "d", "e"])
print(",".join(result))
# ...
class Strategy(ABC):
"""
Интерфейс Стратегии объявляет операции, общие для всех поддерживаемых версий
некоторого алгоритма.
Контекст использует этот интерфейс для вызова алгоритма, определённого
Конкретными Стратегиями.
"""
@abstractmethod
def do_algorithm(self, data: List):
pass
"""
Конкретные Стратегии реализуют алгоритм, следуя базовому интерфейсу Стратегии.
Этот интерфейс делает их взаимозаменяемыми в Контексте.
"""
class ConcreteStrategyA(Strategy):
def do_algorithm(self, data: List) -> List:
return sorted(data)
class ConcreteStrategyB(Strategy):
def do_algorithm(self, data: List) -> List:
return reversed(sorted(data))
if __name__ == "__main__":
# Клиентский код выбирает конкретную стратегию и передаёт её в контекст.
# Клиент должен знать о различиях между стратегиями, чтобы сделать
# правильный выбор.
context = Context(ConcreteStrategyA())
print("Client: Strategy is set to normal sorting.")
context.do_some_business_logic()
print()
print("Client: Strategy is set to reverse sorting.")
context.strategy = ConcreteStrategyB()
context.do_some_business_logic()
Посредник
Посредник — это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.
from __future__ import annotations
from abc import ABC
class Mediator(ABC):
"""
Интерфейс Посредника предоставляет метод, используемый компонентами для
уведомления посредника о различных событиях. Посредник может реагировать на
эти события и передавать исполнение другим компонентам.
"""
def notify(self, sender: object, event: str) -> None:
pass
class ConcreteMediator(Mediator):
def __init__(self, component1: Component1, component2: Component2) -> None:
self._component1 = component1
self._component1.mediator = self
self._component2 = component2
self._component2.mediator = self
def notify(self, sender: object, event: str) -> None:
if event == "A":
print("Mediator reacts on A and triggers following operations:")
self._component2.do_c()
elif event == "D":
print("Mediator reacts on D and triggers following operations:")
self._component1.do_b()
self._component2.do_c()
class BaseComponent:
"""
Базовый Компонент обеспечивает базовую функциональность хранения экземпляра
посредника внутри объектов компонентов.
"""
def __init__(self, mediator: Mediator = None) -> None:
self._mediator = mediator
@property
def mediator(self) -> Mediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: Mediator) -> None:
self._mediator = mediator
"""
Конкретные Компоненты реализуют различную функциональность. Они не зависят от
других компонентов. Они также не зависят от каких-либо конкретных классов
посредников.
"""
class Component1(BaseComponent):
def do_a(self) -> None:
print("Component 1 does A.")
self.mediator.notify(self, "A")
def do_b(self) -> None:
print("Component 1 does B.")
self.mediator.notify(self, "B")
class Component2(BaseComponent):
def do_c(self) -> None:
print("Component 2 does C.")
self.mediator.notify(self, "C")
def do_d(self) -> None:
print("Component 2 does D.")
self.mediator.notify(self, "D")
if __name__ == "__main__":
# Клиентский код.
c1 = Component1()
c2 = Component2()
mediator = ConcreteMediator(c1, c2)
print("Client triggers operation A.")
c1.do_a()
print("\n", end="")
print("Client triggers operation D.")
c2.do_d()
Шаблонный метод
Шаблонный метод — это поведенческий паттерн проектирования, который определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Паттерн позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры.
Облегчает повторное использование кода.
Но есть минусы: вы жёстко ограничены скелетом существующего алгоритма. Вы можете нарушить принцип подстановки Барбары Лисков, изменяя базовое поведение одного из шагов алгоритма через подкласс. С ростом количества шагов шаблонный метод становится слишком сложно поддерживать.
from abc import ABC, abstractmethod
class AbstractClass(ABC):
"""
Абстрактный Класс определяет шаблонный метод, содержащий скелет некоторого
алгоритма, состоящего из вызовов (обычно) абстрактных примитивных операций.
Конкретные подклассы должны реализовать эти операции, но оставить сам
шаблонный метод без изменений.
"""
def template_method(self) -> None:
"""
Шаблонный метод определяет скелет алгоритма.
"""
self.base_operation1()
self.required_operations1()
self.base_operation2()
self.hook1()
self.required_operations2()
self.base_operation3()
self.hook2()
# Эти операции уже имеют реализации.
def base_operation1(self) -> None:
print("AbstractClass says: I am doing the bulk of the work")
def base_operation2(self) -> None:
print("AbstractClass says: But I let subclasses override some operations")
def base_operation3(self) -> None:
print("AbstractClass says: But I am doing the bulk of the work anyway")
# А эти операции должны быть реализованы в подклассах.
@abstractmethod
def required_operations1(self) -> None:
pass
@abstractmethod
def required_operations2(self) -> None:
pass
# Это «хуки». Подклассы могут переопределять их, но это не обязательно,
# поскольку у хуков уже есть стандартная (но пустая) реализация. Хуки
# предоставляют дополнительные точки расширения в некоторых критических
# местах алгоритма.
def hook1(self) -> None:
pass
def hook2(self) -> None:
pass
class ConcreteClass1(AbstractClass):
"""
Конкретные классы должны реализовать все абстрактные операции базового
класса. Они также могут переопределить некоторые операции с реализацией по
умолчанию.
"""
def required_operations1(self) -> None:
print("ConcreteClass1 says: Implemented Operation1")
def required_operations2(self) -> None:
print("ConcreteClass1 says: Implemented Operation2")
class ConcreteClass2(AbstractClass):
"""
Обычно конкретные классы переопределяют только часть операций базового
класса.
"""
def required_operations1(self) -> None:
print("ConcreteClass2 says: Implemented Operation1")
def required_operations2(self) -> None:
print("ConcreteClass2 says: Implemented Operation2")
def hook1(self) -> None:
print("ConcreteClass2 says: Overridden Hook1")
def client_code(abstract_class: AbstractClass) -> None:
"""
Клиентский код вызывает шаблонный метод для выполнения алгоритма. Клиентский
код не должен знать конкретный класс объекта, с которым работает, при
условии, что он работает с объектами через интерфейс их базового класса.
"""
# ...
abstract_class.template_method()
# ...
if __name__ == "__main__":
print("Same client code can work with different subclasses:")
client_code(ConcreteClass1())
print("")
print("Same client code can work with different subclasses:")
client_code(ConcreteClass2())
Следующий текст взят из статьи
TDD
Разработка через тестирование (tdd) — это вид разработки, который предусматривает написание автоматических тестов перед написанием самой функции. Иными словами, это комбинация испытания и написания кода. Этот процесс не только помогает обеспечить корректность кода, но также позволяет развивать дизайн и архитектуру проекта под постоянным контролем.
TDD — это методология разработки ПО, которая основывается на повторении коротких циклов разработки: изначально пишется тест, покрывающий желаемое изменение, затем пишется программный код, который реализует желаемое поведение системы и позволит пройти написанный тест. Затем проводится рефакторинг написанного кода с постоянной проверкой прохождения тестов.
Звучит просто и понятно. Многим знаком такой подход к разработке и даже сам "Uncle Bob" активно его пропагандирует.
TDD считается одной из форм правильного метода построения приложения. Философия разработки на основе тестов заключается в том, что ваши тесты являются спецификацией того, как ваша программа должна вести себя. Если вы рассматриваете свой набор тестов как обязательную часть процесса сборки, если ваши тесты не проходят, программа не собирается, потому что она неверна. Конечно, ограничение заключается в том, что правильность вашей программы определена только как полнота ваших тестов. Тем не менее, исследования показали, что разработка, основанная на тестировании, может привести к снижению ошибок на 40-80% в производстве.
Начав использовать TDD, вы можете почувствовать, что работаете медленнее, чем обычно. Так происходит потому что вы будете работать вне «зоны комфорта», и это вполне нормально.
После того, как вы ощутите, что написание тестов стало простой и естественной частью рабочего процесса, что вам больше не нужно думать об использовании TDD при работе над проектом, вы осознаете, что TDD влилось в вашу работу.
Эта методология позволяет добиться создания пригодного для автоматического тестирования приложения и очень хорошего покрытия кода тестами, так как ТЗ переводится на язык автоматических тестов, то есть всё, что программа должна делать, проверяется. Также TDD часто упрощает программную реализацию: исключается избыточность реализации — если компонент проходит тест, то он считается готовым.
Архитектура программных продуктов, разрабатываемых таким образом, обычно лучше (в приложениях, которые пригодны для автоматического тестирования, обычно очень хорошо распределяется ответственность между компонентами, а выполняемые сложные процедуры декомпозированы на множество простых). Стабильность работы приложения, разработанного через тестирование, выше за счёт того, что все основные функциональные возможности программы покрыты тестами и их работоспособность постоянно проверяется. Сопровождаемость проектов, где тестируется всё или практически всё, очень высока — разработчики могут не бояться вносить изменения в код, если что-то пойдёт не так, то об этом сообщат результаты автоматического тестирования.
TDDx2 - type driven development
Type Driven Development сокращенно пишется также, как и разработка через тестирование, поэтому обычно пишут полное название.
При разработке на основе типов ваши типы данных и сигнатуры типов являются спецификацией программы. Типы также служат формой документации, которая гарантированно обновляется.
Типы представляют из себя небольшие контрольные точки, благодаря которым, мы получаем множество мини-тестов по всему нашему приложению. Причем затраты на создание типов минимальны и актуализировать их не требуется, так как они являются частью кодовой базы.
Разработка по типу — это еще один правильный метод построения приложения. Как и в случае разработки на основе тестирования, разработка на основе типов может повысить вашу уверенность в коде и сэкономить ваше время при внесении изменений в большую кодовую базу.
BDD - Behaviour Driven Development
BDD — behaviour-driven development — это разработка, основанная на описании поведения. Определенный человек(или люди) пишет описания вида "я как пользователь хочу когда нажали кнопку пуск тогда показывалось меню как на картинке" (там есть специально выделенные ключевые слова). Программисты давно написали специальные тулы, которые подобные описания переводят в тесты (иногда совсем прозрачно для программиста). А дальше классическая разработка с тестами.
BDD подход совместно с инженерными практиками позволил нам отказаться от legacy-документации, содержащей неактуальную информацию, и получать новую документацию налету, хранить ее вместе с проектом, что приблизило аналитиков и тестировщиков к коду.
BDD — скорее, процесс, целью которого является удешевление реализации новых фич. Еще на старте разработки мы получаем важные артефакты. Например, понятную для поддержки документацию. Эта документация дает возможность всем заинтересованным лицам сформировать свое представление о продукте и сценариях пользовательского поведения, которые должны быть реализованы в ходе итераций разработки. С BDD-подходом мы также снижаем порог входа в проект новых участников.
Но у данного подхода есть и недостатки — это долго и дорого. BDD неудобен хотя бы тем, что требует привлечения специалистов тестирования уже на этапе проработки требований, а это удлиняет цикл разработки.
DDD — Domain Driven Design
Предметно-ориентированное проектирование (реже проблемно-ориентированное, англ. Domain-driven design, DDD) — это набор принципов и схем, направленных на создание оптимальных систем объектов. Процесс разработки сводится к созданию программных абстракций, которые называются моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая связь между реальными условиями области применения продукта и кодом.
Подход DDD особо полезен в ситуациях, когда разработчик не является специалистом в области разрабатываемого продукта. К примеру: программист не может знать все области, в которых требуется создать ПО, но с помощью правильного представления структуры, посредством предметно-ориентированного подхода, может без труда спроектировать приложение, основываясь на ключевых моментах и знаниях рабочей области.
Надеюсь, вам понравилась моя статья. Вы можете присоединиться к моему телеграм каналу о Data Science.
Буду благодарен за любую критику, надеюсь вам понравилась данная статья-шпаргалка-справочник.
Источники
Комментарии (3)
asantat
09.09.2024 22:48Полезно и интересно, но в первоначальном перечне все навалено без знаков разделения, и непонятно, к чему относится та часть перечня, которая идёт после скобок и стоит с большой буквы - к новому элементу, или характеризует предыдущий, зачастую в неправильном падеже. Нужен явный разделитель. И элементы перечня или хотя бы подразделы лучше заанкорить в оглавлении, потому что текст длинный, и навигация скроллингом утомляет.
olgapavlova
09.09.2024 22:48Интересно, каков реальный первоисточник использованных слайдов.
Вот самое раннее, что мне удалось найти: https://habr.com/ru/articles/210288/ — 2014 год, @WarAngel_alk. И там есть ссылка на англоязычный оригинал.
Renius
Почему коментариев нет? Вдруг всё не правильно?