Understanding JavaScript Composite Patterns

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.

💡
Want to sponsor matcha.fyi? You can have your brand featured right here. Let's connect and chat about it.