Рефакторинг к паттернам
Паттерны — это цели, К которым ты рефакторишь, когда запах их требует, а не шаблоны, навязываемые заранее. Дублированное создание тянет к Фабрике, растущий условный оператор — к Стратегии или Состоянию. YAGNI не даёт впихнуть все 23 паттерна GoF в простое приложение.
Ты выучил каталог «Банды четырёх» и теперь видишь все 23 паттерна повсюду. Поэтому хватаешься за них заранее: новая фича получает Стратегию, и Фабрику, и Строителя ещё до того, как появился хоть один дубль. Полгода спустя кодовая база — лабиринт косвенности, где любое изменение в одну строку требует трассировки через четыре файла, а большая часть заложенной гибкости так и не пригодилась.
Есть другой способ прийти к паттернам, предложенный Джошуа Кериевски: паттерны — это цели, К которым ты рефакторишь, когда код их просит, а не чертежи, от которых ты стартуешь. Появляется запах, ты его убираешь, и форма, на которой ты приземляешься, оказывается именованным паттерном. Паттерн — это направление, в котором тебя тянет запах, а YAGNI — тормоз, который не даёт уехать туда до того, как запах появился.
После этого урока ты можешь объяснить, почему паттерн — это конечное состояние рефакторинга, а не стартовый шаблон; распознать конкретные запахи, которые тянут код к Фабрике, Стратегии или машине Состояний; и применять senior-дисциплину рефакторинга к паттернам вместо преждевременного движения к ним — позволяя запаху оправдывать паттерн и позволяя YAGNI накладывать вето на паттерны, которые не заслужил ни один запах.
Паттерн — это пункт назначения, а не стартовая точка. Каталог «Банды четырёх» описывает повторяющиеся решения; он не говорит тебе, когда проблема присутствует. Переосмысление Кериевски — «рефакторинг к паттернам» — трактует каждый паттерн как цель серии маленьких рефакторингов, сохраняющих поведение, применяемых только тогда, когда код демонстрирует запах, который этот паттерн растворяет. Ты не решаешь «здесь нужна Стратегия» на дизайн-митинге; ты замечаешь условный оператор, который продолжает расти вдоль одной оси, и именно это наблюдение делает Стратегию правильным ходом. Паттерн заслуживает своё место, убирая конкретную боль, а не появляясь в учебнике.
Практическое следствие: ты никогда не пишешь interface PaymentStrategy в первый день. Ты пишешь самое простое, что работает, и позволяешь второму и третьему способу оплаты показать тебе дублирование, прежде чем извлекать абстракцию.
Дублированная логика создания тянет тебя к Фабрике. Когда одно и то же многошаговое создание объекта появляется в нескольких местах вызова — и особенно когда создаваемый тип зависит от значения времени выполнения — запах состоит в дублированном, ветвящемся создании. Рефакторинг, который его убирает, приземляет тебя на фабрику.
// SMELL: the same "which class?" decision is duplicated at every call site
const a = type === "pdf" ? new PdfReport(data) : new CsvReport(data);
// ...elsewhere, again:
const b = kind === "pdf" ? new PdfReport(other) : new CsvReport(other);Решение «какой подкласс Report» скопировано через копипасту. Централизуй его:
function createReport(type: string, data: Data): Report {
if (type === "pdf") return new PdfReport(data);
return new CsvReport(data);
}
const a = createReport(type, data);
const b = createReport(kind, other);Теперь новый формат — это одна правка внутри createReport; вызывающие зависят от концепции «сделать отчёт», а не от конкретных классов. Ты не планировал Фабрику — ты убрал дублирование, и Фабрика — это то, что произвело устранение дублирования.
Условный оператор, растущий вдоль одной оси, тянет тебя к Стратегии. switch или цепочка if, которая продолжает набирать ветви по одной и той же причине — новый способ доставки, новое правило ценообразования, новый формат экспорта, — это запах «операторов switch». Каждая ветвь — взаимозаменяемый алгоритм, втиснутый в одну функцию. Стратегия — это то, куда тебя приводит устранение этого запаха: вытащи каждую ветвь в её собственный объект за общим интерфейсом, и условный оператор схлопывается в поиск по таблице.
// SMELL: one function that grows a branch every time a method is added
function shippingCost(method: string, weight: number): number {
if (method === "standard") return weight * 1.5;
if (method === "express") return weight * 1.5 + 10;
if (method === "overnight") return weight * 3 + 25;
throw new Error("unknown method");
}Рефакторь к Стратегии — но только когда третья или четвёртая ветвь доказали, что ось реальна:
type ShippingStrategy = (weight: number) => number;
const strategies: Record<string, ShippingStrategy> = {
standard: (w) => w * 1.5,
express: (w) => w * 1.5 + 10,
overnight: (w) => w * 3 + 25,
};
function shippingCost(method: string, weight: number): number {
const strategy = strategies[method];
if (!strategy) throw new Error("unknown method");
return strategy(weight);
}Добавить способ теперь — это запись в таблице, а не ветвь в растущей функции, и не связанные между собой способы перестают делить общий путь кода, который приходится перетестировать.
Условный оператор, ветвящийся по фазе жизненного цикла, тянет тебя к Состоянию. Двоюродный брат Стратегии: когда условный оператор не выбирает алгоритм, а проверяет «в каком режиме я сейчас?» — и переходы между режимами разбросаны как присваивания this.status = "..." — тяга направлена к паттерну Состояние. Признак — поле статуса, по которому ветвятся сразу несколько методов, с одной и той же повторяющейся лестницей if (status === ...).
// SMELL: every method re-derives behaviour from a status string
class Order {
status = "draft";
submit() { if (this.status !== "draft") throw Error("can't submit"); this.status = "submitted"; }
pay() { if (this.status !== "submitted") throw Error("can't pay"); this.status = "paid"; }
cancel() { if (this.status === "paid") throw Error("can't cancel"); this.status = "cancelled"; }
}Состояние извлекает каждую фазу в собственный объект, который владеет своими легальными переходами, так что нелегальный переход невозможен по построению, а не охраняется цепочкой строковых проверок. Но обрати внимание на цену: Состояние тяжелее Стратегии. Для трёх состояний ты можешь оставить явные проверки; паттерн заслуживает себя только тогда, когда число состояний и переходов делает разбросанную лестницу по-настоящему трудной для понимания. Запах — продублированный if (status === ...) по многим методам — это ворота.
Пусть решает запах, а не каталог. Коллега открывает PR, добавляющий второй канал уведомлений. Текущий код — это одна функция:
// BEFORE — works, one channel, zero smell yet
function notify(user: User, message: string) {
sendEmail(user.email, message);
}Наивный «паттерн-первый» инстинкт — ввести интерфейс NotificationStrategy, NotifierFactory и реестр — ради одного канала. Это паттерн-ит: косвенность без дублирования, которое бы её оправдывало. YAGNI накладывает вето.
Теперь приходит второй канал, и появляется условный оператор:
function notify(user: User, message: string, channel: string) {
if (channel === "email") sendEmail(user.email, message);
else if (channel === "sms") sendSms(user.phone, message);
}Две ветви всё ещё на грани — но приземляется третья (push), ось теперь явно «канал», и условный оператор — это запах «операторов switch». Теперь ты рефакторишь к Стратегии, потому что запах её заслужил:
type Notifier = (user: User, message: string) => void;
const notifiers: Record<string, Notifier> = {
email: (u, m) => sendEmail(u.email, m),
sms: (u, m) => sendSms(u.phone, m),
push: (u, m) => sendPush(u.deviceId, m),
};
function notify(user: User, message: string, channel: string) {
const send = notifiers[channel];
if (!send) throw new Error(`unknown channel: ${channel}`);
send(user, message);
}Тот же пункт назначения, которого «паттерн-первый» разработчик достиг бы в первый день, — но достигнутый на третий день, после того как дублирование доказало, что абстракция нужна. Два лишних дня купили тебе уверенность в правильной оси для абстрагирования: окажись настоящая вариативность в шаблонизации под пользователя, а не в транспорте под канал, — Стратегия первого дня абстрагировала бы не ту ось, и ты бы её разматывал. Рефакторинг к паттерну позволил коду сказать тебе, какая ось реальна, прежде чем ты на неё ставишь.
▸Почему это работает
Почему ждать запаха, а не закладывать паттерн в дизайн заранее? Потому что паттерн — это ставка на то, какое изменение придёт, а ставки, сделанные до появления свидетельств, обычно ошибочны. Каждый паттерн добавляет косвенность — больше файлов, больше имён, больше прыжков при трассировке во время следующего чтения. Эта цена оправдана только тогда, когда покупает гибкость вдоль оси, которая реально варьируется. Рефакторинг к паттерну означает, что ты уже видел вариативность (второй и третий случай), поэтому абстрагируешь ось, вдоль которой код доказуемо меняется, а не гипотетическую. Вся суть Кериевски в том, что этот порядок — сначала запах, потом паттерн — даёт тебе правильную абстракцию гораздо надёжнее, чем дизайн заранее, и при этом сохраняет простые случаи простыми.
▸Частая ошибка
Характерный режим отказа — паттерн-ит: обёртывание однострочной потребности в Стратегию + Фабрику + Строитель, когда хватило бы обычной функции. Обычно он следует за изучением каталога GoF — каждая проблема вдруг выглядит так, будто ей нужен паттерн, и «гибкий» становится синонимом «хороший». Это не так. Factory, создающая ровно один тип, интерфейс Strategy с единственной реализацией, Builder для объекта с двумя полями — всё это косвенность без устранённого дублирования и поглощённой вариативности. Запах, который оправдывал бы паттерн, отсутствует, поэтому паттерн — чистая цена. Senior-признак: если ты не можешь указать на конкретное дублирование или растущий условный оператор, который паттерн растворяет, у тебя нет паттерна — у тебя карго-культовая церемония, и обычная функция — лучший дизайн.
Разработчик, добавляющий первый формат экспорта, вводит интерфейс ExportStrategy, ExporterFactory и реестр — ради единственного экспортёра в CSV. По дисциплине «рефакторинг к паттернам», какая оценка верна?
Паттерны проектирования — это цели, к которым ты рефакторишь, а не шаблоны, навязываемые заранее (Кериевски). Запах указывает направление: дублированное создание тянет к Фабрике, условный оператор, растущий вдоль одной алгоритмической оси, тянет к Стратегии, а лестница по полю статуса, повторяемая в нескольких методах, тянет к Состоянию. Дисциплина — рефакторить к паттернам, а не преждевременно к ним — запах оправдывает паттерн, а YAGNI накладывает вето на любой паттерн, который не заслужил ни один запах, не давая тебе впихнуть все 23 паттерна GoF в простое приложение. Режим отказа — паттерн-ит: обёртывание однострочной потребности в Стратегию + Фабрику + Строитель, когда хватило бы обычной функции. Тест, как всегда в этом треке: можешь ли ты назвать конкретный запах, который этот паттерн растворяет? Если нет — обычная функция и есть лучший код.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.