Экспресс-курс · No. 04
Большинство из них ты уже не раз изобретал руками. Паттерн — это всего лишь имя: чтобы команда могла одним словом сказать то, на что ушёл бы абзац, и узнать приём раньше, чем сделает его криво.
Только суть · Один образ на паттерн · Примеры важнее UML
Паттерн проектирования — это названное, переиспользуемое решение проблемы, которая всплывает в коде снова и снова. Это словарь, а не библиотека — его не устанавливают, его узнают.
Паттерн — это имя для приёма, который ты и так делаешь
Футболисты не изобретают «стенку» по ходу матча. У приёма есть имя, и назвать его быстрее, чем описывать весь манёвр.
Большинство паттернов — это то, что толковые разработчики и так изобретают руками. Имя превращает абзац объяснений в одно слово, общее для всей команды. Когда кто-то говорит «оберни это в Decorator», все мгновенно видят форму — и в этом весь смысл паттерна: общий словарь для приёмов, которые ты сделал бы в любом случае.
Паттерны находят, а не изобретают
Никто не изобретал арку. Строители раз за разом обнаруживали, что это самый прочный способ перекрыть пролёт, — и ей дали имя.
«Банда четырёх» не придумала 23 хитрых трюка; они каталогизировали решения, которые снова и снова всплывали в хорошем коде. Поэтому паттерн — не цель, в которую надо попасть, а форма, к которой ты приходишь сам, когда задача того требует. Если ты втискиваешь паттерн силой — скорее всего, у тебя ещё нет его проблемы.
Три семьи, три вопроса
На кухне есть инструменты для трёх задач: приготовить еду, выложить на тарелку и подать. Паттерны делятся так же.
Порождающие паттерны — про то, как объекты рождаются: кто их строит и как. Структурные — про то, как объекты складываются в большие формы. Поведенческие — про то, как объекты общаются и делят ответственность. Почти любой классический паттерн — это один из этих трёх ответов.
Паттерн, который тебе не нужен, — это просто сложность
Швейцарский нож в кармане — удобно. Привинтить все двадцать инструментов к входной двери — нет.
Любой паттерн добавляет лишнюю прослойку — слой между тобой и вещью. Это оправдано, когда покупает гибкость, которой ты реально воспользуешься, и чистая трата — когда нет. Главная ошибка не в незнании паттернов, а в том, чтобы тянуться за паттерном раньше, чем появилась проблема.
Паттерн — это сокращение для разговора. Используй его, чтобы тебя поняли, а не чтобы выглядеть умно.
Как объекты появляются на свет — без россыпи new повсюду и без приваривания кода к конкретным классам.
Factory: проси вещь по имени, а не по рецепту
Кофемашина: ты жмёшь «латте» и получаешь его. Ты не мелешь, не взбиваешь и не наливаешь сам — и не обязан знать, как это устроено.
Фабрика выдаёт тебе объект, минуя прямой вызов конструктора, так что вызывающий код никогда не зависит от конкретного класса. Пример: createPaymentProvider("stripe") возвращает что-то, удовлетворяющее интерфейсу PaymentProvider — перейти на PayPal можно, поменяв фабрику, а не каждое место вызова. Это покупает свободу менять то, что строится, не трогая того, кто попросил.
Abstract Factory: целое семейство в едином стиле сразу
Покупаешь мебельный гарнитур — «викторианский» или «модерн» — где каждый предмет приходит в одном стиле, без разношёрстных стульев.
Когда объекты должны идти согласованными семьями, абстрактная фабрика строит весь набор так, чтобы они подходили друг к другу. Пример: UI-тулкит, который выдаёт согласованные кнопки, меню и диалоги под macOS и Windows из одной фабрики. Мощно — но это тяжёлая машинерия, оправданная, только когда семьи реальны.
Builder: собирай сложный объект шаг за шагом
Заказываешь бургер под себя — булка, котлета, без лука, двойной сыр — по одному выбору за раз, потом «готово».
Когда у объекта много необязательных частей, builder позволяет задавать их по кусочку, а не через монструозный конструктор на двенадцать аргументов. Пример: QueryBuilder().select("name").where("age > 18").limit(10).build(). Он меняет один сбивающий с толку конструктор на читаемую цепочку.
Singleton: ровно один, общий для всех
У страны один президент. Все, кто говорит «президент», имеют в виду одного и того же человека — их не двое.
Singleton гарантирует единственный экземпляр с одной глобальной точкой доступа. Пример: конфигурация или логгер, из которых читают все. Но обращайся осторожно: singleton — это глобальное состояние в красивом пальто, а глобальное состояние мешает тестировать и рассуждать. Часто единственный экземпляр, который ты передаёшь явно, лучше того, до которого дотягиваются через глобал.
Prototype: копируй заполненное, а не строй с нуля
Сделать копию заполненного бланка быстрее, чем заполнять пустой с нуля.
Когда создавать объект с нуля дорого или муторно, клонируй существующий и подправь. Пример: дублировать настроенного игрового противника или стилизованный узел документа вместо того, чтобы каждый раз пересобирать его настройку. Удобно, когда объекты дороги в постройке и в основном похожи.
Спрячь
new. Чем меньше мест знает, как строится объект, тем свободнее ты его меняешь.
Как объекты складываются в большие структуры — стыкуя несовместимые части и добавляя возможности без переписывания.
Adapter: переходник между двумя интерфейсами
Зарядка ноутбука за границей — устройство в порядке, розетка в порядке; нужен только маленький переходник между ними.
Adapter оборачивает один интерфейс так, чтобы он выглядел как другой, позволяя двум вещам, не созданным для совместной работы, сотрудничать. Пример: обернуть сторонний платёжный SDK так, чтобы он удовлетворял твоему интерфейсу PaymentProvider — твой код никогда не видит форму вендора. Так ты не даёшь чужому API протечь во весь твой код.
Decorator: добавляй начинку, не меняя пиццу
Простая пицца, потом сыр, потом грибы — каждый слой оборачивает предыдущий и что-то добавляет, и это всё ещё пицца.
Decorator оборачивает объект, добавляя поведение без правки оригинала и без наследования. Пример: обернуть поток данных, добавив сжатие, затем шифрование — каждый слой можно ставить и снимать. Так ты добавляешь возможности в сочетаниях, которые никто не обязан был предусмотреть.
Facade: одна простая стойка перед бардаком за кулисами
Консьерж в отеле — ты говоришь «нужно такси и столик на ужин», а он разруливает клубок звонков за стойкой.
Facade даёт простую, единую точку входа в сложную подсистему, пряча её движущиеся части. Пример: VideoConverter.convert(file, "mp4"), который втихую дирижирует кодеками, буферами и аудиопотоками под капотом. Он сжимает большую поверхность до одной двери, которая нужна большинству вызывающих.
Proxy: подставное лицо, контролирующее доступ
Ассистент знаменитости — ты не дотягиваешься до звезды напрямую; ассистент фильтрует, планирует, а иногда отвечает за неё.
Proxy встаёт перед объектом, контролируя доступ к нему — для ленивой загрузки, прав, кэширования или общения с чем-то удалённым. Пример: прокси картинки, который не грузит реальный файл, пока его не показывают; или прокси доступа, проверяющий права перед тем, как пробросить вызов. Тот же интерфейс, лишние ворота перед ним.
Composite: дерево и лист — на один манер
Папка хранит файлы и другие папки. «Посчитать размер» работает одинаково, спрашиваешь ли ты один файл или всё дерево.
Composite позволяет обращаться с одиночными объектами и группами объектов единообразно, так что ветка и лист делят один интерфейс. Пример: интерфейс, где и кнопка, и панель-полная-кнопок отвечают на render() и getSize(). Так рекурсивные структуры превращаются в то, с чем работаешь без особых случаев на каждом уровне.
Композиция вместо наследования: защёлкивай поведение в рантайме, а не замораживай его в дереве классов.
Как объекты делят работу и говорят друг с другом — чтобы ответственность была разрезана чисто и нужный код запускался в нужный момент.
Strategy: подмени алгоритм, оставь вызывающего
Приложение карт: та же поездка, но ты выбираешь «быстрее», «короче» или «без платных дорог», и маршрут строится иначе. Ты выбираешь стратегию; приложение просто следует ей.
Strategy делает алгоритм взаимозаменяемым в рантайме, так что вызывающий остаётся прежним, а способ меняется. Пример: оформление заказа, принимающее PricingStrategy — обычная, для участника или праздничная распродажа — вместо разрастающейся лестницы if. В современных языках это часто просто передача функции, а не целого класса — и в этом суть: паттерн — это идея, а не церемония.
Observer: подпишись — и узнаешь, когда изменится
Рассылка — ты подписываешься один раз, и каждый новый выпуск приходит к тебе. Издатель не звонит каждому читателю; он просто шлёт по списку.
Observer позволяет объектам подписаться на другой объект и получать уведомление, когда тот меняется, при этом он не знает, кто они. Пример: ячейка таблицы, обновляющая все зависящие от неё графики; UI-фреймворки, реагирующие на состояние; pub/sub повсюду. Это становой хребет событийного и реактивного кода.
Command: оберни запрос в объект
Заказной чек в ресторане — запрос «стол 5, два кофе» становится бумажкой, которую можно поставить в очередь, переставить, залогировать и даже порвать.
Command превращает действие в самостоятельный объект, так что его можно поставить в очередь, залогировать, отложить или отменить. Пример: каждая правка в редакторе — это команда, и именно это делает возможными undo/redo и макросы; очереди задач — это команды, ждущие своей очереди. Так действие становится тем, что можно сохранить и проиграть заново.
Chain of Responsibility: передавай дальше, пока кто-то не разберётся
Уровни поддержки — первая линия пробует, эскалирует специалисту, затем менеджеру, пока кто-то реально не решит вопрос.
Запрос идёт по цепочке обработчиков; каждый либо разбирается с ним, либо передаёт дальше. Пример: веб-middleware — аутентификация, потом логирование, потом rate-limit, потом сам маршрут — каждое звено делает свою часть или пробрасывает. Так отправитель отвязан от того, кто в итоге сделает работу.
Хорошие поведенческие паттерны дают двум кускам сотрудничать, не вшивая один в другой намертво.
Как поведение объекта меняется с его состоянием, как ты идёшь по коллекциям и как чинишь скелет, оставляя один шаг открытым.
State: поведение, которое меняется с режимом
Светофор — зелёный, жёлтый, красный — где каждый цвет решает, что происходит сейчас и какой цвет будет следующим. Светофор тот же, поведение разное в каждом состоянии.
Паттерн State позволяет объекту менять поведение по мере смены внутреннего состояния, заменяя клубок if и switch на ясные состояния и переходы. Пример: заказ, который ведёт себя по-разному как pending, paid, shipped или cancelled, и каждое состояние знает только свои правила и то, чем оно может стать. Так каша из флагов превращается в аккуратный маленький автомат.
Iterator: иди по коллекции, не заглядывая внутрь
Кнопки «дальше» и «назад» на пульте — ты переключаешь каналы, не зная, как они хранятся.
Iterator даёт единый способ пройти коллекцию, не раскрывая, как она устроена — массив, дерево или связанный список. Пример: for (item of collection) работает одинаково над списком и над своей структурой. Этот настолько полезен, что язык обычно встраивает его внутрь — for..of, генераторы, __iter__ — что яснее всего показывает, как паттерн дорастает до фичи.
Template Method: фиксированный рецепт с одним пропуском
Рецепт, одинаковый каждый раз, — кроме «добавь специю на свой вкус» на четвёртом шаге. Скелет зафиксирован; один шаг — твой.
Template Method задаёт скелет операции и позволяет переопределять отдельные шаги, не меняя общей формы. Пример: базовый импортёр данных, который открывает, читает, разбирает (это заполняешь ты) и сохраняет, — а импортёры CSV и JSON подставляют только шаг разбора. Полезно, когда много потоков делят форму, но различаются в одном-двух местах.
И остальное — на одном дыхании
В ящике с инструментами есть те, за которыми тянешься раз в месяц, а не каждый день, — но знать об их существовании всё равно стоит.
Ещё горстка иногда оправдывает своё место: Mediator (диспетчерская, чтобы объекты говорили через один хаб, а не клубок прямых связей), Visitor (добавить новые операции к структуре, не правя её классы), Memento (захватить состояние объекта, чтобы восстановить его позже — точки сохранения, снимки для отмены). Знай имена; тянись за ними, только когда появилась именно эта проблема.
Когда паттерн становится ключевым словом языка — перестань писать паттерн и используй ключевое слово.
Каталог — это десятилетия добытой потом мудрости, и одновременно продукт своего времени. Пользуйся им как сеньор, а не как чек-листом.
Многие паттерны теперь просто фичи языка
Раньше дорогу объясняли по ориентирам; теперь телефон сам ведёт. Навык не исчез — он впитался в инструмент.
Iterator — это for..of. Strategy — часто переданная функция. Decorator и Observer встроены в современные фреймворки. Когда язык даёт тебе форму бесплатно, писать полный паттерн руками — просто церемония. Идея по-прежнему важна; шаблонный код — обычно нет.
Паттерн-ит — реальная болезнь
Повар, вываливающий в одно блюдо всю полку специй, получает не более богатый вкус, а несъедобное месиво.
Классический запах переусложнения — тянуться за паттернами, чтобы выглядеть изощрённо: абстрактная фабрика на один продукт, Strategy на один алгоритм, который никогда не меняется, пять слоёв обёрток вокруг одного if. И сейчас хуже — AI-ассистент с радостью сгенерит «многоуровневую Abstract Factory» там, где должно быть три строки. Более простой код почти всегда — более сеньорский выбор.
Паттерны — это словарь, а не закон
Знание множества слов не делает хорошего писателя. Хорошего делает знание, какое слово — и когда промолчать.
Настоящая ценность каталога — общий язык: «давай поставим тут Facade» мгновенно доходит до любого, кто знает термин, и на команде это стоит немало. Но цель всегда — самый простой код, решающий задачу. Паттерн — это средство; назвать его — никогда не достижение.
Выучи все паттерны, чтобы их узнавать. Используй так мало, как только можешь себе позволить.
Паттерны — это ответы. Мастерство — подобрать их к реальному вопросу и понять, когда правильный ответ — никакого паттерна.
Пусть паттерн вырастает из боли
Ты не берёшь гаечный ключ, пока не нашёл болт. Сначала видишь проблему — потом тянешься за инструментом, который к ней подходит.
Хорошее применение паттерна — это обычно рефакторинг, а не стартовый чертёж: ты пишешь простое, чувствуешь конкретную боль — эта лестница if всё растёт, этот класс слишком много знает о том, как он построен, — и тогда паттерн, который её снимает, очевиден. Втискивать паттерны заранее — это как строить соборы, которых никто не просил.
Называй, только когда имя помогает
Ты не говоришь «я выполнил стенку» в обычной беседе. Но на поле, с партнёрами, это слово экономит целое предложение.
Выигрыш от паттерна — это общение и гибкость. Если имя делает код яснее для следующего человека, произнеси его вслух — в имени класса, в комментарии, в обсуждении дизайна. Если обёртка не покупает ничего — побеждает самый простой код, с паттерном или без.
- Какую повторяющуюся проблему он решает? Скажи одним предложением. - Есть ли у меня эта проблема сейчас, или я гадаю про будущее? - Справилась бы обычная функция или объект? Сначала попробуй так. - Не даёт ли мне язык это уже бесплатно? - Сделает ли имя код яснее для следующего человека — или просто наряднее? - Я рефакторю к нему или втискиваю заранее?
- У тебя фабрика, которая строит ровно одну вещь. - Strategy с единственной стратегией, которая никогда не меняется. - Интерфейсов и обёрток больше, чем реального поведения. - Ты добавил паттерн, чтобы выглядеть сеньором, а не чтобы снять боль. - Новому читателю нужна UML-диаграмма, чтобы проследить три строки логики.
- Паттерн убрал растущую лестницу
if/switch. - Ты можешь подменить реализацию, не трогая вызывающих. - Коллега узнал форму по имени сразу. - Обёртка окупается — ты реально пользуешься гибкостью. - С паттерном код проще, чем без него, а не просто хитрее.
Лучшее применение паттерна — то, которого читатель даже не замечает, потому что код стал казаться очевидным.