Паттерны, которые оправдывают себя

Что это такое

Четыре паттерна покрывают большую часть пользы в повседневном коде: Стратегия подменяет алгоритм, Адаптер вписывает несовместимый интерфейс, Наблюдатель уведомляет заинтересованных, не зная, кто они, а Фабрика решает, какую реализацию создавать. Знать их стоит потому, что решаемые ими задачи возникают постоянно, и потому что их имена позволяют команде быстро договориться о решении.

Почему это важно

Эти четыре встречаются в любой большой кодовой базе, часто безымянными, и по форме легко понять, чего код пытается достичь и где он, вероятно, заболит. Написанное руками условие, которое выбирает между платёжными провайдерами, — Стратегия, не получившая имени; обёртка, перекраивающая ответ сторонней библиотеки, — Адаптер. Дать им имена — значит превратить разговор о переделке в конкретное изменение. При этом чаще всего эти же паттерны применяют преждевременно, поэтому именно знание их цены отличает использование от наряжания прямого вызова.

Ключевые идеи

Стратегия: варьируйте алгоритм за одним интерфейсом

Применяйте, когда вызывающему нужно выбрать между несколькими способами сделать одну и ту же работу, причём выбор делается извне. Интерфейс стоит в точке вариации, и каждая реализация — один алгоритм. Цена — уровень косвенности и ещё один тип. Если применить её преждевременно, когда реализация одна и планов на другую нет, получится вызов функции с лишними шагами.

interface PricingStrategy { price(items): number }
class RetailPricing implements PricingStrategy { /* ... */ }
class WholesalePricing implements PricingStrategy { /* ... */ }

Адаптер: заставьте несовместимый интерфейс подойти

Применяйте, когда приходится использовать нечто, чью форму вы не контролируете, — клиент сторонней библиотеки, легаси-модуль, — и не хотите, чтобы эта форма протекла в ваш код. Адаптер переводит в обе стороны: ценность в том, что остальная система зависит от вашего интерфейса, поэтому замена лежащей в основе библиотеки позже становится изменением одного файла.

class StripeGateway implements PaymentGateway {
  charge(order) { return this.stripe.charges.create(toStripePayload(order)); }
}

Наблюдатель: уведомляйте, не зная слушателей

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

emitter.on("order-placed", (order) => mailer.sendReceipt(order));

Фабрика: решайте, какую реализацию создавать

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

function createNotifier(config): Notifier {
  return config.email ? new EmailNotifier(config) : new SlackNotifier(config);
}

Назовите альтернативу, прежде чем применять паттерн

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

Практика

Замените условие Стратегией

Найдите условие, которое выбирает между двумя-тремя способами сделать одну работу. Выделите каждую ветвь в отдельную реализацию за интерфейсом, а выбор между ними перенесите в место вызова.

Готово, когда: добавление четвёртого случая означает один класс и одну строку в месте вызова, без правок внутри существующих ветвей.

Урежьте перестроенный Адаптер

Найдите класс-обёртку, у которого методов больше, чем использует любой вызывающий. Перепишите его так, чтобы он открывал только нужные вызывающим операции, а остальное делегировал.

Готово, когда: у каждого метода обёртки есть хотя бы один вызывающий, а лежащая в основе библиотека упоминается ровно в одном файле.

Частые ошибки

  • Стратегия с одной реализацией — косвенность решает вариацию, которой нет; исправить: используйте прямой вызов, пока не появится второй случай.
  • Адаптер, протекающий обёрнутым типом — форма сторонней библиотеки утекает в ваш код через параметры или возвращаемые значения; исправить: переводите на границе и никогда не открывайте чужой тип.
  • Наблюдатель без отписки — слушатели накапливаются и удерживают мёртвые объекты живыми; исправить: храните ручку отписки и вызывайте её там, где кончается время жизни подписчика.
  • Фабрика как выбор по строке типа — каждый новый случай правит фабрику, а строку не проверяет компилятор; исправить: предпочтите явное значение или реестр, в который модули добавляются сами.
  • Применение паттерна без называния простой альтернативы — читатель не может понять, взвешивали ли сложность; исправить: скажите в коммите или комментарии, чего стоила бы прямая версия.

Что читать дальше

  • Зачем паттернам имена (01-cs-fundamentals/18-design-patterns/01-why-patterns-are-named) — рамка цены и вариации, удерживающая эти четыре от превращения в церемонию, поэтому прочтите первым.
  • Архитектура программного обеспечения (01-cs-fundamentals/25-software-architecture) — что происходит с этими взаимодействиями, когда части развёртываются отдельно.
  • Код-ревью (01-cs-fundamentals/17-code-review) — как поднять «это паттерн с одной реализацией» как полезный комментарий, а не спор о вкусе.