Введение объекта-параметра
Замени повторяющуюся группу данных или длинный список параметров объектом-параметром, называющим реальную концепцию: связанное поведение и валидация притягиваются к нему, а места вызова становятся яснее — не превращая объект в свалку.
Ты снова и снова видишь, как одни и те же три аргумента путешествуют вместе: startDate, endDate и timezone едут бок о бок через bookRoom, затем priceStay, затем renderCalendar, затем экспортёр отчётов. Никто не решал, что они принадлежат друг другу; они просто всегда появляются троицей, потому что концептуально они и есть одно — отрезок времени. Код нигде этого не говорит, поэтому каждая функция заново выводит «валиден ли этот диапазон?», «сколько ночей?», «пересекаются ли они?» сама по себе, каждый раз чуть иначе.
Эта повторяющаяся троица — группа данных (data clump), а длинные списки аргументов, которые она питает, — это запах с конкретным лекарством: дай группе имя. Введение объекта-параметра заменяет группу (или чрезмерно длинный список параметров) одним типизированным объектом — и неожиданная выгода вовсе не в укороченной сигнатуре. Она в том, что новый объект становится домом для всего поведения, что было разбросано по вызывающим.
После этого урока ты можешь распознать группу данных и слишком длинный список параметров как триггер для Introduce Parameter Object; выполнить рефакторинг безопасными шагами под зелёным набором тестов; объяснить, почему настоящий выигрыш — это миграция поведения и валидации на новый тип, а не укороченная сигнатура; и назвать режим отказа — объект-параметр, который оказался свалкой несвязанных полей, собранных лишь ради укорачивания вызова, вместо связной концепции.
Триггер — это группа данных или длинный список параметров: одни и те же поля, путешествующие вместе. Группа — это два и более значения, которые постоянно появляются как набор: (startDate, endDate), (x, y), (amount, currency), (host, port, protocol). Признак механический: если удалить одно из них, остальные перестают иметь смысл. Длинный список параметров — та же болезнь в одном месте вызова: пять, шесть, восемь позиционных аргументов, где читатель уже не может сказать, который из них что.
// before: the (start, end) clump rides through every function, plus boolean noise
function priceStay(
start: Date,
end: Date,
ratePerNight: number,
taxRate: number,
includeCleaning: boolean,
isWeekendPremium: boolean,
): number { /* ... */ }
book(start, end, 120, 0.2, true, false); // which boolean is which?Позиционные булевы нечитаемы, а пара (start, end) уже появилась в четырёх функциях. И то и другое — сигналы, а не шум: они говорят тебе, что не хватает концепции.
Делай это безопасными шагами, оставаясь зелёным всю дорогу. Introduce Parameter Object — это механический рефакторинг с заданной последовательностью, и тесты остаются зелёными после каждого шага, а не переписываются одним прыжком:
- Создай новый тип с полями группы. Пока не меняй ни одной сигнатуры.
- Добавь объект как новый параметр целевой функции, сохранив старые; тело пусть читает из нового объекта. Тесты по-прежнему зелёные.
- Обнови одного вызывающего, чтобы он передавал объект. Зелёный. Затем следующего вызывающего. Зелёный.
- Как только каждый вызывающий передаёт объект, удали ставшие неиспользуемыми отдельные параметры.
- Перенеси поведение, оперировавшее значениями группы, на новый тип (следующий шаг).
type DateRange = { readonly start: Date; readonly end: Date };
function priceStay(range: DateRange, rate: RateCard): number { /* ... */ }Каждый шаг обратим и проверен, поэтому ошибка ловится в одном ходу от того места, где ты её совершил, — в этом весь смысл рефакторинга под тестами, а не через big-bang-переписывание.
Настоящая выгода: поведение и валидация притягиваются к новому объекту. Как только DateRange появился, у каждой проверки «валиден ли этот диапазон?» / «сколько ночей?» / «пересекаются ли они?», дублировавшейся по вызывающим, есть очевидный дом — на типе. Мартин Фаулер называет это более глубокой причиной рефакторинга: новый объект становится магнитом, притягивающим разбросанное вокруг него поведение.
class DateRange {
constructor(readonly start: Date, readonly end: Date) {
if (end < start) throw new RangeError("end before start");
}
get nights(): number {
return Math.round((+this.end - +this.start) / 86_400_000);
}
overlaps(other: DateRange): boolean {
return this.start < other.end && other.start < this.end;
}
}Валидация end < start теперь происходит один раз, при конструировании, — так что ни одна нижестоящая функция уже не может получить перевёрнутый диапазон. nights вычисляется в одном месте, а не выводится заново (с off-by-one-багами) в прайсинге, календаре и отчётности. В этом senior-мысль: объект-параметр не просто укорачивает сигнатуры, он называет недостающую концепцию и даёт связанной логике место, где жить, что убивает целый класс дублирования.
Целый объект (whole-object): передай объект, который у тебя уже есть, вместо его распаковки. Близкий родственник: когда вызывающий вытаскивает несколько полей из существующего объекта лишь для того, чтобы передать их как отдельные аргументы, передай сам объект. Это сокращает список параметров, развязывает вызываемого от раскладки полей вызывающего и позволяет вызываемому позже спросить объект о большем без изменения сигнатуры.
// before: caller unpacks, callee re-couples to the field names
const within = isWithinBudget(quote.low, quote.high, order.total);
// after: pass the whole object; the rule moves where it belongs
const within = quote.contains(order.total);Когда НЕ стоит: если вызываемому действительно нужно лишь одно поле, передача целого объекта переусиливает связь с типом, который он едва использует — предпочти единственное значение (это напряжение между Законом Деметры и сегрегацией интерфейсов). Whole-object уместен, когда вызываемый использует несколько полей или когда «правило живёт на объекте» читается естественнее, чем «вызывающий вычисляет правило».
Вызов с пятью аргументами превращается в один типизированный объект — и логика следует за ним. Начни с длинного списка параметров, чьи первые два аргумента — группа, а вызовы нечитаемы:
// before
function reserve(
start: Date,
end: Date,
guestId: string,
roomId: string,
notifyByEmail: boolean,
): Reservation {
if (end < start) throw new Error("bad dates"); // re-checked everywhere
const nights = (+end - +start) / 86_400_000; // re-derived everywhere
// ...
}
reserve(start, end, "g_91", "r_204", true); // positional soupШаги 1–4 (механически, под зелёными тестами): введи DateRange, проведи его сквозь, отправь start/end на покой. Шаг 5 (выгода): перенеси проверку валидности и подсчёт ночей на DateRange, удалив копии в reserve, priceStay и calendar:
// after
class DateRange {
constructor(readonly start: Date, readonly end: Date) {
if (end < start) throw new RangeError("end before start");
}
get nights(): number { return Math.round((+this.end - +this.start) / 86_400_000); }
}
function reserve(stay: DateRange, guestId: string, roomId: string, notifyByEmail: boolean): Reservation {
const nights = stay.nights; // no validation here — it can't be invalid
// ...
}
reserve(stay, "g_91", "r_204", true); // stay reads as one thingЧто изменилось помимо сигнатуры: перевёрнутый диапазон теперь невозможно передать (конструктор — единственная дверь), nights существует один раз, а priceStay/calendar лишились своих дублированных копий обоих. Объект-параметр не просто прибрал вызов — он переместил правила туда, где им место. Заметь, что мы не запихали guestId, roomId и notifyByEmail в тот же объект: они не образуют связную концепцию с датами, поэтому их объединение создало бы свалку (см. ниже).
▸Почему это работает
Почему «объект притягивает поведение» — настоящий смысл, а не укороченная сигнатура? Потому что укороченная сигнатура косметична и обратима — её можно получить кортежем // prettier и ничего не выиграть. Долговременная выгода в том, что именованный тип даёт дублированной логике однозначный дом, а как только у неё есть дом, дублирование схлопывается, а инварианты можно обеспечивать при конструировании. DateRange, валидирующий end >= start один раз, означает, что каждая нижестоящая функция освобождена от этой проверки и не может получить сломанный диапазон. Это структурная гарантия, а не предпочтение опрятности. Senior-ревьюеры одобряют этот рефакторинг за инвариант и дедупликацию, а более чистое место вызова считают приятным побочным эффектом.
▸Частая ошибка
Режим отказа: объект-параметр-свалка. Под давлением укоротить сигнатуру reserve(a, b, c, d, e, f, g) кто-то связывает все семь аргументов в мешок ReserveParams — даты, id гостя, id комнаты, флаг email, промокод, источник — исключительно чтобы сделать вызов одним аргументом. Это не Introduce Parameter Object; это прятание длинного списка параметров внутри структуры. Поля не связны, никакое поведение не может разумно жить на мешке (что вообще означало бы ReserveParams.validate()?), и ты сделал тип труднее для чтения, никого ничему не научив. Тест на настоящий объект-параметр: можешь ли ты назвать его в честь концепции, которая уже есть в домене (диапазон дат, денежная сумма, адрес), и хочет ли поведение жить на нём? Если единственное, что объединяет поля, — это «они аргументы одной и той же функции», то это свалка — держи их отдельно или найди меньшие связные группы, прячущиеся внутри (даты, затем, может быть, Guest), и вынеси их.
Ты вводишь DateRange, чтобы заменить повторяющуюся группу (start, end), и места вызова становятся короче. Помимо укороченных сигнатур, каково главное senior-обоснование этого рефакторинга?
Группа данных (одни и те же поля, всегда путешествующие вместе) и длинный список параметров оба сигнализируют о недостающей концепции. Introduce Parameter Object лечит их: создай типизированный объект для полей группы и проведи его сквозь вызывающих безопасными, зелёными-всю-дорогу шагами, затем удали разрозненные параметры. Укороченная сигнатура — видимое изменение, но долговременный выигрыш в том, что связанное поведение и валидация притягиваются к новому типу — валидность обеспечивается один раз при конструировании, производные значения вроде nights существуют в одном месте, а нижестоящие функции уже не могут получить сломанное значение. Целый объект (whole-object) — родственный ход: передай объект, который у тебя уже есть, вместо распаковки его полей, когда вызываемый использует несколько из них. Режим отказа — свалка: связывание несвязанных аргументов лишь ради укорачивания вызова. Дисциплина: объект-параметр должен называть реальную, связную концепцию, которая есть в домене, такую, на которой поведение действительно хочет жить; если нет — ты лишь спрятал длинный список параметров.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.