Find me on medium
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 intentions of a composite is to compose multiple objects into a certain tree structure. This tree structure represents a part-whole hierarchy.
In order to understand the composite pattern in greater detail we'd have to understand what a part-whole is and what it would look like in 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 leafs/nodes) is also a leaf or node.
In a visual perspective, an example of that can end up looking something like this:
Now that we have a clearer understanding of the part-whole concept, lets go back to the term composite. We said that the intentions of a composite is to compose any of these objects (leafs/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 is using the pattern?
Operating systems use the pattern which in turn led to useful features like allowing us to create directories inside other directories.
The files (we can refer to anything inside a directory an "item" at this point which makes more sense) are the leafs/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 in 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 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 your 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
Lets 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 how the Document
will look like:
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
has defined.
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 in a visual perspective:
Great! It seems like we are on the right track. We know this because what we have resembles the diagram we had before:
So our tree structure contains 2 leafs/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
shares 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:
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:
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)
:
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:
This closely resembles our original diagram as we added the 2 forms:
However, our tree stops because the last leaf of the tree rendered 2 leafs only, which isn't exactly the same as this last screenshot. If we instead made w2form
a composite instead like this:
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:
And in the end, we still achieved the same goal where we needed our mandatory documents to be signed:
And that, is the power of the composite pattern.
Conclusion
And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!
Find me on medium
Top comments (3)
Beautifully explained, I learned so much from this. Thank you dearly for the post!
I'm trying to understand and researching for data structures for law text. Law text is numbered. I was thinking of using nodes. What's your opinion?
I really liked this post! Really helpful and easy to read. Just wanted to remind though, that in browser JS, the Document type already exists.