Практический ООП в Python: рефакторинг легаси-кода

Практико-ориентированный курс по применению объектно-ориентированного программирования для рефакторинга и улучшения поддерживаемости Python-кода. Фокус на конкретных примерах из Django, FastAPI и Flask, разбор паттернов и антипаттернов для декомпозиции сложных модулей и повышения читаемости кодовой базы.

1. Принципы SOLID на примерах рефакторинга реального кода

Принципы SOLID на примерах рефакторинга реального кода

Почему одни модули в легаси-проекте переписываются за час, а на другой уходит неделя? Ответ почти всегда один: нарушены принципы SOLID — пять базовых принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином. Они не догма, а практический инструмент: если код им следует, рефакторинг превращается в локальную операцию. Если нет — любое изменение тянет за собой цепочку зависимостей.

Разберём каждый принцип на реальном коде из Django-проекта, который обрабатывает заказы в интернет-магазине.

Single Responsibility Principle: один класс — одна причина для изменения

Вот типичный «богоподобный» класс из легаси-кода:

Класс OrderProcessor меняется по четырём причинам: бизнес-логика скидок, формат email-сообщений, структура БД и формат PDF. Одно изменение в шаблоне письма ломает расчёт скидок. Решение — декомпозиция по ответственности:

Теперь изменение шаблона письма затрагивает только EmailNotifier. Каждый класс имеет одну причину для изменения — и это именно SRP.

Open/Closed Principle: открыты для расширения, закрыты для изменения

Допустим, в проекте есть функция расчёта доставки:

Каждый новый способ доставки требует модификации существующего кода. Нарушение OCP — принципа открытости/закрытости. Решение через полиморфизм:

Новый способ доставки — добавляем класс и запись в словарь. Существующий код не трогаем.

Liskov Substitution Principle: подклассы должны быть взаимозаменяемы

В Django-проекте часто встречается такой код:

Клиентский код, ожидающий JSON от любого экспортера, сломается при подстановке CsvExporter. Нарушение LSP — принципа подстановки Лисков. Контракт базового класса гласит: «возвращает строку с данными в формате JSON». Подкласс обязан соблюдать этот контракт.

Правильный подход — убрать жёсткий контракт формата из базового класса:

Теперь контракт базового класса — «возвращает строку», и оба подкласса его соблюдают. Клиентский код, работающий с BaseExporter, корректно обрабатывает любой формат.

Interface Segregation Principle: не заставляй клиента зависеть от того, что он не использует

Распространённая ошибка — один «толстый» интерфейс для всех:

Класс OneTimePaymentProcessor зависит от абстракции подписок, хотя никогда их не использует. Нарушение ISP — принципа разделения интерфейсов. Решение — разбить интерфейс на узкие:

Каждый класс реализует только те интерфейсы, которые ему действительно нужны.

Dependency Inversion Principle: зависеть от абстракций, а не от деталей

Финальный принцип — DIP. Вот типичный анти-паттерн в Django-вьюхах:

Вьюха жёстко привязана к SMTP и Stripe. Замена платёжного шлюза или способа отправки писем — переписывание вьюхи. Решение — инверсия зависимостей:

Теперь вьюха работает с абстракциями. Замена Stripe на ЮKassa — пишем новый класс YooKassaGateway(PaymentGateway) и подставляем его. Вьюха не меняется.

Как SOLID работает вместе

Эти пять принципов не изолированы — они усиливают друг друга. SRP даёт маленькие классы с чёткой зоной ответственности. OCP позволяет расширять поведение без правки существующего кода. LSP гарантирует, что полиморфизм работает predictably. ISP не даёт интерфейсам разрастаться. DIP обеспечивает слабую связность между модулями.

В легаси-коде SOLID нарушения почти всегда идут парами: богоподобный класс (нарушение SRP) содержит цепочку if/elif (нарушение OCP) и зависит от конкретных библиотек (нарушение DIP). Рефакторинг начинается с SRP — разбиваем класс на ответственности, затем применяем OCP через стратегии, затем выстраиваем зависимости через DIP. Этот порядок работает в 90% случаев.

2. Выявление и исправление распространённых антипаттернов в Python

Выявление и исправление распространённых антипаттернов в Python

Когда вы открываете легаси-модуль на 800 строк и чувствуете физическое отвращение — это не субъективное впечатление. Ваш мозг распознаёт антипаттерны — устойчивые структуры кода, которые выглядят как решение, но на деле создают больше проблем, чем решают. В Python-проектах на Django и FastAPI есть набор антипаттернов, которые встречаются с пугающей регулярностью. Разберём самые разрушительные и способы их исправления.

God Object: класс, который знает и делает всё

Вот реальный фрагмент из Django-проекта управления складом:

Класс WarehouseManager — это God Object: он управляет инвентарём, заказами, поставщиками, уведомлениями и отчётностью. Любое изменение в логике уведомлений рискует сломать обработку заказов.

Исправление — выделяем зоны ответственности:

Каждый класс делает одно дело. Меняем уведомления — трогаем только notifier.

Spaghetti Code: логика, размазанная по методам без структуры

В FastAPI-эндпоинтах часто встречается такая картина:

Вся бизнес-логика свалена в один обработчик. Невозможно протестировать расчёт скидок отдельно от сохранения в БД. Решение — выделение слоёв:

Эндпоинт стал тонким оркестратором. Бизнес-логика живёт в отдельных классах и тестируется независимо.

Primitive Obsession: примитивы вместо объектов

Этот антипаттерн особенно коварен, потому что выглядит безобидно:

Строковые примитивы email, role разносят валидацию по всему проекту. Забыли проверить в одном месте — баг. Решение — Value Objects:

Теперь невозможно создать пользователя с невалидным email — объект Email не сконструируется. Валидация написана один раз, работает везде.

Copy-Paste Programming: дублирование логики

Один из самых разрушительных антипаттернов. Вот реальный пример из Django-проекта с тремя похожими вьюхами:

Два метода делают одно и то же — отличаются только источник данных и колонки. При изменении формата CSV придётся править оба. Решение — выделение общей логики:

Дублирование устранено. Изменение формата CSV — правим один класс.

Magic Numbers и неявные зависимости

Последний антипаттерн, который отравляет поддерживаемость:

Что означают 5000, 0.15, 0.10? Через полгода ни один разработчик не ответит. Решение — именованные константы и конфигурация:

Теперь пороги и ставки читаемы, легко настраиваемы и вынесены в конфигурацию.

Стратегия работы с антипаттернами

Антипаттерны редко встречаются по одному. В реальном легаси-коде God Object содержит Spaghetti Code с Primitive Obsession и копипастой. Порядок рефакторинга:

  • Находим дублирование — оно указывает на скрытую абстракцию
  • Выделяем Value Objects для примитивов, которые несут бизнес-смысл
  • Разбиваем God Object по зонам ответственности
  • Структурируем Spaghetti Code через выделение слоёв
  • Заменяем магические числа на именованные константы
  • Каждый следующий шаг проще предыдущего, потому что предыдущий уже навёл порядок в структуре.

    3. Применение паттернов проектирования для упрощения сложной логики

    Применение паттернов проектирования для упрощения сложной логики

    Когда легаси-код превращается в нагромождение условий, циклов и вложенных вызовов, первая мысль — «нужно переписать с нуля». Но это почти всегда ошибка. Чаще всего сложность возникает не из-за отсутствия кода, а из-за отсутствия структуры. Паттерны проектирования — это готовые структурные решения, которые превращают хаос в предсказуемую архитектуру. Разберём пять паттернов, которые дают наибольший эффект при рефакторинге Python-проектов.

    Strategy: замена цепочек if/elif на подключаемое поведение

    Вот типичный код из Flask-приложения для обработки уведомлений:

    Каждый новый канал уведомлений — модификация существующего кода. Нарушение OCP, о котором мы говорили в статье о SOLID. Паттерн Strategy инкапсулирует каждое поведение в отдельный класс:

    Новый канал — новый класс и одна строка регистрации. Существующий код не трогаем.

    Chain of Responsibility: обработка запроса через цепочку обработчиков

    В Django-проекте часто встречается валидация заказа, размазанная по одному методу:

    Паттерн Chain of Responsibility превращает этот монолит в цепочку независимых обработчиков:

    Каждый обработчик знает только о своей проверке и о следующем звене. Добавление новой валидации — новый класс и вставка в цепочку.

    Observer: реакция на события без жёстких связей

    В легаси-коде обработка событий часто выглядит так:

    Паттерн Observer разрывает эту жёсткую связь:

    Новое действие при завершении заказа — ещё одна подписка. Функция complete_order не меняется.

    Factory Method: создание объектов без привязки к конкретным классам

    В FastAPI-проекте с несколькими платёжными системами:

    Паттерн Factory Method централизует создание объектов:

    Новый платёжный провайдер — регистрируем класс. Функция process_payment не знает, какой именно класс создаётся.

    Decorator: расширение поведения без наследования

    Частая задача в Django — добавить кэширование, логирование или повторные попытки к существующему сервису. Легаси-подход — модификация метода:

    Бизнес-логика смешана с инфраструктурой. Паттерн Decorator оборачивает объект, добавляя поведение прозрачно:

    Нужно логирование без кэша — убираем один слой. Нужна retry-логика — добавляем ещё один декоратор. Бизнес-логика ProductService остаётся нетронутой.

    Когда какой паттерн применять

    | Ситуация | Паттерн | Признак в коде | |----------|---------|----------------| | Цепочка if/elif выбирает поведение | Strategy | Условия выбирают алгоритм | | Последовательная обработка с возможностью пропуска | Chain of Responsibility | Несколько независимых проверок | | Действия при событии разбросаны по коду | Observer | Один вызов тянет за собой много побочных эффектов | | Создание объектов зависит от условий | Factory Method | if/elif для выбора класса | | Нужно добавить поведение без правки класса | Decorator | Логирование, кэширование, retry |

    Паттерны — не самоцель. Если цепочка if/elif содержит три ветки и не будет расти — оставьте как есть. Паттерн оправдан, когда сложность растёт и текущая структура начинает мешать. В легаси-коде это почти всегда так.

    4. Инкапсуляция и эффективное управление состоянием объектов

    Инкапсуляция и эффективное управление состоянием объектов

    Баг, который невозможно воспроизвести. Данные, которые «сами по себе» меняются между вызовами. Тесты, которые проходят по отдельности, но падают в suite. Все эти проблемы имеют одну общую причину: неконтролируемое изменение состояния объектов. Инкапсуляция — это не просто приватные атрибуты с подчёркиванием. Это система правил, определяющих, кто и когда может менять внутреннее состояние объекта.

    Проблема: распределённая мутация состояния

    Вот реальный фрагмент из Django-проекта — сервис обработки корзины:

    Объект Cart — это просто контейнер данных. Его инварианты (связь между items, total и discount) нарушаются при каждом внешнем вмешательстве. Состояние распределено по трём атрибутам, которые зависят друг от друга, но обновляются независимо.

    Инкапсуляция через контролируемые точки доступа

    Первый шаг — убрать прямой доступ к изменяемым атрибутам и предоставить методы, которые поддерживают инварианты:

    Ключевые моменты:

  • _items и _discount_rate — приватные по соглашению. Изменение только через add_item и remove_item
  • totalвычисляемое свойство, а не хранимое значение. Невозможно рассинхронизировать
  • items возвращает копию списка — внешний код не может мутировать внутреннее состояние
  • Property vs прямой доступ: когда что использовать

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

    Property оправдан, когда:

  • Нужна валидация при установке значения
  • Значение вычисляется из других атрибутов
  • Установка значения имеет побочные эффекты (логирование, уведомления)
  • Property не нужен, если атрибут — простое хранилище без инвариантов:

    Управление жизненным циклом состояния

    В легаси-коде часто встречается объект, который проходит через состояния, но эти состояния неявны:

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

    Решение — явное описание состояний и переходов:

    Теперь переход состояний — один метод transition_to. Добавление нового статуса — расширяем словарь TRANSITIONS. Невалидный переход выбрасывает исключение с понятным сообщением.

    Иммутабельные объекты как способ избежать мутаций

    Иногда лучший способ контролировать состояние — запретить его изменение. Python поддерживает это через frozen dataclass:

    Объект Money неизменяем. Операции add и multiply возвращают новый объект. Это исключает целый класс багов, связанных с непреднамеренной мутацией:

    Иммутабельные объекты особенно полезны для Value Objects — объектов, идентифицируемых по значению, а не по идентификатору: деньги, координаты, диапазоны дат, email-адреса.

    Капсуляция коллекций: защита от внешней мутации

    Одна из самых частых ловушек в Python — возврат внутренней коллекции по ссылке:

    Три способа защиты:

    Третий вариант — самый надёжный, потому что он не просто защищает от мутации, а инкапсулирует бизнес-правила добавления.

    Инкапсуляция в контексте Django-моделей

    Django ORM поощряет прямой доступ к атрибутам модели. Это удобно, но создаёт проблемы, когда бизнес-логика размазана по вьюхам и сериализаторам:

    Решение — толстые модели с инкапсулированной логикой:

    Вся логика перехода состояний — в модели. Вьюха вызывает один метод. Тестируем mark_as_paid отдельно от HTTP-слоя.

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

    5. Рефакторинг наследования: переход к композиции и гибким связям

    Рефакторинг наследования: переход к композиции и гибким связям

    Наследование — самый переоценённый инструмент в арсенале ООП-разработчика. В легаси-коде оно используется как универсальный способ «повторно использовать код», но почти всегда приводит к хрупким иерархиям, где изменение базового класса ломает половину проекта. Если вы работаете с Django, где модели часто наследуются друг от друга, вы наверняка сталкивались с этим. Разберём, почему наследование становится проблемой, и как композиция даёт более гибкое решение.

    Проблема: иерархия наследования, которая не масштабируется

    Вот реальная структура из Django-проекта электронной коммерции:

    Выглядит логично. Но бизнес требует: «Нужен цифровой товар с подпиской». Что делать? Наследовать от DigitalProduct? От SubscriptionProduct? Создать DigitalSubscriptionProduct? Каждый новый вариант — новый класс в иерархии. Через полгода иерархия превращается в лес подклассов, где половина дублирует логику.

    Это классическая ловушка наследования: оно моделирует «является»-связи, но реальные домены редко укладываются в дерево.

    Шаг первый: выделяем поведение в отдельные компоненты

    Вместо наследования разложим поведение на независимые компоненты:

    Каждый компонент — самодостаточный объект с одной ответственностью. Они не знают друг о друге и не зависят от общей иерархии.

    Шаг второй: собираем продукт через композицию

    Теперь Product — это контейнер, который содержит компоненты, а не наследует поведение:

    Физический товар с доставкой:

    Цифровой товар с подпиской — без нового класса:

    Любая комбинация поведений — без новых классов и без взрывного роста иерархии.

    Mixin: наследование, которое не создаёт иерархию

    Иногда наследование всё-таки полезно — но не для построения иерархий, а для примешивания поведения. В Python для этого используются mixin-классы:

    Ключевое отличие от обычного наследования: mixin не является полноценным предком. Он не определяет самостоятельную сущность, а лишь добавляет набор полей и методов. Класс Order не «является» TimestampMixin — он просто получает его поведение.

    Правила хорошего mixin:

  • Нет собственного состояния кроме добавляемых полей
  • Не переопределяет методы других mixin
  • Может использоваться с любым классом-наследником models.Model
  • Называется с суффиксом Mixin
  • Замена глубокой иерархии на плоскую композицию

    Вот типичная легаси-иерархия из Django-проекта с системой уведомлений:

    Каждый уровень иерархии добавляет одно поведение, но наследует всё предыдущее. Нужно отправить email с трекингом, но без вложений — невозможно без нового класса.

    Композиция через Decorator (рассмотренный в статье о паттернах проектирования) решает это:

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

    Когда наследование всё-таки уместно

    Наследование не зло. Оно уместно, когда выполняются все три условия:

  • Настоящая «является»-связь: Dog действительно является Animal, а EmailNotifier не является BaseNotifier — он его использует
  • Стабильная иерархия: базовый класс не будет меняться часто
  • Полиморфное использование: клиентский код работает с базовым типом, не зная конкретный подкласс
  • В Django это означает: наследуйте модели, если они представляют разные типы одной сущности (например, AbstractUserUser). Не наследуйте, если вы просто хотите добавить поведение — для этого есть композиция и mixin.

    Практический чек-лист рефакторинга наследования

    Когда в легаси-коде встречается глубокая иерархия наследования:

  • Выделите поведение в отдельные компоненты — что именно делает каждый уровень иерархии?
  • Проверьте, является ли связь настоящей — может ли подкласс использоваться везде, где ожидается базовый класс (LSP)?
  • Замените наследование композицией, если поведение комбинируется произвольно
  • Оставьте наследование для mixin и настоящих «является»-связей
  • Проверьте отсутствие дублирования — если два подкласса копируют код, это сигнал к выделению композиционного компонента
  • Композиция даёт то, что наследование обещает, но не delivers: гибкое повторное использование кода без жёстких связей. В легаси-проектах это разница между рефакторингом за час и переписыванием с нуля за неделю.