open atlas
↑ К треку
Паттерны и качество кода CP · 08 · 01

Длинный метод, большой класс

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

CP Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Ты открываешь handleCheckout, чтобы добавить промокод. В нём 240 строк. Ты листаешь. Тут валидация, потом проверки остатков, потом расчёт налога, потом вызов платежа, потом письмо, потом аналитика, потом цикл повторов, обёрнутый вокруг чего-то, чего из места открытия цикла не видно. Чтобы найти единственное место, куда должен встать промокод, тебе нужно загрузить весь метод в голову — а не можешь, потому что он туда не влезает. Поэтому ты читаешь его трижды, ставишь правку туда, где она кажется безопасной, и надеешься.

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

Цель

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

1

Раздуватели — это распад связности, а не проблема размера. «Связность» — это насколько сильно части юнита принадлежат друг другу, насколько единственна его цель. Длинный метод или большой класс — это то, как выглядит низкая связность, когда она проработала какое-то время: каждое новое требование прикручивалось к ближайшему существующему месту вместо своего собственного, и теперь несвязанные заботы делят один scope. Длина — симптом; болезнь в том, что несколько обязанностей запутаны в один юнит.

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

2

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

// Сигнал 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», метод, который приходится листать, класс со множеством слабо разделяемых полей и стрелочная вложенность — это не придирки к стилю. Каждое говорит: здесь прячется меньший, именованный юнит — кластеры полей и вложенные блоки буквально указывают на швы.

3

Выделение метода: дай каждому блоку имя, и обработчик превратится в историю. Лекарство от длинного метода — заменить каждый кусок-с-целью вызовом хорошо названной функции. 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 и игнорирует остальное. Стоимость понимания теперь масштабируется с задаваемым вопросом, а не со всем методом.

4

Выделение класса: когда поля группируются, второй объект просится наружу. Большой класс — обычно два или три класса, носящих одно имя. Подсказка — в сигнале 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, не читая налоговый код. Радиус поражения каждого вероятного изменения схлопнулся до одного связного юнита — а в этом и весь экономический смысл разбиения раздувателя.

5

Режим отказа: дробление, а не выделение. Любительское прочтение «разбивай длинные методы» — это сделать всё коротким. Так метод на 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.