На каждом ревью найдётся кто‑то, кто спросит «Зачем четыре файла, если это один пайплайн?»

А затем, давайте объясню!


Как это происходит

Очевидно, что никто не садится и не пишет processor.py на 900 строк намеренно.

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

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


Почему лимит вообще работает

Популярный аргумент звучит так: «большие файлы труднее читать, потому что рабочая память ограничена» — с отсылкой к Миллеру (1956) и Коуэну (2001). Это правда, но прямой экстраполяции нет: Миллер изучал запоминание случайных слогов, не чтение кода в IDE с навигацией по символам.

Реальная проблема — не в скролле. Она в неопределённости.

Когда открываешь permissions.py — область понятна из имени. Когда открываешь service.py на 800 строк с четырьмя разными ответственностями — сначала нужно восстановить карту файла в голове, и только потом трогать. Этот overhead не катастрофичен сам по себе, но он накапливается каждый раз, когда ты заходишь в файл.

Есть и эмпирика: Jay et al. (2009) проверили более 1,2 млн файлов из SourceForge и обнаружили линейную зависимость между LOC и цикломатической сложностью — устойчивую к языку и парадигме. Большой файл с высокой вероятностью сложный, независимо от аккуратности написания. Правда, Landman et al. (2016) эту корреляцию оспаривают — на Java и C результаты слабее. Данные неоднозначны. Но как первая метрика, которая дёшево считается и достаточно часто срабатывает — LOC работает.


Мои цифры — и почему они разные

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

Функция — 80 строк. Если не помещается в экран без скролла, скорее всего делает больше одного дела. 80 строк — это точка, где я останавливаюсь и спрашиваю: это действительно одна задача?

Handler / pipeline — 350 строк. Хендлер принимает событие, валидирует данные, передаёт дальше. Всё. Бизнес‑логики здесь нет. 350 строк — это около 10–12 хендлеров с валидацией. Больше — возможно, логика уже поехала не туда.

Processor / service — 450 строк. Здесь живёт реальная логика. 450 — точка, после которой мне становится тяжелее держать в голове внутренние зависимости класса без постоянного возврата наверх. Если перерастает — почти всегда это два смешанных контракта, а не просто «много кода».

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


Пример 1 — FSM‑хендлер, который вырос органически

Задача: пошаговый мастер подачи заявки в боте (aiogram 3 + FSMContext). Девять шагов с ветками, отмена на любом этапе, inline‑клавиатуры на каждый переход.

До:

bot/
├── handlers/
│   ├── hr_request/
│   │   ├── __init__.py         # регистрация router
│   │   └── wizard.py           # ~800 строк за полгода выросло из 200:
│   │                           # добавлялись ветки флоу, keyboards рядом с хендлерами,
│   │                           # валидация там же - ведь "один флоу, незачем дробить"
│   └── admin.py
├── services/
│   └── ticket_service.py       # 310 строк
└── db/
    └── repository.py           # ~490 строк: один TicketRepository, CRUD + аналитические JOIN
                                # добавлялись методы по запросам

~800 строк в wizard.py — не хаос. Логика прослеживается, имена понятны. Проблема появляется, когда клавиатура на шаге 6 рендерит не то и нужно найти конкретный InlineKeyboardMarkup среди всего потока переходов. А правка клавиатуры рядом с логикой перехода — это риск задеть соседний код, который ты в этот момент вообще не читаешь.

Часто возникает вопрос: почему не два класса в одном файле? Потому что StepsHandler и WizardKeyboards меняются по разным причинам — первый при изменении бизнес‑логики, второй при изменениях UI.

В одном файле это смешивается в git diff: правка кнопки выглядит как правка флоу.

В разных файлах граница видна сразу — и в diff, и при импорте.

После:

bot/
├── handlers/
│   ├── hr_request/
│   │   ├── __init__.py
│   │   ├── states.py           # 25 строк  - StatesGroup
│   │   ├── steps.py            # 310 строк - только переходы
│   │   ├── keyboards.py        # 195 строк - клавиатуры по шагам
│   │   └── validators.py       # 120 строк - input валидация
│   └── admin.py
├── services/
│   └── ticket_service.py       # 310 строк
└── db/
    ├── ticket_repo.py          # 280 строк - CRUD + простые выборки
    └── ticket_queries.py       # 230 строк - агрегаты, JOIN, аналитика

Суммарный LOC немного вырос — за счёт импортов и init.py, но цель не в этом, цель — предсказуемая граница изменений: клавиатура шага 6 это keyboards.py на 195 строк, а не поиск по 800.

Репозиторий разбился по той же логике: CRUD‑методы и аналитические запросы меняются по разным поводам и теперьticket_queries.py трогаешь при новых отчётах, ticket_repo.py — при изменениях схемы. Разная частота, разные причины.

aiogram 3.x добавил Scene‑классы как осознанную альтернативу — весь флоу в одном изолированном классе. Валидный подход. Но если внутри сцены начинают жить генераторы клавиатур и валидационная логика — она вырастет точно так же.


Пример 2 — Maya‑процессор по типам ассетов

Начинали с mesh. Потом пришли rig, animation, fx — у каждого своя логика нормализации имён и валидации LOD.

До:

tools/
└── maya_asset_tool/
    ├── ui/
    │   └── main_window.py          # 290 строк
    ├── core/
    │   └── asset_processor.py      # ~680 строк: BaseProcessor + MeshProcessor + RigProcessor + AnimProcessor
    │                               # тут казалось логичным держать все процессоры вместе
    └── integrations/
        ├── perforce.py             # 170 строк
        └── maya_api.py             # 200 строк

Четыре хорошо написанных класса. Проблема в тестах: from core.asset_processor import RigProcessor тянет весь модуль — включая Maya‑зависимости MeshProcessor, которых в тестовой среде нет. Мокаешь то, что к тесту вообще не относится.

После:

tools/
└── maya_asset_tool/
    ├── ui/
    │   └── main_window.py          # 290 строк
    ├── core/
    │   ├── base_processor.py       # 110 строк
    │   ├── mesh_processor.py       # 185 строк
    │   ├── rig_processor.py        # 170 строк
    │   ├── anim_processor.py       # 150 строк
    │   └── validators.py           # 125 строк
    └── integrations/
        ├── perforce.py             # 170 строк
        └── maya_api.py             # 200 строк

from core.rig_processor import RigProcessor — только то, что нужно.

Пришёл VFX — создаёшь vfx_processor.py, остальные не трогаешь.

Граница изменений стала явной.


Когда большой файл нормален

Тексты и шаблоны. Файл на 900 строк из констант или строк, сгруппированных по классам, читается совершенно иначе. Нет зависимостей между блоками, нет сайд‑эффектов. Открыл, нашёл нужный класс, поправил.

Репозиторий на 500+ строк — нормально, если все методы независимы, каждый не длиннее ~50 строк, никакой бизнес‑логики. Например, get_by_id() ничего не знает про get_with_analytics() (у сервисного слоя зависимостей между методами больше, поэтому и порог ниже).

Scene в aiogram 3x — осознанное архитектурное решение фреймворка, а не исключение из правил.


Как это контролировать

Лимиты работают только если их не нужно помнить. Мы с коллегой ввели простую проверку в CI: скрипт считает непустые строки в файлах по суффиксу имени и возвращает варнинги, если файл перевалил за лимит своего типа. Не как строгий запрет, а чтобы сделать превышение видимым.

Если файл растёт — это должно быть осознанным решением с явным исключением, а не случайным дрейфом, который замечаешь через три месяца.


Инструмент, которым пользуются каждый день, не прощает «знаю где баг, но не знаю что зацеплю». Лимиты на размер файлов — один из способов держать эту уверенность на протяжении нескольких лет жизни проекта.

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


А у Вас есть формальные ограничения в команде, или держитесь на договорённостях?