DEV Community

Cover image for The Composite Pattern - Design Patterns Meet the Frontend
Colum Ferry
Colum Ferry

Posted on

The Composite Pattern - Design Patterns Meet the Frontend

The Composite Design Pattern is a structural design pattern with a recursive nature. In this article we will delve into it and hopefully we wont repeat ourselves too much.

We'll go over a few things:

  • What is it? 🤔
  • Let's look at an example 🚀
  • Why do we need it? 😐
  • Let's see some code! 👩‍💻

What is it? 🤔

The Composite Design Pattern is a structual design pattern that is used to represent data and compose objects in the system into a tree-like structure.

It is worth describing to high-level concepts needed to understand how this pattern works.
In our system we will have either Single Objects, or Composite Objects.

Single Objects can be thought of as standalone objects that will implement similar behaviour matching a predefined contract.

Compoite Objects are made up of either Single Objects and/or other Composite Objects.

🤯 Confused yet?

Let's break it down a bit. Let's say we buy a Printer at the store. It comes in a Box. When we open the Box, we see there is a Printer in the Box, but that there is also another Box along side it. This Box contains a Power Lead and a USB Adapter for the Printer.

We can think of the Printer itself as a Single Object, whilst the Box is a Composite Object. It is has a Printer and it has another Box. This nested Box has a Power Lead and a USB Adapter, both Single Objects, making this Box a Composite Object.

Hopefully that has made the concept clearer! ☀️

This structure then allows us to traverse the tree recursively through a single common interface as it allows us to treat single objects and compositions of objects uniformly.

Let's look at an example 🚀

The best way to understand this Pattern is definitely to look at an example of it.

Let us think of an imaginary Task Runner. 🤖

We feed this Task Runner a set of Task Instructions. But each Task Instruction may have Sub Task Instructions, and each Sub Task Instruction might have it's own Sub Task Instructions.

We can see already that this has the potential for being a recursive structure.

We don't necessarily want the Task Runner to have to check at each execution of each Instruction if it is Composite Instruction Set or a Single Instruction.

The Composite Instruction Set should contain a list of children of either Composite Instruction Set or Single Instruction that the Task Runner doesn't need to know about.

Therefore, to tackle this, we would define a common Instruction interface containing an execute() method that the Composite Instruction Set and the Single Instruction implement.

The Task Runner will Iterate through a list of Instructions calling the execute() method.

Single Instructions will execute their custom logic, whilst Composite Instruction Sets will Iterate through their children and call their execute() method.

They don't need to know if their children are Composite or Single Instructions, and the Task Runner also does not need to know the concrete makeup of the Instructions it needs to run, allowing for a lot of flexibility!

Here is a diagram illustrating the example above:

Composite Pattern Example

Why do we need it? 😐

The core problem arises when we have different types of objects that have a similar behaviour or contain children that have similar behaviour.

Type checking before running the required logic isn't desired as it will force the Client code to be tightly coupled to the structure of the objects it is working with to potentially iterate through children if required to do so.

Instead we want our objects themselves to know what their own logic needs to be to perform the action at hand, allowing us to traverse the Tree-like Structure recursively without needing to worry about what type each Leaf node within the Tree is.

Let's see some code! 👩‍💻

Taking our Task Runner example above, let's put it into code.

We need an interface to define common behaviour between Single Instructions and Composite Instructions.

export interface Instruction {
    /**
     * Each instruction should have a name for
     * enhanced reporting and identification
     */
    name: string;

    /**
     * We want our execute method to return wether
     * it was executed successfully or not
     */
    execute(): boolean;
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our interface defined, we will define our SingleInstruction and CompositeInstructionSet classes.

We want our SingleInstructions to be flexbile and extensible to allow developers to create custom instructions the Task Runner can understand.

export abstract class SingleInstruction implements Instruction {
    name: string;

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

    abstract execute(): boolean;
}

export class CompositeInstructionSet implements Instruction {
    // Our composite instruction should have children
    // that can be any implementation of Instruction
    private children: Instruction[] = [];

    name: string;

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

    execute() {
        let successful = false;

        // We'll iterate through our children calling their execute method
        // We don't need to know if our child is a Composite Instruction Set
        // or just a SingleInstruction
        for (const child of this.children) {
            successful = child.execute();

            // If any of the child tasks fail, lets fail this one
            if (!successful) {
                return false;
            }
        }
    }

    // Our CompositeInstructionSet needs a public API to manage it's children
    addChild(child: Instruction) {
        this.children.push(child);
    }

    removeChild(child: Instruction) {
        this.children = this.children.filter(c => c.name !== child.name);
    }
}
Enter fullscreen mode Exit fullscreen mode

For example purposes let's create a Logging Instruction that will always return true, but output a log.

export class LogInstructon extends SingleInstruction {
    log: string;

    constructor(name: string, log: string) {
        super(name);

        this.log = log;
    }

    execute() {
        console.log(`${this.name}: ${this.log}`);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have defined the Structure of our Task Instructions let's create our Task Runner itself.

export class TaskRunner {
    tasks: Instruction[];

    constructor(tasks: Instruction[]) {
        this.tasks = tasks;
    }

    runTasks() {
        for (const task of this.tasks) {
            task.execute();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It's as simple as that! The Task Runner doesn't need to know or care what type of Instruction it is dealing with, so long as it can call it's execute() method, offloading the hard work to the Instructions themselves!

Let's see the code in action.

function main() {
    // Lets start by creating a SingleInstruction and our CompositeInstructionSet
    const startUpLogInstruction = new LogInstructon('Starting', 'Task runner booting up...');
    const compositeInstruction = new CompositeInstructionSet('Composite');

    // Now let's define some sub tasks for the CompositeInstructionSet
    const firstSubTask = new LogInstructon('Composite 1', 'The first sub task');
    const secondSubTask = new LogInstructon('Composite 2', 'The second sub task');

    // Let's add these sub tasks as children to the CompositeInstructionSet we created earlier
    compositeInstruction.addChild(firstSubTask);
    compositeInstruction.addChild(secondSubTask);

    // Now let's create our TaskRunner with our Tasks
    const taskRunner = new TaskRunner([startUpLogInstruction, compositeInstruction]);

    // Finally, we'll ask the TaskRunner to run the tasks
    taskRunner.runTasks();
    // Output:
    // Starting: Task runner booting up...
    // Composite 1: The first sub task
    // Composite 2: The second sub task
}
Enter fullscreen mode Exit fullscreen mode

Hopefully looking at this code has made the power of this particular Design Pattern stand out!
It can be used for all manner of tree-like shaped data systems from shopping carts to delivering packages containing packages!

Isn't that awesome! 🚀🚀🚀


Hopefully you learned a bit (more?) about the Composite Pattern from this article.

If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.

Top comments (5)

Collapse
 
marcelgeo profile image
MarcelGeo

Good Job. ♥️

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
coly010 profile image
Colum Ferry • Edited

Maybe not blog and blog collections as such, but I have definitely seen situations where a Post has Text and Comments, but Comments also have Text and Comments.

Reddit for example.

Both Post and Comments would composite objects in this regard however.

The Text would need to be an object of its own, with no children to fit this pattern.

But the Text may have other data it holds such as likes, reactions etc.

If we ask a Post how many reactions it contains, it can ask it's Text object for its number of reactions, then ask it's comments for its number of reactions, which will iterate through each top level comment, ask it's Text for number of reaction, then it'll ask it's own children for their number of reactions, fulfilling the recursive tree structure of the pattern.

Collapse
 
ad0791 profile image
Alexandro Disla

This article is not for beginners. But i will keep it in mind. Good job

Collapse
 
jwp profile image
John Peters

Nice...