Перевод статьи подготовлен в преддверии старта курса «JavaScript Developer. Basic».




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

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

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

В иерархии от частного к целому каждый объект коллекции является частью общей композиции. Эта общая композиция, в свою очередь, является коллекцией ее частей. Иерархия от частного к целому строится как древовидная структура, где каждый отдельный «лист» или «узел» воспринимается и обрабатывается точно так же, как любой другой лист или узел в любой части дерева. Таким образом, группа или коллекция объектов (поддерево листов/узлов) также является листом или узлом.

Визуально это можно представить приблизительно так:



Теперь, когда у нас есть более четкое понимание отношений между частным и целым, вернемся к термину компоновщик. Мы определили, что использование компоновщика призвано объединить упомянутые объекты (листы/узлы) в дерево согласно этому принципу.

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

Внутреннее строение


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

Где применяется этот шаблон?


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

Файлы (для удобства все объекты в директории можно считать «элементами») являются листьями/узлами (частями) внутри целого композита (директории). Созданная в директории поддиректория аналогично является листом или узлом, включающим в себя другие элементы, такие как видео, изображения и т. п. В то же время и директории, и поддиректории также являются композитами, поскольку представляют собой коллекции отдельных частей (объектов, файлов и т. п.).

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

Чем интересен этот шаблон?


Если кратко: Он очень мощный.

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

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

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

Примеры


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

Нам предстоит работать с классом Document, который будет иметь свойство signature со значением по умолчанию false. Если врач подпишет документ, то значение signature будет изменено на его подпись. Мы также определяем в этом классе метод sign, с помощью которого реализуется эта функция.

Вот так будет выглядеть Document:

class Document {
  constructor(title) {
    this.title = title
    this.signature = null
  }
  sign(signature) {
    this.signature = signature
  }
}


Теперь, применив компоновщик, мы обеспечим поддержку методов, схожих с теми, что определены в Document.

class DocumentComposite {
  constructor(title) {
    this.items = []
    if (title) {
      this.items.push(new Document(title))
    }
  }

  add(item) {
    this.items.push(item)
  }

  sign(signature) {
    this.items.forEach((doc) => {
      doc.sign(signature)
    })
  }
}

Теперь становится очевидным изящество шаблона. Обратите внимание на два последних сниппета кода: Давайте взглянем на шаблон в визуальном представлении:

Отлично! Похоже, мы на верном пути. То, что у нас получилось, соответствует схеме, представленной выше.



Итак, наша древовидная структура содержит два листа/узла — Document и DocumentComposite. Оба они совместно используют один и тот же интерфейс и, следовательно, действуют как «части» единого композитного дерева.

Здесь следует отметить, что лист/узел дерева, не являющийся композитом (Document), не является коллекцией или группой объектов и, следовательно, не продолжит ветвления. Тем не менее лист/узел, являющийся композитом, содержит коллекцию частей (в нашем случае это items). Также помните, что Document и DocumentComposite используют общий интерфейс, а следовательно, разделяют и метод sign.

Так в чем же заключается эффективность такого подхода? Несмотря на то что DocumentComposite использует единый интерфейс, поскольку задействует метод sign, как и Document, в нем реализован более эффективный подход, позволяющий при этом достичь конечной цели.

Поэтому вместо такой структуры:

const pr2Form = new Document(
  'Primary Treating Physicians Progress Report (PR2)',
)
const w2Form = new Document('Бланк Налогового управления (W2)')

const forms = []
forms.push(pr2Form)
forms.push(w2Form)

forms.forEach((form) => {
  form.sign('Bobby Lopez')
})

Мы можем видоизменить код и сделать его эффективнее, воспользовавшись преимуществами компоновщика:

const forms = new DocumentComposite()
const pr2Form = new Document(
  'Текущие сведения о производственных врачах (PR2)',
)
const w2Form = new Document('Бланк Налогового управления (W2)')
forms.add(pr2Form)
forms.add(w2Form)

forms.sign('Роман Липин')

console.log(forms)

При таком подходе нам потребуется лишь единожды выполнить sign после добавления всех нужных документов, и эта функция подпишет все документы.

Убедиться в этом можно, просмотрев вывод функции console.log(forms):



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

Также не стоит забывать, что наш DocumentComposite может включать коллекцию документов.

Поэтому, когда мы проделали вот это:

forms.add(pr2Form) // Документ
forms.add(w2Form) // Документ

Наша схема обрела следующий вид:



Мы добавили две формы, и теперь эта схема почти полностью соответствует исходной:


Тем не менее наше дерево прекращает свой рост, поскольку последний его лист образовал только два листа, что не вполне соответствует схеме на последнем скриншоте. Если бы вместо этого мы сделали форму w2form композитом, как показано здесь:

const forms = new DocumentComposite()
const pr2Form = new Document(
  'Текущие сведения о производственных врачах (PR2)',
)
const w2Form = new DocumentComposite('Бланк Налогового управления (W2)')
forms.add(pr2Form)
forms.add(w2Form)

forms.sign('Роман Липин')

console.log(forms)

Тогда наше дерево могло бы продолжать расти:


В конечном итоге мы бы достигли той же цели — все документы были бы подписаны:


В этом и заключается польза компоновщика.

Заключение


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

Найти меня на medium



Читать ещё: