Раздел 2 · Программирование / CS

Паттерны GoF и что от них осталось в JS

middle~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Паттерны 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.