While working on the structure of your application, you may notice that sometimes objects form a complex hierarchy that needs to be considered and worked with. However, it could be tricky to work with all the parts and classes of this structure. To avoid complicating the working process in these situations, you can implement a structural design pattern called composite.
Composite pattern
The composite pattern is a structural design pattern that arranges objects of your application hierarchically. This allows you to create several tree structures which can be treated as individual objects. This pattern is used to deal with a group of objects as a single object. This way, you can represent a whole hierarchy just the same as you would represent its parts.
Hierarchy created through this pattern contains two types of elements:
Composite — elements that store children objects in it. You can think of it as a branch node of our hierarchy tree;
Leaf — finite primitive objects that cannot contain any child objects.
A basic composite hierarchy looks similar to this:
Composite pattern example
As an example of the composite pattern, let's take a look at an ordinary PC. We will create an object called computer and divide it into cabinet and peripheral parts. Both are composite-type objects and contain other parts within them.
For this system to work, we need to work with an interface called component.
The component is an interface that describes common methods for both leaves and composites. This will allow users to work with both leaf and composite parts similarly. This structure allows users to interact with computer parts through a common interface, meaning the computer itself. Usually, when users work with a PC, they do not care about its inner parts and just work with it as a whole object. So, by allowing them to work with its parts through a common interface, you will help them avoid direct interaction with these parts and not burden users with their realization.
Like in this example, structuring objects with a composite pattern, will allow you to interact indirectly with application classes, relieving you from any unnecessary intervention in their realization. However, it can be a little tricky to implement component interfaces due to the divergent functionality of different classes. And without it, you wouldn't be able to use composite patterns.
Composite and component implementation
Now, let's try to depict our computer example in the form of pseudocode. For starters, to implement this pattern, you need to know that your application can be represented by a tree structure. If it can be divided into leaf and composite parts, then it'll do.
First, you will need to create a component interface. It can be represented by ComputerPart:
interface ComputerPart is
method connect()In it, we will only define common methods for composite and leaf classes. In our case, we will just include the connect() method.
Next, we will define leaf and composite classes. Let's start with composite classes. In this case, we will define 3 of them: SystemUnit, Peripherals, and Motherboard. Class SystemUnit contains inner PC hardware, so it will be responsible for the common operations of PC parts. The class Peripherals contains external PC parts like a keyboard and a mouse. Class Motherboard is a composite inside the composite, which contains parts that are connected to a motherboard. So, to implement all of them we need to create SystemComposite class:
//Composite class
class SystemComposite implements ComputerPart is
field parts: array of ComputerPart
constructor of SystemComposite ...
method add(part: ComputerPart) is ...
method connect() is
foreach (part in parts) do
part.connect()Objects of this class can store their leaves and other composites. They will delegate all the work to the leaves below them.
Leaf implementation
Usually, a composite class delegates the actual realization of a program to leaves. So they will simply extend their composite class and do all the actual work. In our case, we will have 5 leaf classes: CPU, RAM, HDD, Mouse, Keyboard. They will all have their own methods but will look similar to their composite classes. So for example we will look at CPU class:
class CPU implements ComputerPart is
constructor of CPU...
method boot() is ...
method connect() is ...
method calculate() is ...In this case, a composite class for this leaf is motherboard which is located inside a composite SystemUnit itself. So, part of the code that uses our objects will look similar to this:
SystemComposite sysUnit = new SystemComposite("SystemUnit")
SystemComposite motherboard = new SystemComposite("Motherboard")
CPU cpu = new CPU()
sysUnit.add(motherboard)
motherboard.add(cpu)
sysUnit.connect()As you can see, we created two composite objects. Composite motherboard is located inside sysUnit, because we added it with sysUnit.add(motherboard). And inside motherboard, there is the leaf CPU. All of these objects share the connect() method, so we can call sysUnit.connect() to use it for all elements in this composite simultaneously. We can also call this method for parts of a composite with motherboard.connect() and cpu.connect().
Conclusion
The composite pattern is a good choice as a tool that helps you form a proper tree structure for your project. If your application can be represented in a tree-form, a pattern will simplify your work with any group or individual objects. But it can be difficult to implement, especially if classes in your system have different functionality.