Рефакторинг к паттернам и оверинжиниринг

Паттерн — это не цель, а лекарство. А лекарство без болезни — это яд.

Рефакторинг к паттернам — введение паттерна как ответ на конкретную, уже проявившуюся боль в коде, а не «потому что так положено».

Изучив каталог паттернов, легко впасть в искушение применять их везде — это известный синдром «новенького с молотком, которому всё вокруг кажется гвоздём». Но паттерн — это компромисс: он добавляет гибкость в одной оси ценой лишних классов, косвенности и порога входа для того, кто будет читать код после вас. Хороший инженер вводит паттерн тогда, когда выигрыш от гибкости перевешивает цену сложности, — и ни мгновением раньше. Этот урок собирает воедино всё, о чём шла речь в разделе: SOLID подсказывает какой паттерн, а DRY/KISS/YAGNI — стоит ли вообще.

Зачем это на практике

Две крайности одинаково вредны, но проявляются по-разному. Недодизайн — это спагетти и копипаста, код, который страшно трогать, потому что любое изменение ветвится по всему файлу. Его видно сразу: он болит каждый день. Передизайн (оверинжиниринг) коварнее — это лес абстракций вокруг тривиальной задачи, где, чтобы понять «как добавить кнопку», надо пройти через пять интерфейсов, фабрику и конфиг. Он не болит в момент написания (автору даже приятно — «как солидно»), зато бьёт по каждому, кто придёт следом. Цель урока — научиться двигаться к паттерну от боли, а не от каталога, и так же спокойно откатывать лишнюю абстракцию назад.

Путь к паттерну: от боли к решению

Возьмём типичный сценарий. Сначала есть скромная функция расчёта цены доставки:

def shipping(method, weight):
    if method == "standard":
        return weight * 5
    elif method == "express":
        return weight * 5 + 100
    return 0

print(shipping("express", 2))

Вывод:

110

Пока вариантов два, это нормальный код, и навешивать сюда паттерн — оверинжиниринг. Но проходит время: добавляются «ночная», «международная», «по подписке», у каждой свои правила и параметры, функция распухает до сотни строк elif, её страшно менять. Вот теперь появилась реальная боль — нарушение OCP, и она диктует лекарство: Strategy. Выносим каждый способ в свой класс с общим интерфейсом.

from abc import ABC, abstractmethod

class Shipping(ABC):
    @abstractmethod
    def cost(self, weight): ...

class Standard(Shipping):
    def cost(self, weight):
        return weight * 5

class Express(Shipping):
    def cost(self, weight):
        return weight * 5 + 100

def checkout(strategy: Shipping, weight):
    return strategy.cost(weight)

print(checkout(Express(), 2))

Вывод:

110

Обратите внимание: поведение не изменилось (тот же результат 110), изменилась только структура. Это и есть рефакторинг. Теперь новый способ доставки — новый класс, существующий код не трогаем. Паттерн заработал, потому что его позвала боль, а не мода.

Где паттерн оправдан, а где лишний

Паттерн оправдан, когда…Паттерн лишний, когда…
точек изменения реально несколько и они растутвариант один и второго не предвидится
ветвление по типу расползлось по кодудве ветки if, которые понятны с первого взгляда
нужна замена реализации в рантайме или на тестахреализация всегда одна
гибкость снизит будущую стоимость измененийгибкость нужна «на всякий случай» (YAGNI)

Признаки оверинжиниринга

  • Интерфейс с единственной реализацией, которую никто не собирается заменять, — абстракция без причины.
  • Фабрика, создающая один тип объекта. Если выбора нет, фабрика — это просто длинный способ написать конструктор.
  • Слои-проксёры, которые ничего не делают, кроме как передают вызов дальше.
  • «Конфигурируемость» того, что никогда не конфигурируют — флаги и стратегии под один-единственный режим.

Хорошая проверка: спросите «какую конкретную боль снимает эта абстракция сегодня?». Если ответ начинается с «а вдруг в будущем…», это сигнал YAGNI.

Как это работает под капотом

За выбором «вводить паттерн или нет» стоит экономика изменений. У любой абстракции есть постоянная цена — её надо понять при чтении (когнитивная нагрузка) и сопровождать. И есть выгода — она снижает стоимость определённого типа будущих правок. Пока изменчивости нет, вы платите цену, не получая выгоды, — это чистый убыток. Когда изменчивость доказана (вы уже третий раз правите один и тот же switch), выгода превышает цену. Поэтому правильный момент для паттерна — не в начале «на чистом листе», а в точке, где боль уже измерима. Рефакторинг к паттернам всегда идёт под защитой тестов: они фиксируют, что внешнее поведение не поменялось, пока вы перекраиваете внутренности.

Частые ошибки

  • «Резюме-ориентированное программирование». Внедрять паттерн, чтобы показать, что вы его знаете. Код существует для задачи, а не для демонстрации эрудиции.
  • Паттерн вместо понимания проблемы. Накрыть паттерном плохо понятую задачу — значит закрепить непонимание в архитектуре.
  • Архитектура «на вырост» с первого дня. Слои, шины, плагины под проект из трёх экранов — классический передизайн, нарушающий KISS и YAGNI.
  • Рефакторинг без тестов и мелких шагов. Большая перестройка вслепую почти гарантированно ломает поведение.
  • Боязнь упрощать. Оверинжиниринг лечится в обе стороны: лишний паттерн можно (и нужно) убрать, схлопнув абстракцию обратно в простую функцию.

Итоги

  • Паттерн — лекарство от конкретной боли (обычно нарушения OCP/SRP), а не самоцель.
  • Двигайся к паттерну «от боли»: сначала простой код, паттерн — когда изменчивость доказана.
  • Рефакторинг сохраняет поведение и меняет структуру; делай его маленькими шагами под тестами.
  • Оверинжиниринг — интерфейсы с одной реализацией, фабрики одного типа, гибкость «на всякий случай».
  • Проверочный вопрос: какую боль снимает абстракция сегодня? «А вдруг в будущем» — это YAGNI.
  • Упрощать так же ценно, как и обобщать: лишний паттерн не грех убрать.
Проверьте себя
1. Когда стоит рефакторить простую цепочку из двух if к паттерну Strategy?
AСразу — паттерны всегда лучше, чем if
BКогда вариантов реально становится много и ветвление по типу начинает расползаться и мешать
CНикогда — Strategy запрещён в продакшене
DКак только в коде появляется хотя бы один if
2. Что из перечисленного — типичный признак оверинжиниринга?
AФункция с понятным именем и одной задачей
BИнтерфейс с единственной реализацией, которую никто не планирует заменять
CИспользование именованной константы вместо магического числа
DПокрытие кода тестами перед рефакторингом
3. Что обязательно сохраняет рефакторинг (в том числе рефакторинг к паттерну)?
AКоличество строк кода
BИмена всех классов и методов
CВнешнее наблюдаемое поведение программы
DПроизводительность до миллисекунды