$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогРефакторинг Python-кода: как безопасно выносить функции в зависимости и сервисы

Рефакторинг Python-кода: как безопасно выносить функции в зависимости и сервисы

$ sudo teach IT
·24 июля 2026 г.·9 мин·48
Рефакторинг Python-кода: как безопасно выносить функции в зависимости и сервисы

Пошагово разберём декомпозицию: функции → сервисы → слои, сохраняя тестируемость и управляемость зависимостей. Дадим практические паттерны для миграций без простоя.

Содержание
Почему “выносить функции” нужно не только ради красотыМентальная модель: зависимости, сервисы и слоиЗависимости (dependencies)Сервисы (services)Слои (layers)Пошаговый план рефакторинга без болиШаг 1. Инвентаризация: что делает функция на самом делеШаг 2. Разделите “чистую” логику и “эффекты”Шаг 3. Введите зависимости как параметры, а не как глобальные импортыШаг 4. Соберите сервис: один класс отвечает за use-caseШаг 5. Выведите “слой API” и не дайте инфраструктуре утечь вверхУправление зависимостями: практичные паттерны для Python1) Контейнер зависимостей (composition root)2) Инверсия зависимостей через Protocol3) Минимизируйте “псевдо-инъекции”Тестируемость после декомпозиции: что именно тестироватьUnit-тесты для чистой логикиТесты сервиса с поддельными зависимостямиИнтеграционные тесты — отдельноМиграции без простоя: как менять код в проде осторожноСтратегия 1: Adapter-слой между старой функцией и сервисомСтратегия 2: Постепенная подмена зависимостейСтратегия 3: Режим “dual run”Подводные камни, которые встречаются почти всегда1) Ошибка №1: выносить в сервис “как было”, но сохранить побочные эффекты внутри домена2) Скрытые зависимости через “час/рандом/текущее время”3) Размытый интерфейс зависимостей4) Плохая обработка ошибок5) Непоследовательная структура модулейПрактический рецепт: функция → сервис → слои за 6 шаговКак выбрать уровень декомпозиции: не делать “слишком архитектурно”

Когда Python-проект растёт, “простые” функции постепенно превращаются в комок неявных зависимостей. Сегодня метод делает бизнес-логику и заодно ходит в БД, логирует, читает конфиг и дергает внешний API. Завтра вы добавляете к нему ещё один параметр “на всякий случай”, а послезавтра обнаруживается, что тесты не покрывают важные ветки — потому что их сложно поднять без настоящих сервисов.

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

Ниже — пошаговый, проверенный подход к декомпозиции функции → сервисы → слои с сохранением управляемости зависимостей и возможности миграции без простоя. Фокус — на том, как делать это в Python системно, а не “на глаз”.


Почему “выносить функции” нужно не только ради красоты

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

  • Бизнес-правила (что должно произойти) смешиваются с внешними эффектами (как это сделать).
  • Набор зависимостей становится “скрытым”: внутри функции создаются клиенты БД/HTTP, читаются переменные окружения, вызывается логирование, делаются ретраи.
  • Тесты начинают требовать реальные инфраструктуры или сложные мок-обвязки на уровне модулей.

В результате возникают симптомы:

  • трудно воспроизводить ошибки в тестах;
  • изменение внешнего API ломает бизнес-код;
  • новые фичи требуют копировать/дублировать логику или добавлять параметры “вглубь”.

Рефакторинг становится способом вернуть контроль: зафиксировать границы, определить, кто владеет зависимостями, и упростить тестирование до уровня “unit”, а не “интеграции”.


Ментальная модель: зависимости, сервисы и слои

Перед тем как трогать код, важно договориться о ролях.

Зависимости (dependencies)

Это компоненты, которые предоставляют доступ к внешним ресурсам или к инфраструктурным операциям:

  • репозитории (работа с БД),
  • клиенты HTTP,
  • файловые системы,
  • генераторы токенов,
  • объект доступа к кэшу.

Зависимости должны иметь понятный интерфейс и минимум “магии”. Часто их реализуют адаптеры.

Сервисы (services)

Сервисы — это координаторы бизнес-действий. Они используют зависимости, но не должны знать деталей инфраструктуры:

  • “Создать заказ” — сервис, который вызывает репозиторий и политику ценообразования.
  • “Синхронизировать пользователей” — сервис, который вызывает внешний API и сохраняет результат.

Сервис — это место, где соединяются зависимости и бизнес-правила.

Слои (layers)

Слои — более крупные границы в приложении:

  • API/Controller (принимает запросы, валидирует, вызывает сервисы),
  • Domain (ядро бизнес-логики; иногда выделяют отдельно),
  • Application/Use cases (часто тут живут сервисы),
  • Infrastructure (реализации зависимостей: БД, HTTP, очереди).

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


Пошаговый план рефакторинга без боли

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

Шаг 1. Инвентаризация: что делает функция на самом деле

Начните с описания существующей функции. Вам нужно выделить:

  1. Публичный контракт: входы/выходы, исключения, побочные эффекты.
  2. Состав работы:
    • вычисления/правила,
    • чтение/запись данных,
    • внешние вызовы,
    • логирование/метрики,
    • работа с временем/идентификаторами.

Полезный прием: выписать “эффекты” на отдельную схему. Например:

  • calculate_prices(order) — только математика?
  • или внутри есть db.fetch_product() и http.post(...)?

Если побочных эффектов много, не пытайтесь вынести всё сразу. Сначала зафиксируйте поведение.

Практика: зафиксируйте контракт тестом “до рефакторинга”

Если тестов нет — добавьте хотя бы характеристики:

  • вход → ожидаемый результат,
  • исключение при ошибке,
  • вызовы зависимостей (если легко замокать).

Даже минимальные тесты — страховка, чтобы рефакторинг не превратился в переписывание.


Шаг 2. Разделите “чистую” логику и “эффекты”

Если функция смешивает бизнес и инфраструктуру, сделайте промежуточный слой:

  • “чистая” функция/фрагмент: принимает данные → возвращает результат,
  • “обвязка” вокруг: занимается внешними вызовами.

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

Пример: было

code
# app/pricing.py

def apply_discount(order_id: int, discount_code: str) -> float:
    product_price = db.get_product_price(order_id)  # эффект
    user = http.get_user(order_id)                  # эффект
    discount = discount_engine.calculate(discount_code, user)  # бизнес
    db.log_discount_applied(order_id, discount)    # эффект
    return product_price - discount

В этом коде бизнес-правило размазано по инфраструктуре. Тестировать его сложно.

Пример: стало (промежуточный разрез)

code
# app/pricing_domain.py

def compute_discount(price: float, user_attributes: dict, discount_code: str) -> float:
    discount = discount_engine.calculate(discount_code, user_attributes)
    return price - discount

И отдельно:

code
# app/pricing_service.py

def apply_discount(order_id: int, discount_code: str) -> float:
    product_price = db.get_product_price(order_id)
    user = http.get_user(order_id)
    result = compute_discount(product_price, user, discount_code)
    db.log_discount_applied(order_id, result)
    return result

Теперь “domain”-часть проще покрывать юнит-тестами. А инфраструктура пока остаётся в обвязке — это нормально на данном шаге.


Шаг 3. Введите зависимости как параметры, а не как глобальные импорты

Главная причина хаоса в Python-коде — глобальные зависимости:

  • модуль с БД создаётся в момент импорта,
  • HTTP клиент живёт где-то “внутри модуля”,
  • конфиг читается в процессе импорта.

Чтобы управлять ими, начните с простой декомпозиции: передавайте зависимости параметрами функций/конструкторов.

Пример: плохой паттерн

code
import db
import http

def apply_discount(order_id: int, discount_code: str) -> float:
    product_price = db.get_product_price(order_id)
    user = http.get_user(order_id)
    ...

Пример: лучше

code
from dataclasses import dataclass
from typing import Protocol

class PricingRepository(Protocol):
    def get_product_price(self, order_id: int) -> float: ...
    def log_discount_applied(self, order_id: int, result: float) -> None: ...

class UserClient(Protocol):
    def get_user(self, user_id: int) -> dict: ...

@dataclass(frozen=True)
class PricingDependencies:
    repo: PricingRepository
    user_client: UserClient

def apply_discount(order_id: int, discount_code: str, deps: PricingDependencies) -> float:
    product_price = deps.repo.get_product_price(order_id)
    user = deps.user_client.get_user(order_id)
    result = compute_discount(product_price, user, discount_code)
    deps.repo.log_discount_applied(order_id, result)
    return result

Важный нюанс: Protocol/интерфейсы дают возможность подменять зависимости тестовыми реализациями без “моков по строкам”.


Шаг 4. Соберите сервис: один класс отвечает за use-case

На этом шаге вы преобразуете “функцию-обвязку” в сервис. Сервис инкапсулирует:

  • зависимости,
  • транзакции (если есть),
  • бизнес-оркестрацию (какие шаги и в каком порядке),
  • политику обработки ошибок.

Пример: сервис

code
class PricingService:
    def __init__(self, deps: PricingDependencies) -> None:
        self._deps = deps

    def apply_discount(self, order_id: int, discount_code: str) -> float:
        product_price = self._deps.repo.get_product_price(order_id)
        user = self._deps.user_client.get_user(order_id)
        result = compute_discount(product_price, user, discount_code)
        self._deps.repo.log_discount_applied(order_id, result)
        return result

Теперь сервис — точка входа для бизнес-действия. Все внешнее спрятано в зависимости.


Шаг 5. Выведите “слой API” и не дайте инфраструктуре утечь вверх

Если есть web-фреймворк (FastAPI/Flask/Django), не допускайте, чтобы в контроллере:

  • происходили прямые вызовы к БД/HTTP,
  • выбиралась реализация репозитория,
  • строилась сложная цепочка зависимостей.

Контроллер должен:

  1. валидировать входные данные,
  2. вызвать сервис,
  3. перевести доменные исключения в HTTP-ответы.

Пример: контроллер (FastAPI-стиль)

code
from fastapi import APIRouter, Depends

router = APIRouter()

@router.post("/discount")
def apply_discount_endpoint(payload: dict, pricing_service: PricingService = Depends(get_pricing_service)):
    return {"result": pricing_service.apply_discount(payload["order_id"], payload["code"])}

Где get_pricing_service() поднимает реализации зависимостей из контейнера/конфига.

Это снижает связность: бизнес остаётся тестируемым без HTTP.


Управление зависимостями: практичные паттерны для Python

1) Контейнер зависимостей (composition root)

Рекомендуемый принцип: создавайте зависимости в одном месте — “composition root” (обычно в startup модуля приложения). Это снижает хаос и упрощает тестирование.

В Python часто используют:

  • ручное создание в startup,
  • DI-провайдеры фреймворка,
  • light-weight контейнеры.

Суть не в инструменте, а в границе: сборка зависимостей должна быть сверху, а не внутри домена.

2) Инверсия зависимостей через Protocol

Protocol + dataclass позволяет выразить контракт зависимостей без жестких привязок.

Плюсы:

  • легко создать фейк-репозиторий для тестов,
  • минимальная магия,
  • прозрачные интерфейсы.

3) Минимизируйте “псевдо-инъекции”

Не делайте так:

  • сервис принимает “db_session” и “config” и “http_client” и “logger” — и вы снова получаете кухню в одном классе.

Лучше инкапсулировать в зависимости:

  • repo, user_client, clock, transaction_manager.

Так вы сохраняете стабильность интерфейса сервиса.


Тестируемость после декомпозиции: что именно тестировать

Важно не просто “разнести по папкам”, а привести тесты к адекватным уровням.

Unit-тесты для чистой логики

Там, где есть compute_discount и прочие расчёты без эффектов.

Тесты сервиса с поддельными зависимостями

Сервис можно тестировать, подставив фейковые реализации Protocol.

Пример фейков

code
class FakeRepo:
    def __init__(self):
        self.prices = {1: 100.0}
        self.logs = []

    def get_product_price(self, order_id: int) -> float:
        return self.prices[order_id]

    def log_discount_applied(self, order_id: int, result: float) -> None:
        self.logs.append((order_id, result))

class FakeUserClient:
    def get_user(self, user_id: int) -> dict:
        return {"segment": "vip"}

И тест сервиса:

code
def test_apply_discount_calls_dependencies():
    repo = FakeRepo()
    user_client = FakeUserClient()
    deps = PricingDependencies(repo=repo, user_client=user_client)

    service = PricingService(deps)
    result = service.apply_discount(1, "VIP10")

    assert result == 90.0
    assert repo.logs

Интеграционные тесты — отдельно

Реальные БД и HTTP — для сценариев “вместе”. Но бизнес-ядро не должно требовать их при каждом тесте.


Миграции без простоя: как менять код в проде осторожно

Рефакторинг часто запускают, когда “нельзя остановить сервис”. Значит, нужен подход, который выдержит частичную миграцию: старый и новый код могут сосуществовать.

Стратегия 1: Adapter-слой между старой функцией и сервисом

Вы оставляете старый API (функцию), но внутри делаете “прокладку” в новый сервис.

Пример: старую функцию не ломаем

code
# старый контракт остаётся:
def apply_discount(order_id: int, discount_code: str) -> float:
    deps = build_pricing_deps()  # временно: из composition root
    service = PricingService(deps)
    return service.apply_discount(order_id, discount_code)

На этом этапе:

  • вы не меняете места вызовов,
  • постепенно переводите потребителей на прямой сервис,
  • затем убираете старую функцию, когда всё готово.

Риск: не создавать зависимости “на каждый запрос” без необходимости. Лучше вынести создание в composition root.

Стратегия 2: Постепенная подмена зависимостей

Если проблема в инфраструктуре, можно мигрировать репозиторий или клиент постепенно:

  • сначала делаете новый интерфейс,
  • затем реализуете адаптер к старой БД/HTTP,
  • и только потом переключаете реальные реализации.

Стратегия 3: Режим “dual run”

Для критичных вычислений иногда запускают параллель:

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

Это полезно, когда вы не полностью уверены, что рефакторинг не повлиял на бизнес-правила. Но включать dual run нужно осторожно по стоимости и чувствительности к внешним эффектам.


Подводные камни, которые встречаются почти всегда

1) Ошибка №1: выносить в сервис “как было”, но сохранить побочные эффекты внутри домена

Сервис — это уже шаг вперёд, но если вы продолжаете вызывать БД/HTTP прямо в доменных сущностях, вы не получили выгоды. Правильная граница: доменная функция должна быть чистой (или почти чистой), сервис — организует эффекты.

2) Скрытые зависимости через “час/рандом/текущее время”

Если логика зависит от времени (deadline, TTL, “сегодня”), передавайте clock как dependency.

Иначе тесты станут нестабильными.

3) Размытый интерфейс зависимостей

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

Лучшее правило:

  • сервис просит конкретные операции, которые ему нужны,
  • репозиторий реализует их локально.

4) Плохая обработка ошибок

При рефакторинге часто “теряют” типы исключений. Договоритесь заранее:

  • какие доменные ошибки нужно нормализовать (например, NotFound, ValidationError),
  • какие — оборачивать в инфраструктурные.

Потом это напрямую влияет на то, насколько предсказуемый API у сервиса.

5) Непоследовательная структура модулей

Если сегодня бизнес-логика в app/utils.py, а завтра часть сервисов в services/ и часть в domain/, вы получите “архитектурный зоопарк”.

Ориентируйтесь на принцип: границы ответственности отражаются структурой папок и именами.


Практический рецепт: функция → сервис → слои за 6 шагов

Короткая схема, которую можно повторять для каждой функции с инфраструктурными эффектами:

  1. Сделайте контракт явным: что функция возвращает и какие эффекты делает.
  2. Вынесите чистую логику в отдельные функции/модули (domain).
  3. Передайте зависимости параметрами (Protocol + dataclass/конструктор).
  4. Инкапсулируйте оркестрацию в сервис: один use-case — один сервис.
  5. Изолируйте API-слой: контроллер валидирует и вызывает сервис.
  6. Сделайте миграцию через адаптеры: не ломайте существующие вызовы, переключайте постепенно.

Как выбрать уровень декомпозиции: не делать “слишком архитектурно”

Есть соблазн превратить каждый use-case в “чистую архитектуру” с десятком папок и абстракций. Для Python-проектов это может быть избыточно, особенно если команда небольшая.

Ориентиры:

  • если бизнес-логика тестируется — декомпозиция правильная;
  • если

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

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

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1050
Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1030
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

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

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему “выносить функции” нужно не только ради красотыМентальная модель: зависимости, сервисы и слоиЗависимости (dependencies)Сервисы (services)Слои (layers)Пошаговый план рефакторинга без болиШаг 1. Инвентаризация: что делает функция на самом делеШаг 2. Разделите “чистую” логику и “эффекты”Шаг 3. Введите зависимости как параметры, а не как глобальные импортыШаг 4. Соберите сервис: один класс отвечает за use-caseШаг 5. Выведите “слой API” и не дайте инфраструктуре утечь вверхУправление зависимостями: практичные паттерны для Python1) Контейнер зависимостей (composition root)2) Инверсия зависимостей через Protocol3) Минимизируйте “псевдо-инъекции”Тестируемость после декомпозиции: что именно тестироватьUnit-тесты для чистой логикиТесты сервиса с поддельными зависимостямиИнтеграционные тесты — отдельноМиграции без простоя: как менять код в проде осторожноСтратегия 1: Adapter-слой между старой функцией и сервисомСтратегия 2: Постепенная подмена зависимостейСтратегия 3: Режим “dual run”Подводные камни, которые встречаются почти всегда1) Ошибка №1: выносить в сервис “как было”, но сохранить побочные эффекты внутри домена2) Скрытые зависимости через “час/рандом/текущее время”3) Размытый интерфейс зависимостей4) Плохая обработка ошибок5) Непоследовательная структура модулейПрактический рецепт: функция → сервис → слои за 6 шаговКак выбрать уровень декомпозиции: не делать “слишком архитектурно”