The Composite Pattern is a structural design pattern that allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. This pattern is particularly useful when dealing with complex systems where parts can be combined in various ways.
In the Composite Pattern, there are two main types of components:
The key idea is that you can treat both individual objects (leaf) and compositions of objects (composite) uniformly through a common interface. This simplifies the client code, as it doesn't need to differentiate between leaf and composite nodes.
Let's illustrate the Composite Pattern with an example where we have a simple file system structure consisting of files and directories.
First, we define a common interface for both files and directories. This interface will declare methods that are applicable to all components in the tree structure.
1interface Component {2getName(): string;3getSize(): number;4add(component: Component): void; // Only relevant for Composite5remove(component: Component): void; // Only relevant for Composite6}
Next, we implement the Leaf component, which represents a file. Files do not have children.
1class File implements Component {2private name: string;3private size: number;45constructor(name: string, size: number) {6this.name = name;7this.size = size;8}910getName(): string {11return this.name;12}1314getSize(): number {15return this.size;16}1718add(component: Component): void {19throw new Error("Cannot add to a file");20}2122remove(component: Component): void {23throw new Error("Cannot remove from a file");24}25}
Now, we implement the Composite component, which represents a directory. Directories can contain both files and other directories.
1class Directory implements Component {2private name: string;3private components: Component[] = [];45constructor(name: string) {6this.name = name;7}89getName(): string {10return this.name;11}1213getSize(): number {14let totalSize = 0;15for (const component of this.components) {16totalSize += component.getSize();17}18return totalSize;19}2021add(component: Component): void {22this.components.push(component);23}2425remove(component: Component): void {26const index = this.components.indexOf(component);27if (index !== -1) {28this.components.splice(index, 1);29}30}31}
Finally, we can use the components to create a file system structure and calculate the total size of directories.
1const root = new Directory("root");2const documents = new Directory("documents");3const pictures = new Directory("pictures");45const report = new File("report.pdf", 1024);6const photo = new File("photo.jpg", 2048);78documents.add(report);9pictures.add(photo);10root.add(documents);11root.add(pictures);1213console.log(`Total size of root: ${root.getSize()} bytes`);
Total size of root: 3072 bytes
In this example, both File and Directory implement the Component interface. The Directory class can contain other Component objects, allowing for a hierarchical structure. This uniformity allows clients to treat individual files and directories in the same way.
Next up, we will explore the Decorator Pattern, which allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class.