Understanding JavaScript Composite Patterns

A composite pattern deals with the complexity of how objects are related to one another. According to the original definition,
[a composite pattern] allows you to compose objects into tree structures to represent part-whole hierarchies
Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley Professional Computing Series)
But what does this mean? Let’s take a look at our cart object again. If we break it down, the entire cart can be composed of many ‘sub-items’ that can also exist as stand-alone items. The cart process involves multiple parts that may or may not be needed, depending on context and situational requirements.
In a composite pattern, an object can have a leaf and/or a composite. A leaf is the object’s implementations while a composite is the child object.
So if you look at it in the form of a tree diagram, a composite pattern looks something like this:

The composite structural pattern keeps the relationships and hierarchies of the objects (composites) together. However, you can take a branch and create ‘part-whole’ objects out of them.
For example, if you take Composite2
and its associated branches, the object created will be a hybrid solution of Composite2
and Composite3
. Leaf3
is the actual object methods and properties of Composite2
.
So why separate it out and call it a leaf? Why not just have it as part of the composite? Because the composite node itself contains information that helps makes the connection with the child composite. The leaf is a visual representation of the composite’s object.
This can feel like a lot to take in, so lets us go back to our trusty Cart
object.

If we scaffold this out, the JavaScript code can look something like this:
class Component {
constructor(props) {
this.props = props;
}
update() {
console.log('default update');
}
}
class Cart extends Component {
update() {
console.log('cart updated');
}
}
class ItemsManager extends Component{
update() {
console.log('items manager updated');
}
}
class VariationManager extends Component{
update() {
console.log('variation manager updated');
}
}
class CountryManager extends Component{
update() {
console.log('country manager updated');
}
}
class ShippingManager extends Component{
update() {
console.log('shipping manager updated');
}
}
class ShippingAddOnManager extends Component{
update() {
console.log('shipping addon manager updated');
}
}
class TaxesManager extends Component{
update() {
console.log('taxes manager updated');
}
}
class DiscountManager extends Component{
update() {
console.log('discount manager updated');
}
}
class PromoCodeManager extends Component{
update() {
console.log('promo code manager updated');
}
}
class SpecialDealsManager extends Component{
update() {
console.log('special deals manager updated');
}
}
//bringing it all together
//initializing the individual leaves
const variationManager = new VariationManager();
const countryManager = new CountryManager();
const shippingAddOnManager = new ShippingAddOnManager();
const promoCodeManager = new PromoCodeManager();
const specialDealsManager = new SpecialDealsManager();
const taxesManager = new TaxesManager();
//initalizing the composite branches
const itemsManager = new ItemsManager({
children: [variationManager]
})
const taxManager = new CountryManager({
children: [taxesManager]
})
const shippingManager = new CountryManager({
children: [ shippingAddOnManager]
})
const discountManager = new DiscountManager({
children: [promoCodeManager, specialDealsManager]
})
//bringing it all together to form the tree
const cart = new Cart({
children:[
itemsManager,
taxManager,
shippingManager,
discountManager]
})
console.log(cart);
When you console.log
your cart
object, you'll get something like this:
Cart {props: {…}}
props:
children: Array(4)
0: ItemsManager {props: {…}}
1: CountryManager {props: {…}}
2: CountryManager {props: {…}}
3: DiscountManager {props: {…}}
length: 4
__proto__: Array(0)
__proto__: Object
__proto__: Component
Alternatively, if you console.log
one of the branches, you'll get the part-whole object. For example, if we console.log(discountManager)
, we will get something like this:
DiscountManager {props: {…}}
props:
children: Array(2)
0: PromoCodeManager {props: undefined}
1: SpecialDealsManager {props: undefined}
length: 2
__proto__: Array(0)
__proto__: Object
__proto__: Component
props
is undefined because we've only done the cold scaffold and only have one method inside the PromoCodeManager
and SpecialDealsManager
objects. To add properties to your objects, you can just write your class
object as per usual.
The Component
class acts as the parent class and keeps everything together. Composing your relationships as a composite tree is a matter of putting them together in the children
array when you declare it.
This is the barebones of what a composite pattern looks like in JavaScript. As your code grows in size and complexity, abstracting out your relationships into a visual composite tree can help keep your code in check. Why? Because the visual diagram helps act as a checklist and ensure that your relationships make sense before you start hitting your favorite IDE.
A composite pattern is a representation of ideas and their relationships with one another. Your JavaScript is just a translation of the pattern into a programmatic format. When you draw your composite tree first, it can also double up as documentation. This can come in handy, especially as the code increases in size and complexity. It also makes it easier for other developers who may not be familiar with your code to understand the relationships between each class on a logical level.
Comments ()