|
Professor Seleznov
|
На каждом ревью найдётся кто‑то, кто спросит «Зачем четыре файла, если это один пайплайн?» А затем, давайте объясню! - Как это происходит Очевидно, что никто не садится и не пишет 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: скрипт считает непустые строки в файлах по суффиксу имени и возвращает варнинги, если файл перевалил за лимит своего типа. Не как строгий запрет, а чтобы сделать превышение видимым. Если файл растёт — это должно быть осознанным решением с явным исключением, а не случайным дрейфом, который замечаешь через три месяца.-Инструмент, которым пользуются каждый день, не прощает «знаю где баг, но не знаю что зацеплю». Лимиты на размер файлов — один из способов держать эту уверенность на протяжении нескольких лет жизни проекта. Не универсальный рецепт. Просто то, что работает у меня уже несколько лет.-А у Вас есть формальные ограничения в команде, или держитесь на договорённостях?-Источник
|