Кратко

Паттерны проектирования

Список паттернов проектирования для ООП собеседований

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

Каталог "Банды четырех" (Gang of Four), написанный в 1994 году для C++ и Smalltalk, определил 23 паттерна, которые стали обязательными для изучения инженерами-программистами. Книга получила легендарный статус, но в реальности большинство из этих паттернов уже не так важны. Современные языки имеют встроенные функции, которые заменили половину из них. Например, итераторы теперь являются примитивами языка, а не паттернами. Переход от объектно-ориентированного программирования с наследованием к композиции и функциональному программированию сделал другие паттерны устаревшими. Так что на собеседованиях вас спросят максимум о пяти паттернах, а не о двадцати трех.

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

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

Отношение к паттернам проектирования различается в зависимости от того, где вы проходите собеседование. В США на большинстве ООП собеседований не проверяют, знаете ли вы названия паттернов - вас оценивают по качеству вашего дизайна. В России интервьюеры с большей вероятностью будут спрашивать о паттернах напрямую. Хотя мы считаем правильным первый подход, мы хотим, чтобы вы были готовы к любой ситуации, с которой вы можете столкнуться на собеседовании.

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

Порождающие паттерны

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

Фабричный метод (Factory Method)

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

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

Паттерн фабрика регулярно появляется на собеседованиях, обычно когда в требованиях говорится "поддержать разные типы уведомлений" или "обрабатывать несколько способов оплаты". Вместо того чтобы писать новые new EmailNotification() по всему вашему коду, вы вызываете notificationFactory.create(type). Теперь, когда вы добавляете SMS-уведомления, вы просто обновляете фабрику. Остальная часть вашего кода при этом не меняется.

PythonFactory Method
from abc import ABC, abstractmethod

class Notification(ABC):
    @abstractmethod
    def send(self, message: str) -> None:
        pass

class EmailNotification(Notification):
    def send(self, message: str) -> None:
        # Email sending logic
        pass

class SMSNotification(Notification):
    def send(self, message: str) -> None:
        # SMS sending logic
        pass

class NotificationFactory:
    @staticmethod
    def create(notification_type: str) -> Notification:
        if notification_type == "email":
            return EmailNotification()
        elif notification_type == "sms":
            return SMSNotification()
        raise ValueError("Unknown type")

# Usage
notif = NotificationFactory.create("email")
notif.send("Hello")

Технически это называется Simple Factory, а не паттерн Factory Method от "Банды четырех". Версия GoF использует абстрактные классы фабрик с подклассами, которые переопределяют фабричный метод. Это сложнее и редко встречается в реальном коде или на собеседованиях. То, что мы показываем здесь - это то, что люди на самом деле создают и чего ожидают интервьюеры, когда говорят "используйте фабрику".

Строитель (Builder)

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

Это встречается при проектировании HTTP-запросов, запросов к базе данных или объектов конфигурации. Вместо использования конструктора с десятью параметрами, половина из которых равна null, вы собираете объект постепенно.

PythonBuilder
from typing import Optional

# NOTE: Builder не очень распространен в Python. Python имеет лучшие альтернативы, такие как
# dataclasses с default values, keyword arguments, или simple dictionaries.
# Этот паттерн добавляет лишнюю сложность для большинства Python-сценариев.

class HttpRequest:
    def __init__(self):
        self.url: Optional[str] = None
        self.method: Optional[str] = None
        self.headers: dict[str, str] = {}
        self.body: Optional[str] = None

    class Builder:
        def __init__(self):
            self._request = HttpRequest()

        def url(self, url: str) -> 'HttpRequest.Builder':
            self._request.url = url
            return self

        def method(self, method: str) -> 'HttpRequest.Builder':
            self._request.method = method
            return self

        def header(self, key: str, value: str) -> 'HttpRequest.Builder':
            self._request.headers[key] = value
            return self

        def body(self, body: str) -> 'HttpRequest.Builder':
            self._request.body = body
            return self

        def build(self) -> 'HttpRequest':
            # Validate required fields
            if self._request.url is None:
                raise ValueError("URL is required")
            return self._request

# Usage
request = (HttpRequest.Builder()
    .url("https://api.example.com")
    .method("POST")
    .header("Content-Type", "application/json")
    .body('{"key": "value"}')
    .build())

Builder делает конструирование читаемым и изящно обрабатывает необязательные поля. Чаще всего он встречается на ООП собеседованиях, когда вы проектируете API-клиенты или сложные конфигурации, но редко используется в других контекстах.

Если интервьюер не описал сложный объект с множеством необязательных деталей, паттерн Builder, вероятно, не нужен. Большинство задач на собеседованиях включают простые доменные объекты с двумя-четырьмя обязательными полями, для которых отлично работает обычный конструктор.

Одиночка (Singleton)

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

В большинстве случаев Singleton на самом деле не нужен. Вы можете просто передавать общие объекты через конструкторы. Это понятнее и легче тестировать. Singleton скрывает зависимости и усложняет тестирование.

PythonSingleton
# NOTE: Singleton не является идиоматичным для Python. В Python модули
# сами по себе являются singleton-объектами, потому что импортируются только один раз.
# Для общих ресурсов лучше создать экземпляр на уровне модуля, а не использовать
# этот паттерн.

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def query(self, sql: str) -> None:
        # Database operations
        pass


# Usage
db = DatabaseConnection()
db.query("SELECT * FROM users")

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

Структурные паттерны

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

Декоратор (Decorator)

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

Декоратор - это мощный инструмент, но он встречается реже, чем Strategy или Observer. Он может понадобиться, когда в требованиях говорится о таких вещах, как "добавить логирование к определенным операциям" или "зашифровать определенные сообщения". Вместо того чтобы создавать подклассы для каждой комбинации (например, LoggedEmailNotification, EncryptedEmailNotification, LoggedEncryptedEmailNotification), вы оборачиваете базовый объект декораторами. Если вы видите такие слова, как "необязательные функции", "наслаивание поведения" или "комбинирование нескольких расширений", подумайте о Decorator.

PythonDecorator
from abc import ABC, abstractmethod

# NOTE: Это паттерн проектирования Decorator, который отличается от синтаксиса
# @decorator в Python. Название может сбивать с толку, поскольку декораторы Python (с @)
# являются особенностью языка для модификации функций/классов, тогда как это -
# паттерн композиции объектов для добавления поведения во время выполнения.

class DataSource(ABC):
    @abstractmethod
    def write_data(self, data: str) -> None:
        pass

    @abstractmethod
    def read_data(self) -> str:
        pass

class FileDataSource(DataSource):
    def __init__(self, filename: str):
        self.filename = filename

    def write_data(self, data: str) -> None:
        # Write to file
        pass

    def read_data(self) -> str:
        # Read from file
        return "data from file"

class EncryptionDecorator(DataSource):
    def __init__(self, source: DataSource):
        self._wrapped = source

    def write_data(self, data: str) -> None:
        encrypted = self._encrypt(data)
        self._wrapped.write_data(encrypted)  # Delegate to wrapped object

    def read_data(self) -> str:
        data = self._wrapped.read_data()
        return self._decrypt(data)

    def _encrypt(self, data: str) -> str:
        return f"encrypted:{data}"

    def _decrypt(self, data: str) -> str:
        return data.replace("encrypted:", "")

class CompressionDecorator(DataSource):
    def __init__(self, source: DataSource):
        self._wrapped = source

    def write_data(self, data: str) -> None:
        compressed = self._compress(data)
        self._wrapped.write_data(compressed)  # Delegate to wrapped object

    def read_data(self) -> str:
        data = self._wrapped.read_data()
        return self._decompress(data)

    def _compress(self, data: str) -> str:
        return f"compressed:{data}"

    def _decompress(self, data: str) -> str:
        return data.replace("compressed:", "")

# Usage
source = FileDataSource("data.txt")
source = EncryptionDecorator(source)
source = CompressionDecorator(source)
source.write_data("sensitive info")
# Data gets compressed, then encrypted, then written to file

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

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

Фасад (Facade)

Фасад - это просто класс-координатор, который скрывает сложность. Скорее всего, вы уже строите фасады на каждом ООП интервью, даже если не называете их так. Вы определили класс класс Game в крестиках-ноликах? Это фасад. Любой оркестратор, который координирует несколько компонентов за чистым интерфейсом? Тоже фасад.

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

PythonFacade
from enum import Enum

class GameState(Enum):
    IN_PROGRESS = 1
    WON = 2
    DRAW = 3

class Board:
    def place_mark(self, row: int, col: int, mark: str) -> bool:
        # Place mark logic
        return True

    def check_win(self, row: int, col: int) -> bool:
        # Check win logic
        return False

    def is_full(self) -> bool:
        # Check if board is full
        return False

class Player:
    def __init__(self, mark: str):
        self.mark = mark

    def get_mark(self) -> str:
        return self.mark

class Game:
    def __init__(self):
        self.board = Board()
        self.player_x = Player("X")
        self.player_o = Player("O")
        self.current_player = self.player_x
        self.state = GameState.IN_PROGRESS

    def make_move(self, row: int, col: int) -> bool:
        # Coordinates board, player, and state logic
        # Caller doesn't need to understand internal details
        if self.state != GameState.IN_PROGRESS:
            return False
        if not self.board.place_mark(row, col, self.current_player.get_mark()):
            return False

        if self.board.check_win(row, col):
            self.state = GameState.WON
        elif self.board.is_full():
            self.state = GameState.DRAW
        else:
            self.current_player = (
                self.player_o if self.current_player == self.player_x
                else self.player_x
            )
        return True

# Usage - simple interface hides all the coordination
game = Game()
game.make_move(0, 0)
game.make_move(1, 1)

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

Поведенческие паттерны

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

Стратегия (Strategy)

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

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

Интервьюеры любят паттерн Стратегия. Это самый распространенный паттерн на ООП интервью, потому что он напрямую проверяет, понимаете ли вы полиморфизм и композицию вместо наследования. Когда вы видите набор if/else или switch по типу, перед вами почти готовый паттерн Стратегия. Если вы выучите только один паттерн из этой статьи, пусть это будет он. Вы уже видели это в статье о концепциях ООП в примере с транспортными средствами на парковке.

PythonStrategy
from abc import ABC, abstractmethod


class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> bool:
        pass


class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def pay(self, amount: float) -> bool:
        # Credit card processing logic
        print(f"Paid {amount} with credit card")
        return True


class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def pay(self, amount: float) -> bool:
        # PayPal processing logic
        print(f"Paid {amount} with PayPal")
        return True


class ShoppingCart:
    def __init__(self):
        self.payment_strategy = None

    def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
        self.payment_strategy = strategy

    def checkout(self, amount: float) -> None:
        self.payment_strategy.pay(amount)


# Usage
cart = ShoppingCart()
cart.set_payment_strategy(CreditCardPayment("1234-5678"))
cart.checkout(100.00)
cart.set_payment_strategy(PayPalPayment("user@example.com"))
cart.checkout(50.00)

Вместо логики checkout, полной условий if (paymentType == "credit"), каждый способ оплаты обрабатывает себя сам. Стратегия меняет поведение во время выполнения посредством композиции. Корзина содержит ссылку на стратегию и делегирует ей задачи. Фабрика решает, какой тип создать. Стратегия решает, какое поведение использовать после того, как объект уже создан.

Наблюдатель (Observer)

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

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

PythonObserver
from abc import ABC, abstractmethod

class Observer(ABC):
    @abstractmethod
    def update(self, symbol: str, price: float) -> None:
        pass

class Subject(ABC):
    @abstractmethod
    def attach(self, observer: Observer) -> None:
        pass

    @abstractmethod
    def detach(self, observer: Observer) -> None:
        pass

    @abstractmethod
    def notify_observers(self) -> None:
        pass

class Stock(Subject):
    def __init__(self, symbol: str):
        self._observers: list[Observer] = []
        self.symbol = symbol
        self.price = 0.0

    def attach(self, observer: Observer) -> None:
        self._observers.append(observer)

    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)

    def set_price(self, price: float) -> None:
        self.price = price
        self.notify_observers()  # Price changed, tell everyone

    def notify_observers(self) -> None:
        for observer in self._observers:
            observer.update(self.symbol, self.price)

class PriceDisplay(Observer):
    def update(self, symbol: str, price: float) -> None:
        print(f"Display updated: {symbol} = ${price}")

class PriceAlert(Observer):
    def __init__(self, threshold: float):
        self.threshold = threshold

    def update(self, symbol: str, price: float) -> None:
        if price > self.threshold:
            print(f"Alert! {symbol} exceeded ${self.threshold}")

# Usage
stock = Stock("AAPL")

display = PriceDisplay()
alert = PriceAlert(150.00)

stock.attach(display)
stock.attach(alert)

stock.set_price(145.00)  # Both observers get notified
stock.set_price(155.00)  # Both observers get notified

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

Конечный автомат (State Machine)

Конечный автомат корректно обрабатывает переходы между состояниями. Используйте его, когда поведение объекта меняется в зависимости от внутреннего состояния и правила переходов становятся сложными. В некоторых источниках это называется паттерном "Состояние" (State), но конечный автомат (State Machine) - это более распространенный термин.

Конечный автомат встречается реже, чем Стратегия или Наблюдатель, но когда он вам нужен, он обычно является центральным элементом всего вашего дизайна. Если в вашем решении есть конечный автомат, собеседование, скорее всего, будет построено вокруг него. Это самая важная часть, которую нужно проговорить. Этот паттерн встречается на ООП интервью при проектировании таких вещей, как торговые автоматы, системы документооборота или состояния игры. Если слово "состояние" несколько раз появляется в требованиях, скорее всего, вам нужен Конечный автомат. Вместо разбросанных по коду условий, проверяющих текущее состояние, вы инкапсулируете поведение каждого состояния в отдельном классе.

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

PythonState Machine
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from __future__ import annotations

class VendingMachineState(ABC):
    @abstractmethod
    def insert_coin(self, machine: 'VendingMachine') -> None:
        pass

    @abstractmethod
    def select_product(self, machine: 'VendingMachine') -> None:
        pass

    @abstractmethod
    def dispense(self, machine: 'VendingMachine') -> None:
        pass

class NoCoinState(VendingMachineState):
    def insert_coin(self, machine: 'VendingMachine') -> None:
        print("Coin inserted")
        machine.set_state(HasCoinState())

    def select_product(self, machine: 'VendingMachine') -> None:
        print("Insert coin first")

    def dispense(self, machine: 'VendingMachine') -> None:
        print("Insert coin first")

class HasCoinState(VendingMachineState):
    def insert_coin(self, machine: 'VendingMachine') -> None:
        print("Coin already inserted")

    def select_product(self, machine: 'VendingMachine') -> None:
        print("Product selected")
        machine.set_state(DispenseState())

    def dispense(self, machine: 'VendingMachine') -> None:
        print("Select product first")

class DispenseState(VendingMachineState):
    def insert_coin(self, machine: 'VendingMachine') -> None:
        print("Please wait, dispensing")

    def select_product(self, machine: 'VendingMachine') -> None:
        print("Please wait, dispensing")

    def dispense(self, machine: 'VendingMachine') -> None:
        print("Dispensing product")
        machine.set_state(NoCoinState())

class VendingMachine:
    def __init__(self):
        self._current_state: VendingMachineState = NoCoinState()

    def insert_coin(self) -> None:
        self._current_state.insert_coin(self)

    def select_product(self) -> None:
        self._current_state.select_product(self)

    def dispense(self) -> None:
        self._current_state.dispense(self)

    def set_state(self, state: VendingMachineState) -> None:
        self._current_state = state

# Usage
machine = VendingMachine()

machine.select_product()  # "Insert coin first"
machine.insert_coin()     # "Coin inserted"
machine.select_product()  # "Product selected"
machine.dispense()        # "Dispensing product"

Каждое состояние знает, какое состояние идет следующим и какие действия являются допустимыми. Никаких огромных switch, которые проверяют текущее состояние в каждом методе.

Заключение

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

Вот краткая шпаргалка по паттернам, сгруппированная по категориям:

Порождающие паттерны

  • Factory -> используйте, когда вызывающему коду не должно быть важно, какой конкретный класс создается.
  • Builder -> используйте, когда у объекта есть много необязательных полей или запутанные детали конструирования.
  • Singleton -> используйте, когда вам нужен ровно один глобальный экземпляр (что редко встречается на собеседованиях).

Структурные паттерны

  • Decorator -> используйте, когда нужно наслаивать необязательное поведение во время выполнения без взрывного роста числа подклассов.
  • Facade -> используйте, когда хотите скрыть внутреннюю сложность за простой точкой входа.

Поведенческие паттерны

  • Strategy -> используйте, когда заменяете if/else различными вариантами поведения.
  • Observer -> используйте, когда несколько компонентов должны реагировать на одно событие.
  • State Machine -> используйте, когда поведение объекта зависит от его текущего состояния, а переходы становятся запутанными.
Войдите чтобы отмечать прогресс