JavaScript

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

Пример использования:

@filterMales // Это декоратор
class MyClass {
  constructor(children) {
    this.children = children
  }
}

Декораторы — это синтаксический сахар. Если вы работали с JavaScript, то, вероятно, уже сталкивались с их реализацией. Они просты в написании, но эффективны в использовании.

Рассмотрим несколько примеров декораторов в JavaScript и узнаем, как можно применять их в коде.

Когда нужно использовать декоратор?

Существует несколько случаев применения:

Добавление динамического поведения к существующим объектам

Как было сказано ранее, один из сценариев использования декораторов — динамическое добавление дополнительной логики к объектам без необходимости использовать такие альтернативы, как наследование.

Примечание: декораторы могут вводить функции в объекты не заметно для остальных частей кода.

Допустим, у нас есть класс Frog, который будет реализовывать метод lick. У лягушек есть зубы, поэтому мы также реализуем метод getTeeths, чтобы вернуть количество зубов, которое у них есть.

Реализация может выглядеть следующим образом:

function Frog(name) {
  this.name = name
}

Frog.prototype.getTeeths = function() {
  return 2
}

Frog.prototype.lick = function(target) {
  console.log(`I'm going lick you, ${target.name}. You better taste delicious`)
}

// Или с классами

class Frog {
  constructor(name) {
    this.name = name
  }

  getTeeths() {
    return 2
  }

  lick(target) {
    console.log(
      `I'm going lick you, ${target.name}. You better taste delicious`,
    )
  }
}

В реальности существуют и другие разновидности лягушек, например жаба. Жаба относится к лягушкам, но лягушка не является жабой. Это означает, что между ними должны быть отличительные особенности, которые нельзя смешивать.

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

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

Реализация декоратора withToad выглядит довольно просто:

function withToad(frog) {
  frog.getTeeths = function() {
    return 0
  }
}

const mikeTheFrog = new Frog('mike')
withToad(mikeTheFrog)

console.log(mikeTheFrog.getTeeths())

Декоратор withToad повторно реализует getTeeths, возвращающий 0, поскольку у жаб нет зубов. При использовании этого декоратора лягушка «преобразуется» в жабу.

Достичь той же цели можно с помощью метода subclass с наследованием, как показано ниже:

function Toad(name) {
  Frog.call(this, name)
  
  this.getTeeths = function() {
    return 0
  }
}

const kellyTheToad = new Toad('kelly')

// или с помощью классов

class Toad extends Frog {
  getTeeths() {
    return 0
  }
}

const kellyTheToad = new Toad('kelly')

Разница между этими двумя подходами заключается в том, что при использовании декораторов не нужно создавать классы для жаб.

Теперь переходим к более интересным примерам.

Допустим, мы создаем приложение, которое поддерживает различные предварительно определенные пользовательские темы, с помощью которых можно изменять стиль панели управления.

Мы реализуем Theme с помощью метода createStylesheet для создания совместимой таблицы стилей, а также applyStyles для анализа и применения этой таблицы к DOM:

function Theme() {}

Theme.prototype.createStylesheet = function() {
  return {
    header: {
      color: '#333',
      fontStyle: 'italic',
      fontFamily: 'Roboto, sans-serif',
    },
    background: {
      backgroundColor: '#fff',
    },
    button: {
      backgroundColor: '#fff',
      color: '#333',
    },
    color: '#fff',
  }
}

Theme.prototype.applyStylesheet = function(stylesheet) {
  const bodyElem = document.querySelector('body')
  const headerElem = document.getElementById('header')
  const buttonElems = document.querySelectorAll('button')
  this.applyStyles(bodyElem, stylesheet.background)
  this.applyStyles(headerElem, stylesheet.header)
  
  buttonElems.forEach((buttonElem) => {
    this.applyStyles(buttonElem, stylesheet.button)
  })
}

Theme.prototype.applyStyles = function(elem, styles) {
  for (let key in styles) {
    if (styles.hasOwnProperty(key)) {
      elem.style[key] = styles[key]
    }
  }
}

Мы определили API Theme и теперь можем создать таблицу стилей следующим образом:

const theme = new Theme()
const stylesheet = theme.createStylesheet()

На данный момент stylesheet выглядит так:

{
  "header": {
    "color": "#333",
    "fontStyle": "italic",
    "fontFamily": "Roboto, sans-serif"
  },
  "background": { "backgroundColor": "#fff" },
  "button": { "backgroundColor": "#fff", "color": "#333" },
  "color": "#fff"
}

Теперь ее можно использовать для преображения веб-страницы:

theme.applyStylesheet(stylesheet)

Предоставление открытых возможностей для поддержки разработки плагинов

Как сделать так, чтобы theme возвращал пользовательскую тему при вызове createStylesheet, которую можно использовать для расширения вместо темы по умолчанию?

В таком случае могут пригодиться декораторы, так как с их помощью можно вернуть другую предварительно определенную тему по умолчанию.

Мы создадим декоратор, чтобы применить тему blood, преобразующую Theme так, чтобы он генерировал таблицу стилей по умолчанию, которая будет представлять тему blood вместо оригинала.

Назовем этот декоратор bloodTheme:

function bloodTheme(originalTheme) {
  const originalStylesheet = originalTheme.createStylesheet()
  
  originalTheme.createStylesheet = function() {
    return {
      name: 'blood',
      ...originalStylesheet,
      header: {
        ...originalStylesheet.header,
        color: '#fff',
        fontStyle: 'italic',
      },
      background: {
        ...originalStylesheet.background,
        color: '#fff',
        backgroundColor: '#C53719',
      },
      button: {
        ...originalStylesheet.button,
        backgroundColor: 'maroon',
        color: '#fff',
      },
      primary: '#C53719',
      secondary: 'maroon',
      textColor: '#fff',
    }
  }
}

Теперь нужно добавить в theme только одну строку:

const theme = new Theme()
bloodTheme(theme) // Applying the decorator
const stylesheet = theme.createStylesheet()
console.log(stylesheet)

Теперь тема предоставляет таблицу стилей blood по умолчанию:

{
  "name": "blood",
  "header": {
    "color": "#fff",
    "fontStyle": "italic",
    "fontFamily": "Roboto, sans-serif"
  },
  "background": { "backgroundColor": "#C53719", "color": "#fff" },
  "button": { "backgroundColor": "maroon", "color": "#fff" },
  "color": "#fff",
  "primary": "#C53719",
  "secondary": "maroon",
  "textColor": "#fff"
}

Как видите, код/реализация theme не изменились. Применение пользовательской таблицы стилей также не изменилось:

theme.applyStylesheet(stylesheet)

Теперь веб-страница раскрашена красным цветом:

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

Применение временного поведения

Декораторы также стоит использовать при необходимости временно применить поведение к объектам, которые будут удалены в дальнейшем.

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

Чтобы вернуться к исходной таблице стилей нужно просто удалить строку bloodTheme(theme).

Наследование

Еще один пример использования декораторов — при создании подклассов в большом коде.

Однако этот случай не является большой проблемой в JavaScript (если только вы не используете реализации наследования классов), в отличие от статических языков, таких как Java.

Режимы отладки

Еще один полезный вариант использования — создание декоратора режима отладки, который будет регистрировать все события, происходящие в консоли. Например, декоратор debugTheme, который пригодится в режиме разработки:

function debugTheme(originalTheme) {
  const stylesheet = originalTheme.createStylesheet()
  
  console.log(
    '%cStylesheet created:',
    'color:green;font-weight:bold;',
    stylesheet,
  )
  
  if (!stylesheet.primary) {
    console.warn(
      'A stylesheet was created without a primary theme color. There may be layout glitches.',
    )
  }
}

const theme = new Theme()
bloodTheme(theme)
if (process.env.NODE_ENV === 'development') debugTheme(theme)

Теперь консоль предоставляет полезную информацию при запуске приложения в режиме development:

Читайте также:


Перевод статьи jsmanifest: Learn About Decorators in JavaScript