Design Patterns — Composite in Angular

Image generated by Copilot AI

Structural patterns describe ways to combine classes and objects into larger structures.

Structural patterns consist of:
1. Adapter
2. Composite
3. Proxy
4. Flyweight
5. Facade
6. Bridge
7. Decorator

The Composite design pattern is used to create a hierarchy of whole-part relationships or to create tree data structures.

The structure of the Composite pattern consists of several elements:

Client — interacts with all elements through the component interface. As a result, the client can operate in the same way on both simple and complex tree elements.

Component — the Component interface describes operations common to both simple and complex tree elements.

Leaf — is the basic element of the tree that doesn’t have any subordinate elements. Typically, leaf components perform most of the function, because it doesn’t have any subordinate elements

Container (also known as Composite) — an element that has child elements: leaves or other containers. The container doesn’t know the concrete classes of its contents. It communicates with all child elements only through the component interface.

Please don’t confuse composite with composition. Composite is a pattern operating on a hierarchy of objects. Composition is one object containing another.

If you want to see the full demo, check out my demo project.

The Composite design pattern can be illustrated in many ways. I will present an example of displaying the company structure with the functions of adding managers and assigning added employees to a manager.

Initially, the interface EmployeeHierarchyStructure was added with methods, some of which are marked with a ? to indicate they are optional. The implementation of the EmployeeHierarchyStructure interface is presented as follows:

export interface EmployeeHierarchyStructure {
add?(employee: EmployeeHierarchyStructure): void;
remove?(employee: EmployeeHierarchyStructure): void;
getChild?(index: number): EmployeeHierarchyStructure | null;
getName(): string;
printDetails(indent?: string): void;
getEmployees?(): EmployeeHierarchyStructure[];
}

The Employee class doesn’t require their implementation. In the structure of the Composite design pattern, the Employee class represents the leaf element. The implementation of the Employee class is as follows:


export class Employee implements EmployeeHierarchyStructure {
private name: string;

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

add(employee: EmployeeHierarchyStructure): void {
throw new Error("Cannot add to a employee.");
}

remove(employee: EmployeeHierarchyStructure): void {
throw new Error("Cannot remove from a employee.");
}

getChild(index: number): EmployeeHierarchyStructure | null {
return null;
}

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

printDetails(indent: string = ''): void {
console.info(`${indent}Employee: ${this.getName()}`);
}

getEmployees(): EmployeeHierarchyStructure[] {
return [];
}
}

The Manager class reflects the Composite (Container) element. Below is the implementation of the Manager class:

export class Manager implements EmployeeHierarchyStructure {
private name: string;
private employees: Set<EmployeeHierarchyStructure>;

constructor(name: string) {
this.name = name;
this.employees = new Set<EmployeeHierarchyStructure>();
}

add(employee: EmployeeHierarchyStructure): void {
this.employees.add(employee);
}

remove(employee: EmployeeHierarchyStructure): void {
this.employees.delete(employee);
}

getChild(index: number): EmployeeHierarchyStructure | null {
return Array.from(this.employees)[index] || null;
}

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

printDetails(indent: string = ''): void {
console.info(`${indent} Manager: ${this.getName()}`);
for (const employee of this.employees) {
employee.printDetails(indent + ' ');
}
}

getEmployees(): EmployeeHierarchyStructure[] {
return Array.from(this.employees);
}
}

The Client element is the CompositeComponent, in which the highest level initially defines the company owner as Owner. In the addEmployeeForManager method, partial aggregation occurs. Removing the manager object will not cause an error when creating an employee object, only during the assignment of the employee to the manager. Assigning an employee to a manager represents full aggregation (composition). A component of the CompositeComponent class is implemented as follows:

import { Component } from '@angular/core';
import { Employee, Manager, } from '../model/model';

export interface ConfigurationOfAddingAnEmployee {
managerName: string;
employeeNeme: string;
selectedManagerName: string;
}

@Component({
selector: 'app-composite',
templateUrl: './composite.component.html',
styleUrls: ['./composite.component.scss']
})
export class CompositeComponent {

boss: Manager = new Manager('Owner');
selectedManager: Manager | null = null;

company: ConfigurationOfAddingAnEmployee = {
managerName: '',
employeeNeme: '',
selectedManagerName: ''
};

addManager(name: string) {
const manager = new Manager(name);
this.boss.add(manager);
this.company.managerName = '';
}

addEmployeeForManager(managerName: string, employeeNeme: string) {
const manager = this.findManager(managerName);
if (manager) {
const developer = new Employee(employeeNeme);
manager.add(developer);
this.company.employeeNeme = '';
}
}

findManager(name: string): Manager | null {
const managers = this.getAllManagers(this.boss);
return managers.find(manager => manager.getName() === name) || null;
}

getAllManagers(manager: Manager): Manager[] {
let managers: Manager[] = [manager];
for (const employee of manager.getEmployees()) {
if (employee instanceof Manager) {
managers = managers.concat(this.getAllManagers(employee));
}
}
return managers;
}

setSelectedManager(managerName: string) {
this.selectedManager = this.findManager(managerName);
}

}

The CompositeComponent template is as follows:

<div class="container">
<div class="row">
<label for="selectedManager">Select manager: </label>
<select id="selectedManager" [(ngModel)]="company.selectedManagerName" (ngModelChange)="setSelectedManager($event)">
<option *ngFor="let manager of getAllManagers(boss)" [value]="manager.getName()">
{{ manager.getName() }}
</option>
</select>
</div>

<div class="row">
<label for="managerName">Name manager: </label>
<input type="text" id="managerName" name="managerName" [(ngModel)]="company.managerName">
<button (click)="addManager(company.managerName)">Add Manager</button>
</div>

<div class="row" *ngIf="selectedManager">
<label for="employeeNeme">Name employee: </label>
<input type="text" id="employeeNeme" name="employeeNeme" [(ngModel)]="company.employeeNeme">
<button (click)="addEmployeeForManager(company.selectedManagerName, company.employeeNeme)">
Add Employee for Manager
</button>
</div>
</div>

<div>
<h3>Company Structure</h3>
<ul>
<ng-container *ngIf="boss">
<li *ngFor="let manager of boss.getEmployees()">
<app-employee [manager]="manager"/>
</li>
</ng-container>
</ul>
</div>

Summary

The Composite pattern defines two basic types of elements sharing the same interface: simple leaves and complex containers. A container can consist of both leaves and other containers. This allows for the construction of a nested, recursive structure of objects resembling a tree.

Materials:

  1. Java. Wzorce projektowe. Translation: Piotr Badarycz. Original title: Java design patterns. A Tutorial. Author: James William Cooper
  2. https://refactoring.guru/pl/design-patterns/composite
  3. https://java.edu.pl/inne/designPatterns/8.composite.php

--

--