Паттерны GoF и что от них осталось в JS
Книга банды четырёх вышла в 1994 году для C++ и Smalltalk. С тех пор паттерны успели стать религией, потом мемом, а правда посередине: часть из 23 паттернов в JS жива и полезна, часть растворилась в самом языке, а часть переехала в стандартную библиотеку. Питер Норвиг ещё в 1996 показал, что 16 из 23 паттернов в динамическом языке либо упрощаются, либо становятся невидимыми. Этот урок строится вокруг его тезиса: разберём, какую дырку в языке закрывал каждый паттерн, и проверим, есть ли эта дырка в JS. База про SOLID и ответственность из прошлого урока понадобится постоянно.
Зачем вообще каталог
Паттерн это имя для повторяющегося решения, и главная его ценность словарная: «тут Strategy» на код-ревью экономит абзац объяснений. GoF делит каталог на три категории: порождающие (как создавать объекты), структурные (как объекты составлять) и поведенческие (как объектам взаимодействовать).
Чего паттерн не даёт, так это инструкции к применению. Джошуа Кериевский сформулировал правильное направление: к паттерну приходят рефакторингом, когда код сам показал повторяющуюся боль, а не закладывают заранее «на вырост». Заложенный заранее паттерн это ставка на будущее, которая обычно не сбывается, а абстракция остаётся.
И ещё одно следствие тезиса Норвига, ради которого этот урок вообще существует: паттерн это название дырки в языке. Меняется язык, меняется список паттернов. Поэтому учить каталог 1994 года дословно бессмысленно, а понять, какая дырка чем закрылась, полезно: это прививка от переноса Java-бойлерплейта в JS.
Живые: паттерны, которые стоит знать как есть
Strategy
Выбор алгоритма в рантайме. В GoF это интерфейс плюс класс на каждую стратегию; в языке с first-class functions весь бойлерплейт схлопывается в объект функций:
type SortOrder = 'newest' | 'cheapest' | 'popular';
const strategies: Record<SortOrder, (a: Product, b: Product) => number> = {
newest: (a, b) => b.createdAt - a.createdAt,
cheapest: (a, b) => a.price - b.price,
popular: (a, b) => b.rating - a.rating,
};
const sortProducts = (products: Product[], order: SortOrder) =>
[...products].sort(strategies[order]);
Три класса, интерфейс и фабрика из классической версии превратились в один Record. Это канонический пример схлопывания, и ровно его ты сделаешь руками в домашке.
Observer
Подписки и события: один объект сообщает многим, что что-то произошло,
не зная их по именам. Паттерн жив, но писать его руками почти не
нужно, он уже в платформе: EventTarget в браузере и Node,
EventEmitter в Node, AbortSignal как наблюдатель отмены.
const target = new EventTarget();
target.addEventListener('order:paid', (event) => sendReceipt(event));
target.dispatchEvent(new CustomEvent('order:paid', { detail: order }));
Свой Observer стоит написать один раз в жизни, чтобы понять механику (подписка, отписка, утечки через забытые подписки), и дальше брать платформенный.
Decorator
Обёртка с тем же интерфейсом, добавляющая поведение. В JS это чаще higher-order function, чем класс:
const withRetry =
<Args extends unknown[], R>(fn: (...args: Args) => Promise<R>, attempts = 3) =>
async (...args: Args): Promise<R> => {
let lastError: unknown;
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await fn(...args);
} catch (error) {
lastError = error;
}
}
throw lastError;
};
const fetchUser = withRetry(loadUser);
Та же сигнатура снаружи, новое поведение внутри, обёртки складываются
в цепочку: withLog(withRetry(loadUser)). Не путай с декораторами
TC39 (@logged): это синтаксис из
урока про метапрограммирование,
а здесь паттерн.
Adapter
Прослойка, переводящая чужой интерфейс в твой. Жив без изменений и нужен ежедневно, особенно на границах внешних API: обернуть SDK платёжки в свой узкий интерфейс, чтобы остальной код зависел от твоего типа, а не от чужого. Бонус при структурной типизации TS: адаптеру не нужно ничего декларировать, достаточно совпасть по форме.
Factory
Функция, возвращающая объект. В JS паттерн выродился в идиому: любая
функция уже фабрика, makeAccount из прошлого урока тому пример.
Отдельный класс AbstractFactory нужен в языке, где функцию нельзя
передать как значение; у нас можно.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.