Программирование — это решение задач. Часть задач в повседневной работе повторяется от проекта к проекту. У таких задач, как правило, уже есть решения — такие решения называются паттернами или шаблонами проектирования.
Структурные паттерны помогают решать задачи с тем, как совмещать и сочетать сущности вместе. Они заботятся о том, как сущности могут использовать друг друга. Простыми словами — они отвечают на вопрос «Как составить программный компонент так, чтобы его можно было компоновать с другими?».
Среди структурных паттернов можем выделить:
- Адаптер.
- Фасад.
- Декоратор.
- Прокси.
Адаптер
Секция статьи "Адаптер"Адаптер (англ. adapter) помогает сделать не совместимое с нашим модулем API совместимым и использовать его.
Пример
Секция статьи "Пример"Когда мы пишем фронтенд-приложения, нам часто нужно получить данные от сервера или отправить данные на сервер.
Иногда формат данных на сервере и клиенте не совпадает. В таких случаях мы будем использовать адаптер, чтобы сделать несовместимые форматы данных совместимыми.
Допустим, мы получаем от сервера данные в виде объекта:
function fakeAPI() { return { entries: [ { user_name: "Alex", email_address: "some@site.com", ID: "some-unique-id", }, { user_name: "Alice", email_address: "some@other-site.com", ID: "another-unique-id", }, ], };}
function fakeAPI() { return { entries: [ { user_name: "Alex", email_address: "some@site.com", ID: "some-unique-id", }, { user_name: "Alice", email_address: "some@other-site.com", ID: "another-unique-id", }, ], }; }
А хотим — преобразовать их в массив и чтобы поля всегда были набраны в camelCase:
const wantedResponse = [{ userName: "Alex", email: "some@site.com", id: 'some-unique-id'}, { userName: "Alice", email: "some@other-site.com", id: "another-unique-id"}],
const wantedResponse = [{ userName: "Alex", email: "some@site.com", id: 'some-unique-id' }, { userName: "Alice", email: "some@other-site.com", id: "another-unique-id" }],
Тогда мы напишем адаптер, который будет заниматься преобразованиями данных после получения ответа от API:
function responseToWantedAdapter(response) { return response.entries.map((entry) => ({ userName: entry.user_name, email: entry.email_address, id: entry.ID, }));}
function responseToWantedAdapter(response) { return response.entries.map((entry) => ({ userName: entry.user_name, email: entry.email_address, id: entry.ID, })); }
И будем использовать его при получении данных:
const response = fakeAPI();const compatibleResponse = responseToWantedAdapter(response);// ...
const response = fakeAPI(); const compatibleResponse = responseToWantedAdapter(response); // ...
Когда использовать
Секция статьи "Когда использовать"Используйте адаптер, если работаете с сервисами или модулями, API которых не совместимо с требованиями вашего приложения. Это позволит снизить сцепление кода.
Фасад
Секция статьи "Фасад"Фасад (англ. facade) прячет за собой сложную логику других модулей, предоставляя более простые методы или функции.
Он немного похож на адаптер, потому что тоже может делать несовместимое API совместимым, но его основная цель всё же — инкапсулировать часть связанной логики и дать к ней доступ через один метод.
Пример
Секция статьи "Пример"Допустим, мы пишем мобильное приложение — пульт для кофеварки.
Мы хотим добавить кнопку «Нагреть воду» или «Помолоть зерно», но кофеварка предлагает нам более атомарное API: она может по отдельности включить машину, узнать, сколько воды набрано, включить набор воды, отключить набор воды и т. д.
class CoffeeMachine { turnOn() {} getWaterLevel() {} getWater() {} turnOnHeater() {} turnOffHeater() {} getTemperature() {} // ...}
class CoffeeMachine { turnOn() {} getWaterLevel() {} getWater() {} turnOnHeater() {} turnOffHeater() {} getTemperature() {} // ... }
Тогда для нагрева воды мы можем написать фасад:
const machine = new CoffeeMachine();function heatWater() { machine.turnOn(); while (machine.getWaterLevel() <= 1000) { machine.getWater(); } machine.turnOnHeater(); if (machine.getTemperature() >= 90) { machine.turnOffHeater(); }}heatWater();
const machine = new CoffeeMachine(); function heatWater() { machine.turnOn(); while (machine.getWaterLevel() <= 1000) { machine.getWater(); } machine.turnOnHeater(); if (machine.getTemperature() >= 90) { machine.turnOffHeater(); } } heatWater();
Когда использовать
Секция статьи "Когда использовать"Используйте фасад, когда вам нужно объединить несколько методов стороннего сервиса или модуля в одну цепочку действий, которая будет повторяться в других местах программы.
Декоратор
Секция статьи "Декоратор"Декоратор (англ. decorator) позволяет динамически менять поведение объекта в рантайме.
Пример
Секция статьи "Пример"Допустим, нам надо логировать каждый вызов функции update
:
const user = { name: "Alex", email: "example@site.com",};function update(name, email) { user.name = name; user.email = email;}
const user = { name: "Alex", email: "example@site.com", }; function update(name, email) { user.name = name; user.email = email; }
Мы можем добавить логирование прямо в саму функцию:
function update(name, email) { console.log(`Logging... ${name}, ${email}`); user.name = name; user.email = email;}
function update(name, email) { console.log(`Logging... ${name}, ${email}`); user.name = name; user.email = email; }
Но это не лучшее решение, потому что если мы захотим добавить логирование куда-то ещё, нам придётся дублировать ту же функциональность. Лучше использовать декоратор, который будет «оборачивать» функцию и «обогащать» её поведение логированием:
function loggingDecorator(fn) { return function wrapped(...args) { console.log(`Logging... ${args.join(",")}`); return fn(...args); };}
function loggingDecorator(fn) { return function wrapped(...args) { console.log(`Logging... ${args.join(",")}`); return fn(...args); }; }
Мы создаём функцию высшего порядка — то есть функцию, которая принимает другую функцию как аргумент и возвращает функцию как результат.
Аргумент fn
— это функция, которую мы хотим «обогатить» дополнительной функциональностью. Сама эта дополнительная функциональность находится внутри возвращаемой функции wrapped
.
Во wrapped
мы сперва логируем переданные аргументы, потом вызываем оригинальную функцию fn
и возвращаем её результат.
Использовать теперь мы это можем так:
const updateWithLogging = loggingDecorator(update);updateWithLogging("Alice", "test@test.com");// Logging... Alice, test@test.comconsole.log(user);// {name: 'Alice', email: 'test@test.com'}
const updateWithLogging = loggingDecorator(update); updateWithLogging("Alice", "test@test.com"); // Logging... Alice, test@test.com console.log(user); // {name: 'Alice', email: 'test@test.com'}
Когда использовать
Секция статьи "Когда использовать"Используйте декораторы для выделения повторяющейся и расширяющей поведение объектов логики. Особенно это полезно для выделения кода, который можно использовать в разных модулях и задачах.
Прокси
Секция статьи "Прокси"Прокси (англ. proxy) — это промежуточный модуль, предоставляет интерфейс к какому-либо другому модулю.
Он похож на декоратор, но в отличие от него не меняет поведение оригинального объекта в рантайме. Вместо этого он «вмешивается» в общение с оригинальным объектом.
Пример
Секция статьи "Пример"В JavaScript есть встроенный механизм работы с прокси — Proxy
. Мы можем подменить свойство или метод объекта «на лету»:
const original = { name: "Alice", email: "hi@site.com",};const proxied = new Proxy(original, { get: function (target, prop, receiver) { if (prop === "name") return "ALICE"; return "YOU HAVE BEEN PWND!"; },});
const original = { name: "Alice", email: "hi@site.com", }; const proxied = new Proxy(original, { get: function (target, prop, receiver) { if (prop === "name") return "ALICE"; return "YOU HAVE BEEN PWND!"; }, });
Теперь при обращении к проксированному объекту будет запускаться функция-геттер, которая проверит, к какому свойству мы обратились, и решит что именно вернуть:
console.log(proxied.name); // ALICEconsole.log(proxied.email); // YOU HAVE BEEN PWND!
console.log(proxied.name); // ALICE console.log(proxied.email); // YOU HAVE BEEN PWND!
Оригинальный объект остаётся при этом нетронутым:
console.log(original.name); // Aliceconsole.log(original.email); // hi@site.com
console.log(original.name); // Alice console.log(original.email); // hi@site.com
Когда использовать
Секция статьи "Когда использовать"Используйте прокси, когда вам необходимо заменить полностью или поменять API другого модуля, не трогая оригинальный объект.
Такое бывает полезно при обработке пользовательских событий в сложных сценариях или при работе с DOM, когда перед вставкой нового DOM-узла нужно проверить какие-то условия.
Другие паттерны
Секция статьи "Другие паттерны"Мы рассмотрели самые частые из структурных паттернов проектирования. Их немного больше, но остальные используются реже.
Кроме структурных также существуют и другие виды паттернов проектирования:
- Порождающие — помогают решать задачи с созданием сущностей или групп похожих сущностей, убирают лишнее дублирование, делают процесс создания объектов короче и прямолинейнее.
- Поведенческие — распределяют ответственности между модулями и определяют, как именно будет происходить общение.