Going over the Composite Design Pattern in JavaScript

In this post, we will be going over the composite design pattern in JavaScript.

In software engineering, the composite pattern is a pattern where a group of objects is to be treated in the same way as a single instance of a single object, resulting in uniformity with these objects and compositions.

The intention of a composite is to compose multiple objects into a certain tree structure. This tree structure represents a part-whole hierarchy.

To understand the composite pattern in greater detail, we’d have to understand what a part-whole is and what it would look like from a visual perspective.

In terms, a part-whole relationship is basically where each object in a collection is a part of the whole composition. This whole composition is a collection of parts.

Now, 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. This means that a group or collection of objects (sub-tree of leaves/nodes) is also a leaf or node.

From a visual perspective, an example of that can end up looking something like this:

Composite Design Pattern

Now that we have a clearer understanding of the part-whole concept, let’s go back to the term composite. We said that the intention of a composite is to compose any of these objects (leaves/nodes) into a tree, representing this concept.

And so, the composite design pattern is where each item in a collection can hold other collections themselves, enabling them to create deeply nested structures.

The Anatomy

Every node in the tree structure shares a common set of properties and methods which enables them to support individual objects and treat them the same as a collection of objects.

This interface promotes the construction and design of algorithms that are recursive and iterate over each object in the composite collection.

Who Uses the Pattern?

Operating systems use the pattern which, in turn, leads to useful features like allowing us to create directories inside other directories.

The files (we can refer to anything inside a directory as an “item” at this point, which makes more sense) are the leaves/nodes (parts) of the whole composite (the directory).

Creating a sub-directory in this directory is also a leaf/node including other items like videos, images, etc. However, a directory or sub-directory is also a composite because it’s also a collection of parts (objects/files/etc.).

Popular libraries like React and Vue make extensive use of the composite pattern to build robust, reusable interfaces. Everything you see on a web page is represented as a component.

Each component of the web page is a leaf of the tree and can compose multiple components together to create a new leaf. (When this happens, it’s a composite but it is still a leaf of the tree).

This is a powerful concept as it helps make development much easier for consumers of the library, in addition to making it highly convenient to build scalable applications that utilize many objects.

Why Should We Care About This Pattern?

The easiest way to put it: Because it’s powerful.

What makes the composite design pattern so powerful is its ability to treat an object as a composite object. This is possible because they all share a common interface.

What this means is that you can reuse objects without worrying about incompatibility with others.

When you’re developing an application and you come across a situation where you’re dealing with objects that have a tree structure, it could end up being a very good decision to adopt this pattern into your code.

Examples

Let’s say we are building an application for a new business where its main purpose is to help doctors qualify for telemedicine platforms. They do this by collecting their signatures for mandatory documents that are required by law.

We’re going to have a Document class that will have a signature property with a default value of false. If the doctor signs the document, signature should flip its value to their signature.

We’re also defining a sign method onto it to help make this functionality happen.

This is what the Document will look like:

Document.ts

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

Now, when we implement the composite pattern, we’re going to support similar methods that a Document defined.

DocumentComposite.js

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

Now, here comes the beauty of the pattern. Pay attention to our two most recent code snippets. Let’s see this from a visual perspective:

Composite Design Pattern

Great! It seems like we are on the right track. We know this because what we have resembles the diagram we had before:

Composite Design Pattern

So, our tree structure contains two leaves/nodes, the Document, and the DocumentComposite. They both share the same interface so they both act as “parts” of the whole composite tree.

The thing here is that a leaf/node of the tree that is not a composite (the Document) is not a collection or group of objects, so it will stop there.

However, a leaf/node that is a composite holds a collection of parts (in our case, the items). And remember, the Document and DocumentComposite share an interface, sharing the sign method.

So, where’s the power in this?

Well, even though the DocumentComposite shares the same interface because it has a sign method just like the Document does, it is actually implementing a more robust approach while still maintaining the end goal.

So, instead of this:

forms.js

const pr2Form = new Document(
  'Primary Treating Physicians Progress Report (PR2)',
)

const w2Form = new Document('Internal Revenue Service Tax Form (W2)')

const forms = []

forms.push(pr2Form)
forms.push(w2Form)

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

We can change our code to make it more robust, taking advantage of the composite:

forms.js

const forms = new DocumentComposite()

const pr2Form = new Document(
  'Primary Treating Physicians Progress Report (PR2)',
)

const w2Form = new Document('Internal Revenue Service Tax Form (W2)')

forms.add(pr2Form)
forms.add(w2Form)

forms.sign('Bobby Lopez')

console.log(forms)

In the composite approach, we only need to sign once after we added the documents we needed, and it signs all of the documents.

We can confirm this by looking at the result of console.log(forms):

Composite Design Pattern

In the example prior to this, we had to manually add the items to an array, loop through each document ourselves, and sign them.

Let’s also not forget the fact that our DocumentComposite can hold a collection of items.

So, when we did this:

forms.add(pr2Form) // Document
forms.add(w2Form) // Document

Our diagram turned into this:

Composite Design Pattern

This closely resembles our original diagram as we added the two forms:

Composite Design Pattern

However, our tree stops because the last leaf of the tree rendered two leaves only, which isn’t exactly the same as this last screenshot. If we instead made w2form a composite, like this:

forms.js

const forms = new DocumentComposite()

const pr2Form = new Document(
  'Primary Treating Physicians Progress Report (PR2)',
)

const w2Form = new DocumentComposite('Internal Revenue Service Tax Form (W2)')

forms.add(pr2Form)
forms.add(w2Form)

forms.sign('Bobby Lopez')

console.log(forms)

Then our tree can continue to grow:

Composite Design Pattern

And, in the end, we still achieved the same goal where we needed our mandatory documents to be signed:

Composite Design Pattern

And that is the power of the composite pattern.

Conclusion

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

#javascript #programming

Going over the Composite Design Pattern in JavaScript
3.10 GEEK