Паттерн проектирования “Компоновщик” может быть универсальным решением для создания динамических пользовательских интерфейсов. При разработке веб-приложений мы работаем с DOM на стороне клиента. И это просто отличный вариант, учитывая структуру DOM. 

Цель паттерна  —  объединить несколько объектов в древовидную структуру, представляющую собой иерархию “часть- целое”.  

Такой тип взаимоотношений предполагает, что каждый объект коллекции  —  это часть общей структуры, которая является совокупностью этих частей. Иерархия “часть-целое” представляет собой древовидную структуру, которая отличается единообразной трактовкой каждого листа (leaf) и узла (node). Данный принцип позволяет одинаково работать с ними в программе. Это значит, что совокупность или группа объектов (поддерево листьев/узлов) также является листом или узлом. 

Описанная структура выглядит так:

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

В статье мы приведем аргументы, доказывающие значимость паттерна “Компоновщик”. 

Вы работаете с коллекциями точно так же, как и с одиночными объектами 

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

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

const container = document.getElementById('root')

function traverse(callback, elem) {
if (elem.children.length) {
for (const childNode of elem.children) {
callback(childNode)
traverse(callback, childNode)
}
}
}

В структуре DOM узлы находятся в отношениях “часть-целое” с окружающими их узлами, что позволяет единообразно работать с DOM. Рассмотрим простой способ выделения потомка элемента img с помощью функции traverse:

#gallery-container {
margin: auto;
}

#gallery {
display: flex;
max-height: 200px;
justify-content: center;
}

#gallery img {
max-width: 100px;
}

.highlight {
border: 1px solid magenta;
}

button {
display: block;
margin: auto;
}
<div id="root">
<main id="content">
<div id="gallery-container">
<div id="gallery">
<img src="https://jsmanifest.s3.us-west-1.amazonaws.com/other/apple.jpg"></img>
<img src="https://jsmanifest.s3.us-west-1.amazonaws.com/other/orange.jpg"></img>
<img src="https://jsmanifest.s3.us-west-1.amazonaws.com/other/banana.jpg"></img>
</div>
</div>
<div style="height: 10px"></div>
<button type="button" onclick="function() { highlight(); }">
Highlight
</button>
</main>
</div>
const container = document.getElementById('root')

function traverse(callback, elem) {
if (elem && elem.children && elem.children.length) {
for (const childNode of elem.children) {
callback(childNode)
traverse(callback, childNode)
}
}
}

document.querySelector('button').addEventListener('click', function () {
traverse(function onChild(childNode) {
console.log(childNode.tagName)
if (childNode.tagName === 'IMG') {
childNode.classList.add('highlight')
}
}, document.getElementById('gallery'))
})

Упрощается рекурсия 

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

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

Вы определяете положение любого элемента DOM 

Благодаря структурным особенностям паттерна “Компоновщик” мы можем использовать его для обхода дерева узлов DOM. 

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

Конечно, можно воспользоваться методом myElement.getBoundingClientRect() и добиться желаемого результата, поскольку он возвращает позиции. Но такой способ не совсем надежный. Дело в том, что при прокрутке страницы мы можем получить отрицательное значение (в этом случае придется включить window.pageYOffset), так как оно вычисляется относительно прокрученного окна.  

С помощью данного паттерна мы можем определить положение элемента в DOM, перемещаясь вверх по дереву, поскольку они все используют этот интерфейс: 

function getParentOffset(el): number {
if (el.offsetParent) {
return el.offsetParent.offsetTop + getParentOffset(el.offsetParent)
} else {
return 0
}
}

Вы понимаете, как происходит передача свойств в React 

Вы должны понимать, что передача свойств зависит от каждого компонента React, совместно использующего интерфейс children (возвращаемые элементы/компоненты негласно являются children): 

import React, { useState } from 'react'

function Parent() {
const [myLabel, setMyLabel] = React.useState('')

React.useEffect(() => {
setMyLabel('Hello')
}, [])

return <ChildA label={myLabel} />
}

function ChildA({ label }) {
return <ChildB label={label} />
}

function ChildB({ label }) {
return (
<>
Our label is: <ChildC label={label} />
</>
)
}

function ChildC({ label }) {
return <span>{label}</span>
}

export default Parent

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

Вы легко создаете формы 

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

class Form {
constructor(...fields) {
this.el = document.createElement('form')
this.fields = fields.reduce((acc, field) => {
acc[field.getName()] = field
this.el.appendChild(field.el)
return acc
}, {})
}

get value() {
const values = {}

for (const [name, field] of Object.entries(this.fields)) {
values[name] = field.value
}

return values
}

getName() {
return this.el.name
}

setName(name) {
this.el.name = name
}

render() {
if (!document.body.contains(this.el)) {
document.body.appendChild(this.el)
}
}

submit() {
// Отправляем куда-нибудь this.value
}
}

class Field {
constructor(name = '', value = '') {
this.name = name
this.value = value
}

getName() {
return this.name
}

setName(name) {
this.name = name
}
}

class InputField extends Field {
constructor(...args) {
super(...args)
this.el = document.createElement('input')
}

get value() {
if (!this.el) return ''
return this.el.value
}

set value(value) {
if (this.el) {
this.el.value = value
}
}
}

class SelectField extends Field {
constructor(...args) {
super(...args)
this.el = document.createElement('select')
this.selected = ''
}

get value() {
return this.el.value
}

set value(value) {
if (this.el) {
this.el.value = value
}
}

select(option) {
this.el.value = option
}

setOptions(options) {
this.options = options
}
}

class EmployeeField extends Field {
constructor(...args) {
super(...args)
this.el = document.createElement('div')
this.firstNameField = new InputField('firstName')
this.lastNameField = new InputField('lastName')
this.el.appendChild(this.firstNameField.el)
this.el.appendChild(this.lastNameField.el)
}

get value() {
return {
[this.firstNameField.getName()]: this.firstNameField.value,
[this.lastNameField.getName()]: this.lastNameField.value,
}
}

set value(values) {
for (const [key, value] of Object.entries(values)) {
if (key === 'firstName') {
this.firstNameField.value = value
} else if (key === 'lastName') {
this.lastNameField.value = value
}
}
}

setFirstName(firstName) {
this.firstNameField.value = firstName
}

setLastName(lastName) {
this.lastNameField.value = lastName
}
}

Расширение класса Field было простым и кратким. Составная природа экземпляров позволяет собрать все значения в объект JSON путем вызова единой функции-геттера value:

const emailField = new InputField('email')
const nameField = new InputField('name')
const genderSelectField = new SelectField('gender')
const employeeField = new EmployeeField('employee')
const form = new Form(emailField, nameField, genderSelectField, employeeField)

emailField.value = '[email protected]'
nameField.value = 'loc'
genderSelectField.value = 'Female'
employeeField.setFirstName('Holly')
employeeField.setLastName('Le')

console.log(form.value)

Результат:

{
"email": "[email protected]",
"name": "loc",
"gender": "",
"employee": { "firstName": "Holly", "lastName": "Le" }
}

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

Заключение 

Вот мы и подошли к логическому завершению статьи. Надеюсь, что материал был полезен. До новых встреч!

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

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи jsmanifest: 5 Reasons Why You Should Know The Composite Design Pattern

Предыдущая статьяКак использовать шаблон проектирования “Адаптер” в React
Следующая статьяРуководство по подготовке к собеседованию по SQL