Skip to main content

Introduction To The Composite Design Pattern

The Composite pattern is a structural design pattern that lets you compose objects into tree-like structures and then work with these structures as if they were individual objects.

Composite Design Pattern is a part of the Gang of Four design patterns and is categorized under structural design patterns. The pattern is used where we need to treat a group of objects in a similar way as a single object.

Components Of The Composite Design Pattern

  1. Component: This is an interface for all objects in the composition. It defines the default behavior for all objects and behaviors for accessing components in the composite structure.
  2. Leaf: It defines the behavior for the elements in the composition. It has no children.
  3. Composite: It stores child components and implements child-related operations in the component interface.

Classic Implementation

Let's take the case of a company where there are employees and managers. Managers can be seen as composite, as they can have other managers or employees under them.

Let's see how we can implement this in TypeScript:

// Component
interface Employee {
getName(): string;

getSalary(): number;

getRole(): string;
}

// Leaf
class Developer implements Employee {
constructor(private name: string, private salary: number) {}

getName(): string {
return this.name;
}

getSalary(): number {
return this.salary;
}

getRole(): string {
return "Developer";
}
}

// Another Leaf
class Designer implements Employee {
constructor(private name: string, private salary: number) {}

getName(): string {
return this.name;
}

getSalary(): number {
return this.salary;
}

getRole(): string {
return "Designer";
}
}

// Composite

interface CompositeEmployee extends Employee {
addEmployee(employee: Employee): void;

removeEmployee(employee: Employee): void;

getEmployees(): Employee[];
}

class Manager implements CompositeEmployee {
private employees: Employee[] = [];

constructor(private name: string, private salary: number) {}

getName(): string {
return this.name;
}

getSalary(): number {
return this.salary;
}

getRole(): string {
return "Manager";
}

addEmployee(employee: Employee) {
this.employees.push(employee);
}

removeEmployee(employee: Employee) {
const index = this.employees.indexOf(employee);
if (index !== -1) {
this.employees.splice(index, 1);
}
}

getEmployees(): Employee[] {
return this.employees;
}
}

Here's how you could use these classes:

let dev1 = new Developer("John Doe", 12000);
let dev2 = new Developer("Jane Doe", 15000);
let designer = new Designer("Mark", 10000);

let manager = new Manager("Michael", 25000);
manager.addEmployee(dev1);
manager.addEmployee(dev2);
manager.addEmployee(designer);

console.log(`${manager.getRole()} ${manager.getName()} manages: `);
manager
.getEmployees()
.forEach((employee) =>
console.log(
`${employee.getRole()} ${employee.getName()} with a salary of ${employee.getSalary()}`
)
);

The main benefit of this design pattern is that it allows you to run operations over both simple and complex elements. The client code can treat both types of elements in the same way. It can be a very useful tool in situations where behavior needs to be dispatched across a structure of objects.

When To Use

The Composite pattern is best used when clients ignore the difference between the compositions of objects and individual objects. If programmers find that they are using multiple objects in the same way, and often have nearly identical code to handle each of them, then the composite is a good choice.

Here are some specific cases where the Composite Pattern might be useful:

  1. Representing Part-Whole Hierarchies: If the problem domain includes a part-whole hierarchy, the composite pattern is a natural choice. It's particularly useful when you want to treat the part and whole in the same way. For example, in graphical systems, shapes can be simple (like a single line or a circle) or complex (a combination of shapes).

  2. You want to perform operations on a collection of objects the same way you’d perform them on individual objects: For example, you can use the Composite pattern when you need to apply the same operations to the various objects in your application, regardless of their complexity. In a filesystem, you might want to perform operations like listing, moving or copying on both files and directories. The composite pattern allows you to do this.

  3. The structure of objects forms a tree-like pattern: A company's organizational structure is a good example. A member of a company can be either an employee or a manager, the latter of which can manage other members, whether they be employees or other managers.

  4. You want clients to be able to ignore the difference between compositions of objects and individual objects: Clients will treat all objects in the composite structure uniformly, which makes client code simpler.

Remember, like any design pattern, Composite should not be used when the design becomes more complicated by its use. Only use it when it simplifies the client code or when the object structure has a clear, tree-like hierarchy.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.

YouTube @cloudaffle