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

Рефакторинг к паттернам

Паттерны — это цели, К которым ты рефакторишь, когда запах их требует, а не шаблоны, навязываемые заранее. Дублированное создание тянет к Фабрике, растущий условный оператор — к Стратегии или Состоянию. YAGNI не даёт впихнуть все 23 паттерна GoF в простое приложение.

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

Ты выучил каталог «Банды четырёх» и теперь видишь все 23 паттерна повсюду. Поэтому хватаешься за них заранее: новая фича получает Стратегию, и Фабрику, и Строителя ещё до того, как появился хоть один дубль. Полгода спустя кодовая база — лабиринт косвенности, где любое изменение в одну строку требует трассировки через четыре файла, а большая часть заложенной гибкости так и не пригодилась.

Есть другой способ прийти к паттернам, предложенный Джошуа Кериевски: паттерны — это цели, К которым ты рефакторишь, когда код их просит, а не чертежи, от которых ты стартуешь. Появляется запах, ты его убираешь, и форма, на которой ты приземляешься, оказывается именованным паттерном. Паттерн — это направление, в котором тебя тянет запах, а YAGNI — тормоз, который не даёт уехать туда до того, как запах появился.

Цель

После этого урока ты можешь объяснить, почему паттерн — это конечное состояние рефакторинга, а не стартовый шаблон; распознать конкретные запахи, которые тянут код к Фабрике, Стратегии или машине Состояний; и применять senior-дисциплину рефакторинга к паттернам вместо преждевременного движения к ним — позволяя запаху оправдывать паттерн и позволяя YAGNI накладывать вето на паттерны, которые не заслужил ни один запах.

1

Паттерн — это пункт назначения, а не стартовая точка. Каталог «Банды четырёх» описывает повторяющиеся решения; он не говорит тебе, когда проблема присутствует. Переосмысление Кериевски — «рефакторинг к паттернам» — трактует каждый паттерн как цель серии маленьких рефакторингов, сохраняющих поведение, применяемых только тогда, когда код демонстрирует запах, который этот паттерн растворяет. Ты не решаешь «здесь нужна Стратегия» на дизайн-митинге; ты замечаешь условный оператор, который продолжает расти вдоль одной оси, и именно это наблюдение делает Стратегию правильным ходом. Паттерн заслуживает своё место, убирая конкретную боль, а не появляясь в учебнике.

Практическое следствие: ты никогда не пишешь interface PaymentStrategy в первый день. Ты пишешь самое простое, что работает, и позволяешь второму и третьему способу оплаты показать тебе дублирование, прежде чем извлекать абстракцию.

2

Дублированная логика создания тянет тебя к Фабрике. Когда одно и то же многошаговое создание объекта появляется в нескольких местах вызова — и особенно когда создаваемый тип зависит от значения времени выполнения — запах состоит в дублированном, ветвящемся создании. Рефакторинг, который его убирает, приземляет тебя на фабрику.

// 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; вызывающие зависят от концепции «сделать отчёт», а не от конкретных классов. Ты не планировал Фабрику — ты убрал дублирование, и Фабрика — это то, что произвело устранение дублирования.

3

Условный оператор, растущий вдоль одной оси, тянет тебя к Стратегии. 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);
}

Добавить способ теперь — это запись в таблице, а не ветвь в растущей функции, и не связанные между собой способы перестают делить общий путь кода, который приходится перетестировать.

4

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.