Composite Pattern in TypeScript — ✨ Structural Design Pattern #3

365kim
4 min readJun 4, 2023

--

Photo by Didssph on Unsplash

🙋🏻‍♀️ What is the Composite Pattern?

The Composite Pattern is a structural design pattern that allows you to treat individual objects and groups of objects uniformly so that you can interact with objects at different levels of the hierarchy more consistently.

This pattern lets you compose objects into tree-like structures to represent part-whole hierarchies.

Part-Whole hierarchy

A part-whole hierarchy(a.k.a. composite hierarchy) is a hierarchical relationship like nesting dolls🪆. In this hierarchy, a whole object is composed of parts, and those parts can themselves be whole objects composed of further parts.

The fundamental concept behind the Composite Pattern is to create an abstract base class or interface, called the Component, which declares common operations for both Leafnodes (representing individual objects) and Composite nodes (representing composite objects).

The fundamental concept behind the Composite Pattern is to create an abstract base class or interface, called the Component, which declares common operations for both Leaf nodes(representing individual objects) and Composite nodes(representing composite objects).

✨ Simple TypeScript Example

The Component interface defines the common methods and properties that both composites and leaves must implement. In this example they are display(), detach(), add() and delete()

interface Component {
name: string
parent?: Composite
display(): void
detach(): void
add(component: Component): void
delete(component: Component): void
}

The Composite class implements the Component interface. It has a name, children and a reference to its parent composite (optional). The display() method recursively calls the display() method on its child components.

class Composite implements Component {
name: string
children: Component[]
parent?: Composite

constructor(name: string) {
this.name = name
this.children = []
}

display(): void {
const composite = `Composite Name:${this.name}`
const parent = `Parent Name:${this.parent?.name ?? '(none)'}`
const children = `Children Count:${this.children.length}`

console.log(`${composite} ${parent} ${children}`)
this.children.forEach((c) => c.display()) // recursive
}

add(component: Component): void {
component.detach() // ✅
component.parent = this
this.children.push(component)
}

delete(component: Component): void {
const index = this.children.indexOf(component)

if (index === -1) return
this.children.splice(index, 1)
}

detach(): void {
if (!this.parent) return
this.parent.delete(this)
this.parent = undefined
}
}

Notice that detach() in the Composite class is called right before a leaf component is added to a composite. If the composite has a parent, it calls delete() on the parent to remove itself from the parent's children.

The Leaf also implements the Component interface, but cannot have any child components.

class Leaf implements Component {
name: string
parent?: Composite

constructor(name: string) {
this.name = name
}

display(): void {
const leaf = `Leaf Name:${this.name}`
const parent = `Parent Name:${this.parent?.name ?? '(none)'}`

console.log(`${leaf} ${parent}`)
}

detach(): void {
if (!this.parent) return
this.parent.delete(this)
}

add(component: Component): void {
throw new Error(`Cannot add ${component.name} to leaf ${this.name}.`)
}

delete(component: Component): void {
throw new Error(`Cannot delete ${component.name} to leaf ${this.name}.`)
}
}

If you want to make certain methods available only in Composite, you can throw an error as demonstrated in the case of add() or delete() in Leaf.

Now, you can treat each component in the hierarchy uniformly, regardless of whether it is a Leaf or Composite.

// Client Code
const manager1 = new Composite('manager1')
const manager2 = new Composite('manager2')

const junior1 = new Leaf('junior1')
const junior2 = new Leaf('junior2')

manager1.add(junior1)
manager1.display()
// Composite Name:manager1 Parent Name:(none) Children Count:1
// Leaf Name:junior1 Parent Name:manager1

manager2.add(manager1)
manager2.display()
// Composite Name:manager2 Parent Name:(none) Children Count:1
// Composite Name:manager1 Parent Name:manager2 Children Count:1
// Leaf Name:junior1 Parent Name:manager1

junior1.display()
// Leaf Name:junior1 Parent Name:manager1
junior2.display()
// Leaf Name:junior2 Parent Name:(none)

And if you call methods like add() or delete() with a Leaf, an error will be thrown as designed because these methods are meant to be used exclusively in the Composite class.

junior1.add(junior2)
// Cannot add junior1 to leaf junior2.

🧑🏻‍💻 Use it or Avoid it

When to use it

The Composite Pattern allows you to represent complex structures in a hierarchical manner, if you have objects that can be composed of other objects, and you want to treat them uniformly and avoid conditional statements for handling objects differently.

This Pattern also simplifies the addition and removal of objects from the hierarchy. You can add or remove objects at runtime without disrupting the overall structure. So it will be a good choice if you want to add or remove objects dynamically.

When to avoid it

The Composite Pattern may not be appropriate objects in the hierarchy have vastly different behaviors. In that case, treating Composite and Leaf uniformly is inappropriate.

Moreover, the complexity and overhead introduced by this pattern may not be necessary if the hierarchy remains fixed and you don’t need to add or remove objects dynamically.

--

--

365kim
365kim

Written by 365kim

Web Front-End Developer who believes Every Day Counts!

No responses yet