Structural Design Pattern Series | Composite (Object Tree)
In this series we will look at the structural design patterns:
- Composite
- Adapter
- Bridge
- Decorator
- Facade
- Flyweight
- Proxy
🤓 What it is?
Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.
TL;DR: The Composite pattern lets clients treat individual objects and compositions of objects uniformly.
🥰 The why..
Imagine you have a bunch of objects that can be organized into a tree-like structure, with some objects acting as “leaves” and others as “branches”. Now, let’s say you want to perform some operation on all of these objects, but you don’t want to write separate code for each type of object. This is where the Composite design pattern comes in handy!
The Composite pattern allows you to treat individual objects and groups of objects in the same way. This means that you can write one set of code that can handle both individual objects and groups of objects, making your code simpler and more modular.
Additionally, the Composite pattern makes it easy to add or remove objects from the tree structure dynamically, without having to change the code that processes the tree. This makes your code more flexible and easier to maintain in the long run.
It will become clearer in the examples ❤️
🥰 The Structure
The composite pattern consists of three main types of objects:
- Component
A component declares the interface for objects in the composition, and implements default behavior for the interface common to all classes, as appropriate. The base component class can declare an interface for accessing and managing its child components. - Leaf
The leaf represents the end objects of a composition. A leaf has no children; it defines the behavior for primitive objects in the composition. - Composite
A composite defines the behavior for components having children; it stores child components and implements child-related operations in the Component interface.
😎 Examples:
🚙 Example 1
Let’s consider an example of a file system where we have files and directories. Directories can contain files as well as other directories. We can implement the Composite Design Pattern to represent this file system hierarchy.
Here, the composite pattern says that we can work with files and directories through a common interface.
interface FileSystemComponent {
getName(): string;
getSize(): number;
addChild(child: FileSystemComponent): void;
}
class File implements FileSystemComponent {
private name: string;
private size: number;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
getName() {
return this.name;
}
getSize() {
return this.size;
}
addChild(child: FileSystemComponent) {
throw new Error('Cannot add child to a file');
}
}
class Directory implements FileSystemComponent {
private name: string;
private children: FileSystemComponent[] = [];
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
getSize() {
let size = 0;
for (const child of this.children) {
size += child.getSize();
}
return size;
}
addChild(child: FileSystemComponent) {
this.children.push(child);
}
}
We can now use the Composite Design Pattern to traverse the file system hierarchy and perform operations on the files and directories as if they were individual objects. For example, we can calculate the total size of a directory and all its subdirectories as follows:
function getTotalSize(component: FileSystemComponent): number {
let size = component.getSize();
if (component instanceof Directory) {
for (const child of component.children) {
size += getTotalSize(child);
}
}
return size;
}
// Client
const root = new Directory('root');
const folder1 = new Directory('folder1');
const folder2 = new Directory('folder2');
root.addChild(folder1);
root.addChild(folder2);
folder1.addChild(new File('file1', 100));
folder1.addChild(new File('file2', 200));
folder2.addChild(new File('file3', 50));
console.log(getTotalSize(root));
👨🏻💻 Example 2:
Imagine that you are creating an online store. The store sells products and services. Each product or service can be sold individually or as part of a bundle. For example, a customer can buy a book, a music CD, and a video DVD as a bundle. The customer can also buy each of these items individually.
Here, the composite pattern says that we can work with Products and Bundles through a one common interface.
import java.util.ArrayList;
import java.util.List;
// Component
public abstract class Product {
public abstract double getPrice();
}
// Leaf
public class Book extends Product {
private String title;
private double price;
public Book(String title, double price) {
this.title = title;
this.price = price;
}
public double getPrice() {
return price;
}
}
// Leaf
public class MusicCD extends Product {
private String title;
private double price;
public MusicCD(String title, double price) {
this.title = title;
this.price = price;
}
public double getPrice() {
return price;
}
}
// Leaf
public class VideoDVD extends Product {
private String title;
private double price;
public VideoDVD(String title, double price) {
this.title = title;
this.price = price;
}
public double getPrice() {
return price;
}
}
// Composite
public class Bundle extends Product {
private String name;
private List<Product> products = new ArrayList<>();
public Bundle(String name, Product... products) {
this.name = name;
for (Product product : products) {
this.products.add(product);
}
}
public void addProduct(Product product) {
products.add(product);
}
public void removeProduct(Product product) {
products.remove(product);
}
public double getPrice() {
double totalPrice = 0;
for (Product product : products) {
totalPrice += product.getPrice();
}
return totalPrice;
}
}
Now we for the client code
// Client code
public class OnlineStore {
public static void main(String[] args) {
Book book = new Book("The Lord of the Rings", 20.0);
MusicCD musicCD = new MusicCD("Abbey Road", 15.0);
VideoDVD videoDVD = new VideoDVD("The Godfather", 25.0);
Bundle bundle = new Bundle("Entertainment Bundle", book, musicCD, videoDVD);
System.out.println("Total price: " + bundle.getPrice());
}
}
🧐 When to use ?
- When you have a collection of objects that can be structured into a tree hierarchy.
- When you want to be able to work with both individual objects and groups of objects in a consistent manner.
- When you need to be able to add or remove objects dynamically without affecting the overall structure of the tree.
- When you want to simplify client code by providing a unified way of working with complex object structures.
- When you need to increase code reusability and maintainability by providing a modular and extensible solution.
✅ Advantages
- Simplifies client code by treating individual objects and groups of objects uniformly.
- Enables you to add or remove objects from the tree structure dynamically, without affecting the overall structure of the tree.
- Provides a clear and consistent way of working with complex object structures.
- Increases code reusability and maintainability by providing a modular and extensible solution.
❌ Disadvantages
- Can add some complexity to the implementation of the Composite pattern itself.
- May require some additional effort to implement compared to simpler design patterns.
- May not be necessary for simpler object structures that do not have a hierarchical tree-like organization.
🚀 Conclusion
The Composite Design Pattern is a powerful tool for representing tree-like hierarchies of objects. By using classes and interfaces, we can create composite and leaf objects that share a common interface, and work with them as if they were individual objects. This pattern can be useful in a variety of applications, such as file systems, GUI components, and organizational hierarchies.
You can find more explanations and code examples in my repository:
Hope you found this article interesting and fun ❤️