Структурные паттерны проектирования

Что программисты подразумевают под адаптерами, фасадами и декораторами.

Время чтения: 8 мин

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

Структурные паттерны помогают решать задачи с тем, как совмещать и сочетать сущности вместе. Они заботятся о том, как сущности могут использовать друг друга. Простыми словами — они отвечают на вопрос «Как составить программный компонент так, чтобы его можно было компоновать с другими?».

Среди структурных паттернов можем выделить:

  • Адаптер.
  • Фасад.
  • Декоратор.
  • Прокси.

Адаптер

Скопировано

Адаптер (англ. adapter) помогает сделать не совместимое с нашим модулем API совместимым и использовать его.

Пример

Скопировано

Когда мы пишем фронтенд-приложения, нам часто нужно получить данные от сервера или отправить данные на сервер.

Иногда формат данных на сервере и клиенте не совпадает. В таких случаях мы будем использовать адаптер, чтобы сделать несовместимые форматы данных совместимыми.

Допустим, мы получаем от сервера данные в виде объекта:

        
          
          function fakeAPI() {  return {    entries: [      {        user_name: 'Александр',        email_address: 'some@site.com',        ID: 'уникальный id',      },      {        user_name: 'Мария',        email_address: 'some@other-site.com',        ID: 'другой уникальный id',      },    ],  }}
          function fakeAPI() {
  return {
    entries: [
      {
        user_name: 'Александр',
        email_address: 'some@site.com',
        ID: 'уникальный id',
      },
      {
        user_name: 'Мария',
        email_address: 'some@other-site.com',
        ID: 'другой уникальный id',
      },
    ],
  }
}

        
        
          
        
      

А хотим — преобразовать их в массив и чтобы поля всегда были набраны в camelCase:

        
          
          const wantedResponse = [{  userName: 'Александр',  email: 'some@site.com',  id: 'уникальный id'}, {  userName: 'Мария',  email: 'some@other-site.com',  id: 'другой уникальный id'}]
          const wantedResponse = [{
  userName: 'Александр',
  email: 'some@site.com',
  id: 'уникальный id'
}, {
  userName: 'Мария',
  email: 'some@other-site.com',
  id: 'другой уникальный 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: 'Александр',  email: 'example@site.com',}function update(name, email) {  user.name = name  user.email = email}
          const user = {
  name: 'Александр',
  email: 'example@site.com',
}

function update(name, email) {
  user.name = name
  user.email = email
}

        
        
          
        
      

Мы можем добавить логирование прямо в саму функцию:

        
          
          function update(name, email) {  console.log(`Логирую... ${name}, ${email}`)  user.name = name  user.email = email}
          function update(name, email) {
  console.log(`Логирую... ${name}, ${email}`)
  user.name = name
  user.email = email
}

        
        
          
        
      

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

        
          
          function loggingDecorator(fn) {  return function wrapped(...args) {    console.log(`Логирую... ${args.join(',')}`)    return fn(...args)  }}
          function loggingDecorator(fn) {
  return function wrapped(...args) {
    console.log(`Логирую... ${args.join(',')}`)
    return fn(...args)
  }
}

        
        
          
        
      

Мы создаём функцию высшего порядка — то есть функцию, которая принимает другую функцию как аргумент и возвращает функцию как результат.

Аргумент fn — это функция, которую мы хотим «обогатить» дополнительной функциональностью. Сама эта дополнительная функциональность находится внутри возвращаемой функции wrapped().

Во wrapped мы сперва логируем переданные аргументы, потом вызываем оригинальную функцию fn и возвращаем её результат.

Использовать теперь мы это можем так:

        
          
          const updateWithLogging = loggingDecorator(update)updateWithLogging('Мария', 'test@test.com')// Логирую... Мария, test@test.comconsole.log(user)// {name: 'Мария', email: 'test@test.com'}
          const updateWithLogging = loggingDecorator(update)
updateWithLogging('Мария', 'test@test.com')

// Логирую... Мария, test@test.com

console.log(user)
// {name: 'Мария', email: 'test@test.com'}

        
        
          
        
      

Когда использовать

Скопировано

Используйте декораторы для выделения повторяющейся и расширяющей поведение объектов логики. Особенно это полезно для выделения кода, который можно использовать в разных модулях и задачах.

Прокси

Скопировано

Прокси (англ. proxy) — это промежуточный модуль, предоставляет интерфейс к какому-либо другому модулю.

Он похож на декоратор, но в отличие от него не меняет поведение оригинального объекта в рантайме. Вместо этого он «вмешивается» в общение с оригинальным объектом.

Пример

Скопировано

В JavaScript есть встроенный механизм работы с прокси — Proxy. Мы можем подменить свойство или метод объекта «на лету»:

        
          
          const original = {  name: 'Мария',  email: 'hi@site.com',}const proxied = new Proxy(original, {  get: function (target, prop, receiver) {    if (prop === 'name') return 'МАРИЯ'    return 'YOU HAVE BEEN PWND!'  },})
          const original = {
  name: 'Мария',
  email: 'hi@site.com',
}

const proxied = new Proxy(original, {
  get: function (target, prop, receiver) {
    if (prop === 'name') return 'МАРИЯ'
    return 'YOU HAVE BEEN PWND!'
  },
})

        
        
          
        
      

Теперь при обращении к проксированному объекту будет запускаться функция-геттер, которая проверит, к какому свойству мы обратились, и решит что именно вернуть:

        
          
          console.log(proxied.name) // МАРИЯconsole.log(proxied.email) // YOU HAVE BEEN PWND!
          console.log(proxied.name) // МАРИЯ
console.log(proxied.email) // YOU HAVE BEEN PWND!

        
        
          
        
      

Оригинальный объект остаётся при этом нетронутым:

        
          
          console.log(original.name) // Марияconsole.log(original.email) // hi@site.com
          console.log(original.name) // Мария
console.log(original.email) // hi@site.com

        
        
          
        
      

Когда использовать

Скопировано

Используйте прокси, когда вам необходимо заменить полностью или поменять API другого модуля, не трогая оригинальный объект.

Такое бывает полезно при обработке пользовательских событий в сложных сценариях или при работе с DOM, когда перед вставкой нового DOM-узла нужно проверить какие-то условия.

Другие паттерны

Скопировано

Мы рассмотрели самые частые из структурных паттернов проектирования. Их немного больше, но остальные используются реже.

Кроме структурных также существуют и другие виды паттернов проектирования:

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