Длинный метод, большой класс
Запахи-раздуватели — длинный метод и большой класс — это распад связности: обязанности копятся, пока юнит уже не удержать в голове. Замечай по сигналам, лечи выделением по швам ответственности, а не по числу строк.
Ты открываешь handleCheckout, чтобы добавить промокод. В нём 240 строк. Ты листаешь. Тут валидация, потом проверки остатков, потом расчёт налога, потом вызов платежа, потом письмо, потом аналитика, потом цикл повторов, обёрнутый вокруг чего-то, чего из места открытия цикла не видно. Чтобы найти единственное место, куда должен встать промокод, тебе нужно загрузить весь метод в голову — а не можешь, потому что он туда не влезает. Поэтому ты читаешь его трижды, ставишь правку туда, где она кажется безопасной, и надеешься.
Этот метод работает. Он работает два года. И каждое изменение в нём стоит дня, потому что юнит кода, который нужно понять, чтобы изменить одну вещь, — это всё целиком. Это и есть раздуватель: метод или класс, накопивший столько обязанностей, что никто не удержит его в голове, — и стоимость каждого изменения масштабируется с размером, который приходится осмыслить.
После этого урока ты можешь назвать сигналы обнаружения, которые помечают длинный метод или большой класс ещё до того, как они станут несопровождаемыми; применять выделение метода и выделение класса, ведомые обязанностями, а не числом строк; превращать длинный обработчик в короткий рассказ из именованных шагов; и распознавать режим отказа, когда ты дробишь раздуватель в мелочёвку без группировки — перемещая сложность вместо того, чтобы её уменьшить.
Раздуватели — это распад связности, а не проблема размера. «Связность» — это насколько сильно части юнита принадлежат друг другу, насколько единственна его цель. Длинный метод или большой класс — это то, как выглядит низкая связность, когда она проработала какое-то время: каждое новое требование прикручивалось к ближайшему существующему месту вместо своего собственного, и теперь несвязанные заботы делят один scope. Длина — симптом; болезнь в том, что несколько обязанностей запутаны в один юнит.
Эта переформулировка важна, потому что говорит, где резать. Будь проблема в размере, ты бы делил по середине. Поскольку проблема в смешанных обязанностях, ты режешь там, где обязанности меняются, — даже если это оставит один кусок на 80 строк, а другой на 8. Цель — шов, а не число строк.
Выучи сигналы обнаружения — они срабатывают раньше, чем код станет неуправляемым. Тебе не нужен порог по строкам; тебе нужно замечать запах рано. Надёжные сигналы:
// Сигнал 1: ты листаешь, чтобы прочитать один логический юнит (он не влезает на экран)
// Сигнал 2: "and" прячется в имени → две работы в одной
function validateAndPersistAndNotify(order: Order) { /* ... */ }
// Сигнал 3: класс копит поля экземпляра, которые делят немногие методы
class OrderService {
private db; private mailer; private taxRates; private inventory;
private pdfRenderer; private analytics; private retryPolicy;
// 7 коллабораторов → это как минимум 7 вещей
}
// Сигнал 4: глубокая вложенность — блоки на 4+ уровня означают скрытые подпроцедуры
if (a) { for (const x of xs) { if (b) { try { /* ... */ } catch {} } } }Имя с «and», метод, который приходится листать, класс со множеством слабо разделяемых полей и стрелочная вложенность — это не придирки к стилю. Каждое говорит: здесь прячется меньший, именованный юнит — кластеры полей и вложенные блоки буквально указывают на швы.
Выделение метода: дай каждому блоку имя, и обработчик превратится в историю. Лекарство от длинного метода — заменить каждый кусок-с-целью вызовом хорошо названной функции. Senior-ход в том, что имена складываются в читаемое резюме — после выделения метод верхнего уровня должен читаться как оглавление, а не как реализация.
// до: одна стена, ты читаешь строку за строкой, чтобы узнать, что он делает
function checkout(cart: Cart, user: User) {
// ...40 строк валидации...
// ...30 строк расчёта цены...
// ...25 строк платежа...
// ...20 строк исполнения заказа...
}
// после: тело — это рассказ; детали уходят на уровень ниже
function checkout(cart: Cart, user: User) {
validate(cart, user);
const price = priceOf(cart, user);
const charge = chargePayment(user, price);
fulfil(cart, charge);
}Ты не удалил сложность — validate по-прежнему делает работу. Но ты её слоил: читатель, которому нужна форма, читает четыре строки; читатель, гоняющийся за багом в ценообразовании, открывает priceOf и игнорирует остальное. Стоимость понимания теперь масштабируется с задаваемым вопросом, а не со всем методом.
Выделение класса: когда поля группируются, второй объект просится наружу. Большой класс — обычно два или три класса, носящих одно имя. Подсказка — в сигнале 3 из шага 2: если половина полей и трогающих их методов касается налога, а другая половина — письма, то это два связных подмножества, делящих scope по случайности. Выдели кластер в собственный класс и пусть оригинал делегирует.
// до: OrderService знает расчёт налога И форматирование письма И аналитику
class OrderService { /* taxRates, mailer, templates, analytics, ... */ }
// после: каждая обязанность — собственный связный юнит
class TaxCalculator { taxFor(amount: number, region: Region): number { /* ... */ } }
class OrderMailer { sendConfirmation(order: Order): void { /* ... */ } }
class OrderService {
constructor(private tax: TaxCalculator, private mailer: OrderMailer) {}
place(order: Order) {
const total = this.tax.taxFor(order.subtotal, order.region) + order.subtotal;
this.mailer.sendConfirmation({ ...order, total });
}
}Теперь изменение налогового правила трогает TaxCalculator и больше ничего; команда писем работает в OrderMailer, не читая налоговый код. Радиус поражения каждого вероятного изменения схлопнулся до одного связного юнита — а в этом и весь экономический смысл разбиения раздувателя.
Режим отказа: дробление, а не выделение. Любительское прочтение «разбивай длинные методы» — это сделать всё коротким. Так метод на 200 строк становится двадцатью однострочными методами — step1, step2, doThing, doThingHelper — и теперь ты прыгаешь двадцать раз, чтобы проследить один поток, без концептуального уровня, на котором можно остановиться. Ты переместил сложность в граф вызовов; ты её не уменьшил. Хуже того, фрагменты, не делящие никакой обязанности, теперь маскируются под собратьев, что активно вводит в заблуждение следующего читателя.
Дисциплина, которая это предотвращает: каждый выделенный юнит должен называть обязанность, которую узнал бы доменный человек — validate, priceOf, chargePayment, — а не позицию в исходном файле. Если ты не можешь дать куску честное, односмысловое имя, ты нашёл кусок кода, который ещё не соответствует одной идее, и нарезка лишь прячет это. Режь по швам, которые что-то значат. Когда настоящего шва нет, оставить связный метод на 60 строк целым лучше, чем раздробить его в шум.
Длинный обработчик становится коротким рассказом из именованных шагов. Вот обработчик регистрации, который рос по одной обязанности за раз:
async function register(req: Request): Promise<Response> {
const body = await req.json();
if (!body.email || !body.email.includes("@")) return bad("email");
if (!body.password || body.password.length < 8) return bad("password");
const existing = await db.users.findByEmail(body.email);
if (existing) return conflict("email taken");
const hash = await bcrypt.hash(body.password, 12);
const user = await db.users.insert({ email: body.email, hash });
const token = jwt.sign({ sub: user.id }, SECRET, { expiresIn: "15m" });
const link = `${BASE_URL}/verify?token=${token}`;
await mailer.send(user.email, "Verify your email",
`Welcome! Confirm here: ${link}`);
analytics.track("signup", { userId: user.id, source: req.query.utm });
return created({ id: user.id });
}Четыре обязанности делят один scope: провалидировать ввод, создать аккаунт, отправить подтверждение, записать аналитику. Сигналы обнаружения все тут — ты листаешь, чтобы его прочитать, неявное имя — это «validate and create and email and track», а вложенность прячет логику ранних возвратов. Выдели по этим четырём швам:
async function register(req: Request): Promise<Response> {
const input = await parseAndValidate(req); // шов валидации
if (!input.ok) return bad(input.field);
const user = await createAccount(input.email, input.password); // шов аккаунта
if (!user.ok) return conflict("email taken");
await sendVerificationEmail(user.value); // шов уведомления
trackSignup(user.value.id, req.query.utm); // шов аналитики
return created({ id: user.value.id });
}Обработчик теперь — рассказ из пяти строк; каждое имя — обязанность, которую узнал бы продакт. Изменение политики пароля живёт целиком в parseAndValidate; изменение текста письма живёт в sendVerificationEmail. Заметь, чего мы не делали: мы не выделили req.json() в getBody() или created(...) в respond201(). Это не обязанности — это однострочники. Их выделение было бы режимом отказа через дробление: больше прыжков, не яснее смысла.
▸Почему это работает
Почему обязанности, а не строки? Потому что число строк — запаздывающий, произвольный прокси, а обязанность — настоящая причина. Два метода одинаковой длины могут быть противоположными запахами: машина состояний на 90 строк, которая делает одну вещь со множеством случаев, может быть идеально связной и её следует оставить целой, тогда как метод на 30 строк, который валидирует, сохраняет и шлёт письмо, — это три вещи, и его надо резать. Если настроишься на порог по строкам, ты будешь делить связные методы (создавая режим отказа через дробление) и оставлять короткие-но-запутанные в покое (упуская настоящий запах). Порог — это пожарная сигнализация, которая велит тебе посмотреть; решение резать и где — всегда «сколько обязанностей тут живёт и где они меняются?».
▸Частая ошибка
Самый частый способ «починить» раздуватель неправильно — обменять один метод на 200 строк на двадцать однострочников без концептуальной группировки. Это ощущается прогрессом — каждый метод теперь короткий, линтер доволен — но ты измерил не то. Ты оптимизировал под малые юниты, когда целью были связные юниты, о которых можно рассуждать на одном уровне. Читатель теперь скачет через step1 → step2 → helperA → helperB, чтобы проследить единый поток, без высоты покоя, где вся операция подытожена. Ты не уменьшил сложность; ты размазал её по графу вызовов и добавил стоимость двадцати новых имён. Лекарство — честное именование: если выделенный кусок не может взять односмысловое имя из домена, это не шов — там не режь.
Ревьюер рефакторит метод на 180 строк в 18 десятистрочных методов с именами step1…step18, каждый вызывается раз по порядку. Все методы короткие, и линтер длины строк проходит. Раздуватель вылечен?
Длинный метод и большой класс — это запахи-раздуватели, а раздуватель — это распад связности: обязанности копятся в одном юните, пока никто уже не удержит его в голове, и стоимость каждого изменения масштабируется с размером, который надо осмыслить. Замечай их по сигналам, срабатывающим рано — листание ради чтения одного юнита, «and» в имени, множество слабо разделяемых полей экземпляра, глубокая вложенность — и лечи через выделение метода и выделение класса, ведомые обязанностями. Хорошее выделение превращает длинный обработчик в короткий рассказ из именованных шагов, каждый делегирует на уровень ниже, и стоимость понимания масштабируется с вопросом, а не со всем. Режим отказа, выглядящий как успех, — это дробление: обмен одного метода на 200 строк на двадцать однострочников без концептуальной группировки, — оно перемещает сложность в граф вызовов вместо того, чтобы её уменьшить. Правило, которое это предотвращает: режь по швам, называющим настоящую обязанность, никогда по произвольному числу строк, и оставляй связный метод целым, когда честного шва нет.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.