This pattern is used for working with nested-container type of items. It's a uniform way to treat an object
whether it's another container or actually an item that can do some action.
Here's the recipe of all that's needed to implement the pattern:
- General class called "Component" that has basic properties and methods
- Class "Leaf" that accepts an identifier as a parameter (a name in the example case). This class can perform a basic operation that is required (just return its name in our example);
-
Class "Composite" that also takes an identifier as a parameter. It has a few other notable characteristics:
- Tracks a collection of its children items (leafs or composites) rooted under this composite
- Processes additions/removals to this collection
- Primary operation returns its name, as well as triggers a primary operation on all tracked children
The first thing that needs to be done is to extract same functionality between branches and leaves into a class Component.
It defines a parameter for a name, and a method that will work in a Leaf and a Brach class.
class Component { //base class extracting repeating functionality
name: string;
primaryOperation(depth: number): void {}
constructor(name: string) {
this.name = name;
}
}
The next piece of code defines Leaf and Branch classes. Branch class can have many leaves (hence the container for
leaves called "components" and is of type Component[]). Also, it is worth noting the different implementation
of the "primaryOperation" method between the two classes. In the Leaf class, it returns the identifier for the
object it is in. For the Branch class, it also returns an identifier, but it also calls the same method for all
items in its' components container. This is how the pattern is working, method is the same among both types
of objects, but it acts differently between the two types of objects.
class Leaf extends Component {
name: string;
constructor(name: string) {
super(name);
this.name = name;
}
primaryOperation = (depth: number): void =>
console.log(Array(depth).join("-") + this.name);
}
class Branch extends Component {
name: string;
components: Component[];
constructor(name: string) {
super(name);
this.name = name;
this.components = [];
}
primaryOperation(depth: number): void {
console.log(Array(depth).join("-") + this.name);
this.components.forEach((x) => x.primaryOperation(depth + 2));
}
add = (component: Component): number => this.components.push(component);
remove = (component: Component): Component[] =>
(this.components = this.components.filter(
(x) => x.name !== component.name
));
}
Here's some test code and output:
const root = new Branch("root");
root.add(new Leaf("Leaf 1"));
root.add(new Leaf("Leaf 2"));
const comp1 = new Branch("Subtree 1");
comp1.add(new Leaf("Subtree 1 Leaf 1"));
comp1.add(new Leaf("Subtree 1 Leaf 2"));
const comp2 = new Branch("Sub-Subtree 1");
comp2.add(new Leaf("Sub-Subtree 1 Leaf 1"));
comp1.add(comp2);
root.add(comp1);
root.add(new Leaf("Leaf 3"));
root.primaryOperation(1);
/* output:
root
--Leaf 1
--Leaf 2
--Subtree 1
----Subtree 1 Leaf 1
----Subtree 1 Leaf 2
----Sub-Subtree 1
------Sub-Subtree 1 Leaf 1
--Leaf 3
*/
Bonus: Composite with Builder
If you notice in the code above that creates a tree, there's some repetition that
can be automated using the Builder pattern. In short,
our new ConponentTreeBuilder class will have methods to add leafs and composites, as well as let us
navigate between composites on the tree to add/remove elements.
class ComponentTreeBuilder {
rootComponent: Branch;
currentComponent: Branch;
constructor(rootComponentName: string) {
this.rootComponent = new Branch(rootComponentName);
this.currentComponent = this.rootComponent;
}
addComponentItem(name: string): Branch {
let comp = new Branch(name);
this.currentComponent.add(comp);
this.currentComponent = comp;
return comp;
}
addLeaf(name: string): Leaf {
let leaf = new Leaf(name);
this.currentComponent.add(leaf);
return leaf;
}
setCurrentComponent(branchName: string): Branch {
let stack = [];
stack.push(this.rootComponent); //start from the top
while (stack.length > 0) {
let current = stack.pop();
if (current.name === branchName) { //is that the branch we need?
this.currentComponent = current; //set it to current component
return current;
}
let branchesOfCurrent = current.components.filter(
(x) => typeof x.add === "function" //get only branches, no leafs
);
stack.push(...branchesOfCurrent); //work on the next level of branches
}
throw new Error(
`Component name "${branchName}" does not exist in the current hierarchy`
);
}
}
Here's the code that calls the ComponentTreeBuilder to put together a tree. It seems a bit less verbose
than the previous way and it provides for a way to navigate branches easier
const builder = new ComponentTreeBuilder("root");
builder.addComponentItem("left branch");
builder.addLeaf("left branch, leaf 1");
builder.addLeaf("left branch, leaf 2");
builder.setCurrentComponent("root");
builder.addComponentItem("center branch");
builder.addLeaf("center branch, leaf 1");
builder.addLeaf("center branch, leaf 2");
builder.setCurrentComponent("root");
builder.addComponentItem("right branch");
builder.addLeaf("right branch, leaf 1");
builder.addLeaf("right branch, leaf 2");
builder.setCurrentComponent("center branch");
builder.addComponentItem("sub-center branch");
builder.addLeaf("sub-center branch, leaf 1");
builder.rootComponent.primaryOperation(1);
/* output:
root
--left branch
----left branch, leaf 1
----left branch, leaf 2
--center branch
----center branch, leaf 1
----center branch, leaf 2
----sub-center branch
------sub-center branch, leaf 1
--right branch
----right branch, leaf 1
----right branch, leaf 2
*/
Source/Inspiration: C# Design Patterns: Composite on Pluralsight.com