Originally posted here
The Composite is a structural design pattern that allows us to compose objects into tree structures and work with these structures as if they were individual objects.
The Composite represents part-whole hierarchies. This is a fancy way of saying that we can represent all parts of a hierarchy by reducing the pieces of said hierarchies down to common components.
You can find the example code of this post, on GitHub
Conceptualizing the Problem
Using the Composite pattern makes sense only when the core model of our application can be represented as a tree hierarchy.
Imagine that we are creating our unique flavour of a filesystem. We can have two types of objects: Files
and Folders
. A Folder
can contain several Files
as well as several other Folders
. These folders can also hold some Files
or even other Folders
, and so on.
Say we decide to create a function that will calculate the size of a directory. A directory can contain a single File
without any Folders
, as well as Folders
stuffed with Files
...and other Folders
. How would we determine the total size of such a directory?
We could try the direct approach: Move all the files out of their folders, go over each file and then calculate the total size. That would be doable in the real world; just remove the files and count the pages for example.
However, in the digital world, this is not as simple as running a loop. We have to know the classes of File
s and Folder
s we are going through, the nesting level of the Folders
and other nasty details beforehand. All of these details can make the direct approach either too awkward or simply impossible to implement.
The Composite pattern suggests that we work with Files
and Folders
through a common interface. This interface will declare a method for calculating the total size.
How would this method work? For a file, it'd simply return the file's size. For a folder, it'd go over each item the folder contains, ask its size and then return the total of the folder. If one of these items is another folder, that folder would also start going over its contents and so on, until the sizes of all inner components were calculated.
The greatest benefit of this approach is that we don't need to care about the concrete classes that compose the tree. We don't need to know whether the object is a simple product or a sophisticated box. We can treat them all the same via the common interface. When we call the method, the objects themselves pass the request down the tree.
Structuring the Composite Pattern
In this basic implementation, the Composite pattern has 4 participants:
- Component: The Component interface describes operations that are common to both simple and complex elements of the tree.
- Leaf: The Leaf is a basic element of the tree that doesn't have child elements. Usually, leaf components end up having most of the business logic, since they don't have anyone to delegate the work to.
- Container: The Container, also known as Composite is an element that has child elements. A container doesn't know the concrete implementations of its children. It works with all the children only via the component interface. Upon receiving a request, a container delegate the work to its children, processes intermediate results and then returns the final result to the client.
- Client: The Client works with all elements through the component interface. As a result, the client can work in the same way with both simple or complex elements of the tree.
To demonstrate how the Composite pattern works, we are going to create our very own Nuka-Cola Mixer.
For all those people out there, who are not fans of the Fallout 4 game, a Nuka Mixer Station is a machine that allows you to mix different Nuka-Cola flavours to create a new Nuka-Cola, similar to these dispensers.
We can model how this dispenser works. The Nuka-Mixer can accept 5 types of components: Regular Nuka Cola, Flavoured Nuka Cola, Variant Nuka Cola, Meds and Foodstuff. In effect, this creates a hierarchy where "Soda" itself is the root component, the component types are the child Components, and the different flavours are the leaves.
A simplified version of this hierarchy might look like this:
Let's model the hierarchy. For all possible flavours of soda that our machine can dispense, we need to know how many hit points each flavour provides. So, in our abstract class that represents all soft drinks, we need a property for HitPoints
.
namespace Composite
{
/// <summary>
/// Component abstract class
/// </summary>
public abstract class Component
{
public int HitPoints { get; set; }
public List<Component> Flavors { get; set; }
public Component(int hitPoints)
{
HitPoints = hitPoints;
Flavors = new List<Component>();
}
/// <summary>
/// Return all available flavours and their hitpoints
/// </summary>
public void DisplayHitPoints()
{
foreach(var drink in this.Flavors)
{
drink.DisplayHitPoints();
this.HitPoints += drink.HitPoints;
}
Console.WriteLine($"{this.GetType().Name}: {this.HitPoints} hitpoints.");
}
}
}
Note the DisplayHitPoints
method. Is a recursive method that will show the hitpoints of all components, and then display the total hitpoints of the composite object.
Next up, we need to implement several Leaf participants for the concrete Nuka-Cola flavors.
namespace Composite.Leaves
{
/// <summary>
/// Leaf class
/// </summary>
public class NukaCola : Component
{
public NukaCola(int hitPoints) : base(hitPoints)
{
}
}
}
namespace Composite.Leaves
{
public class NukaGrape : Component
{
public NukaGrape(int hitPoints) : base(hitPoints)
{
}
}
}
namespace Composite.Leaves
{
public class NukaCherry : Component
{
public NukaCherry(int hitPoints) : base(hitPoints)
{
}
}
}
namespace Composite.Leaves
{
public class NukaWild : Component
{
public NukaWild(int hitPoints) : base(hitPoints)
{
}
}
}
We now need to implement the Composite participant, which represents the hierarchy which has children.
namespace Composite.Composites
{
public class NewkaCola : Component
{
public NewkaCola(int hitPoints) : base(hitPoints)
{
}
}
}
namespace Composite.Composites
{
public class NukaBerry : Component
{
public NukaBerry(int hitPoints) : base(hitPoints)
{
}
}
}
namespace Composite.Composites
{
public class NukaFancy : Component
{
public NukaFancy(int hitPoints) : base(hitPoints)
{
}
}
}
The Composite classes are the same as the Leaf classes, and this is not an accident.
Finally, our Main()
shows how we might initialize a new cola hierarchy with several flavours and then display all of the calories for each flavour:
using Composite.Composites;
using Composite.Leaves;
var newkaCola = new NewkaCola(0);
newkaCola.Flavors.Add(new NukaCola(150));
newkaCola.Flavors.Add(new NukaCherry(150));
var nukaBerry = new NukaBerry(0);
nukaBerry.Flavors.Add(new NukaCola(150));
nukaBerry.Flavors.Add(new NukaCherry(150));
nukaBerry.Flavors.Add(new NukaGrape(100));
var nukaFancy = new NukaFancy(0);
nukaFancy.Flavors.Add(new NukaWild(50));
nukaFancy.Flavors.Add(new NukaCherry(150));
newkaCola.DisplayHitPoints();
Console.WriteLine("--------------------------");
nukaBerry.DisplayHitPoints();
Console.WriteLine("--------------------------");
nukaFancy.DisplayHitPoints();
The output of this method is:
![[composite-output.png]]
Forms of the Composite Pattern
There are many different forms of the Composite pattern, but at a high level, they can be grouped into two categories: Bill of Material and Taxonomy.
Bill of Material
A Bill of Material form captures the relationship that exists between a complex object and its parts. For example, a car could be said to be composed of a body and a chassis. The body is further made up of fenders, doors, a trunk, a hood etc. The chassis is further composed of a drive train and suspension.
If we are implementing a Bill of Material we will have many options when implementing its behaviour, each of which will have different implications.
- Aggregating Behavior: If we need to determine the weight of the car, for example, we need to aggregate the weights of all the subparts recursively. Often, this behaviour can be hidden from consuming client entities in the way Component nodes are implemented.
- Best/Worst Case Behavior: If we need to determine the overall health of the car, the rule might be that the car is in a healthy state, if and only if all of its parts are healthy. If any one part has a problem, we might say that the whole car has a problem. Thus, we can say that the car is the worst case of all its parts. Again, this behaviour can often be hidden from the consuming class in the way Components are implemented.
- Investigatory: If we need to determine something specific about a part, then we might need to allow the client to iterate through the parts looking for those that qualify.
Taxonomy
A Taxonomy form captures the logical relations that exist between more or less general types. The biological classes work this way. Tetrapoda include the Amphibia, the Reptilia and the Mammalia. The Mammalia are further subdivided into the Prototheria and the Theria and so on.
When we are implementing a Taxonomical composite, our motivation is almost certainly to allow the client to traverse the structure, find information or classify an entity within the context of the existing structure.
Iteration Options
The key decision to make when creating a Composite, which is whether or not to put methods into the target abstraction that allow for the client entity to traverse the Composite. These traversal methods create coupling between the client and the components, and should not be included unless they are needed.
In a Bill of Material, traversal methods may or not be needed, depending on the behaviour desired.
In a Taxonomy, traversal methods are almost always needed, since the purpose of the Composite is for the client to traverse it.
Pros and Cons of Composite Pattern
✔ We can work with complex tree structures more conveniently. We can use polymorphism and recursion to our advantage. | ❌It might be difficult to provide a common interface for classes whose functionality differs too much. In certain scenarios, we’d need to overgeneralize the component interface, making it harder to comprehend. |
✔ We can introduce new element types into the application without breaking the existing code, which now works with the object tree. This respects the Open-Closed Principle. | ❌ We need to take into account what form of composite we are working with. This will determine whether we are going to implement traversal methods for our composite. |
Relations with Other Patterns
- The Builder pattern can be used for creating complex Composite trees because we can program its construction steps to work recursively.
- The Chain of Responsibility pattern is often used in conjunction with the Composite pattern. in this case, when a leaf component gets a request, it may pass it through the chain of all the parent components down to the object tree.
- We can use Iterators to traverse Composite trees.
Final Thoughts
In this article, we have discussed what is the Composite pattern, when to use it and what are the pros and cons of using this design pattern. We then examined what are the different forms of the Composite and how the Composite pattern relates to other classic design patterns.
It's worth noting that the Component pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (1)
Good explanation, however in the implementation example I have a few notes: