Here is 6 Reasons Why You Should Know The Composite Design Pattern

Christopher T.

January 15th, 2022

Share This Post

The composite pattern can be your swiss-army knife when developing dynamic user interfaces. When we develop web applications we work with the DOM in the client side. This is perfect because of how the DOM is structured.

The goal is to compose more than one object into a certain tree structure representing a part-whole hierarchy.

A part-whole relationship is when each object in a collection is a part of the whole composition. This "whole" composition is a collection of parts. When we think of a part whole hierarchy, it's a tree structure where each individual "leaf" or "node" is treated the same as every other leaf or node in the tree, allowing them to be worked with uniformly in our program. This means that a collection or group of objects (sub-tree of leafs/nodes) is also a leaf or node.

Here is how it looks like when we look at it in a visual perspective:

composite-pattern-part-whole-tree

In software engineering developers can take advantage of the pattern where they need to work with deeply nested objects and not have to worry much about the implementation details afterwards. This is a powerful technique that can help solve complex problems as we will see in this post.

In this post I will explain At Least 5 Reasons the Composite Design Pattern Must Be Learned By You

You can work with collections the same way you can with single objects

Working in uniformity with objects and collections of objects is already powerful.

You most likely already worked with code that uses it. If you are familiar with the DOM then you're already familiar with traversing children off of DOM nodes

const container = document.getElementById('root')

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

Because the DOM is structured where nodes have a part-whole relationship with nodes that surround it, we can work with the DOM in uniform. This example below is a simple way to highlight an element's img children using a simple traverse function:

#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'))
})

highlight-dom-nodes-with-border

Recursion becomes easier

In the previous code example we saw how traversing the DOM allowed us to highlight a collection of DOM nodes. But what the code also shows is recursion in the traverse function. Recursion becomes a piece of cake because code becomes a lot smaller which helps to maintain and read the code a lot easier. There is no need to perform a for loop or keep track of any state to traverse and access all descendants.

You can find the position of any DOM element

Thanks to the nature of the composite structure we can take advantage of this ability to walk through a DOM node's tree.

Finding the position of an element in the DOM can be a nightmare especially when they are tweaked with different position values.

Of course we can just use something like myElement.getBoundingClientRect() and call it a day since it returns us positions. But it's not really reliable because if you scroll down the page you can actually get a negative value (in that case you would have to include window.pageYOffset) since it is computed relative to the scrolled window.

We can take advantage of the pattern to find an element's position in the DOM by traversing up the tree because they all this interface:

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

You can understand how to prop drill in react

This may sound silly to say but you should realize that prop drilling is dependent on each react component sharing the children interface (the returned element/components are the children behind the scenes):

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

Extensibility and Scalability

It is a very simple task to continue the uniformity of our objects when we need to extend existing objects as long as we keep the signature or interface of our leaf nodes/composites. This means scalability won't be much of a problem in the future.

Forms can be created with ease

Forms are great demonstrations showing its effectiveness in creating robust applications. Here is an example to demonstrate the power this pattern provides when developing forms:

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() {
    // Submit this.value somewhere
  }
}

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
  }
}

Extending the Field class was simple and short. Thanks to the composite nature of our instances we're able to grab all values into a nice JSON object by calling the uniform value getter function:

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 = 'pfft@gmail.com'
nameField.value = 'loc'
genderSelectField.value = 'Female'
employeeField.setFirstName('Holly')
employeeField.setLastName('Le')

console.log(form.value)

Result:

{
  "email": "pfft@gmail.com",
  "name": "loc",
  "gender": "",
  "employee": { "firstName": "Holly", "lastName": "Le" }
}

The developer does not have to worry about any implementation details when they use the components and this lets them focus on writing the actual code for their application.

Conclusion

And that concludes the end of this post! I hope you found this to be useful and look out for more in the future!


Top online courses in Web Development

Tags


Read every story from jsmanifest (and thousands of other writers on medium)

Your membership fee directly supports jsmanifest and other writers you read. You'll also get full access to every story on Medium.

Subscribe to the Newsletter

Get continuous updates

Mediumdev.toTwitterGitHubrss

© jsmanifest 2023