Интерфейсы и когда паттерны вредят

Главный принцип, на котором держатся паттерны, — «программируй на уровне интерфейсов». И почему слепое применение паттернов вредит.

Программируй на уровне интерфейсов, а не реализаций — опирайся в коде на то, что объект умеет (его контракт), а не на то, каким классом он является.

Почему интерфейсы

Когда функция принимает «что угодно, у чего есть метод area()», вы можете подставить любую реализацию, даже ту, которой ещё не существует. Это и даёт гибкость, ради которой нужны паттерны. В Python контракт часто задают через abc.ABC или просто duck typing.

from abc import ABC, abstractmethod


class Notifier(ABC):
    @abstractmethod
    def send(self, msg):
        ...


class EmailNotifier(Notifier):
    def send(self, msg):
        print(f"[email] {msg}")


class SmsNotifier(Notifier):
    def send(self, msg):
        print(f"[sms] {msg}")


# Клиент работает с интерфейсом Notifier, а не с конкретным классом
def alert(notifier: Notifier, msg):
    notifier.send(msg)


for n in (EmailNotifier(), SmsNotifier()):
    alert(n, "Сервер перезагружен")

Вывод:

[email] Сервер перезагружен
[sms] Сервер перезагружен

Функция alert не знает и не хочет знать, как именно отправляется сообщение. Добавится PushNotifier — код alert не изменится. Большинство паттернов — это разные способы аккуратно применить этот принцип.

Обратная сторона: over-engineering

Паттерны — лекарство, а лекарство в неправильной дозе вредит. Самая частая ошибка новичка — впихнуть паттерн туда, где он не нужен, «чтобы было по-взрослому».

Симптом over-engineeringВ чём вред
Фабрика ради одного классаЛишний слой косвенности без выгоды
Интерфейс с единственной реализацией «на будущее»Усложняет навигацию, будущее может не наступить
Strategy там, где хватило бы функцииТри класса вместо двух строк

Сравните: иногда «паттерн» — это просто передать функцию.

# Не нужен класс-стратегия: достаточно передать функцию
def apply_discount(price, rule):
    return rule(price)


print(apply_discount(100, lambda p: p * 0.9))   # 10% скидка
print(apply_discount(100, lambda p: p - 5))      # минус 5

Вывод:

90.0
95

Правило большого пальца

Помните три аббревиатуры: YAGNI (You Aren't Gonna Need It — не делай впрок), KISS (Keep It Simple) и «правило трёх» — обобщай не раньше, чем увидел третий похожий случай. Паттерн вводят, когда боль изменчивости уже реальна, а не воображаема.

Итог

  • Опирайтесь на контракт (интерфейс), а не на конкретный класс.
  • Паттерн без реальной изменчивости — это over-engineering.
  • YAGNI, KISS и «правило трёх» защищают от лишней сложности.
Проверьте себя
1. Что значит «программируй на уровне интерфейсов»?
AВсегда используй графический интерфейс
BОпирайся на контракт объекта, а не на его конкретный класс
CПиши только абстрактные классы
DИзбегай функций
2. Что такое over-engineering в контексте паттернов?
AСлишком быстрый код
BПрименение паттернов там, где нет реальной изменчивости
CОтказ от тестов
DИспользование старых версий языка
3. Что советует «правило трёх»?
AПисать три реализации сразу
BОбобщать не раньше третьего похожего случая
CДелить класс на три части
DИметь три уровня наследования
Поддержать проект