Composite Design Pattern

The Composite Design Pattern is used to represent part-whole hierarchies using a single consistent interface. This design pattern is a structural design pattern and uses recursive composition, to abstract away the complexities involved in managing the individual as well as the collection of objects. The recursive composition usually yields a tree like structure of all the objects that is being managed. The clients using the pattern can now interact uniformly with both type of objects, using a single set of interface.

Some examples of composite structure are:

  • Directories that can contain files which in turn can be other directories
  • Views that can have sub-views attached to it
  • Shapes that can be composed of other basic shapes
  • Menus that can consist of other menu items layered on top of each other

When should this pattern be used?

The Composite Design Pattern is applicable when we are building an application that involves composing objects, one on top of each other to build complex hierarchies. This would also involve having a single consistent interface to interact with the objects, irrespective of whether it is a Leaf (single) or a composite. This kind of a part-whole structures are seen across multiple domains, and the composite pattern can make it easier to build and manage such object hierarchies.

Class Structure and Participants

An object diagram of the composite pattern can have a structure similar to the one shown below.

Fig 1: Class Structure - Composite Design Pattern

Let us take a look at the participants in this design pattern.

Component: This class contains all the interfaces that are used to interact with the objects. It does not differentiate if the object that is receiving the message is a leaf or a composite. The Component class also defines interfaces to manage the children contained in the relationship.

Leaf: This class represents a single object in the composition. The leaf is the simple most building block in the pattern and does not contain any children. This class declares the interfaces that are used to manipulate leaf objects.

Composite: This class represents the objects that contain other objects within itself. It declares the interfaces that are used to manipulate composite objects. This class manages its children and will usually propagate all messages that it receives, to all its children.

Client: Client code that interacts with the objects used in this pattern.

Example of this Pattern

We shall now look at an example of this pattern for a better understanding. Consider a filesystem and the file abstraction that it provides. The files are usually divided roughly into the following categories:

  • Regular File: These files are made up of a stream of bytes
  • Directory File: These contain files and other directories within them

These whole-part hierarchies can be represented using the composite pattern. The set of classes used to represent the files are shown below.

File class defines an interface for all files. This will act as our Component. The client code will interact only with this class.

The File class declares all operations that are relevant to both the RegularFile and DirectoryFile. Subclasses implement operations specific to them and leave out the rest as unimplemented. The File class contains _name and _permissions as its state and this is shared across its subclasses. We define abstract methods read() and write() to read/write data. add() and remove() methods are also added to add/remove files. The methods described earlier will be implemented by the subclasses. This is a very simple abstraction to think about whole-part hierarchies. We have also defined enum, ReadWriteError for the error that will be returned when we try to read() and write() directories. This behaviour is similar to read and write on Linux. Additional enum, FileType is defined to differentiate between a RegularFile and a DirectoryFile.

RegularFile, a subclass of File, will be represented as,

The RegularFile class maintains its state in a string variable, where new data will be appended to it. The implementation of the methods defined above is as,

And, DirectoryFile, a composite subclass of File, will be represented as,

The DirectoryFile contains the list of files under it using a std::vector<std::unique_ptr<File>>. The implementation of the methods defined above is as,

The classes only implement what is relevant to them and diverge when they need to be implemented differently from each other. For example, the get_size() method is implemented differently for both the RegularFile and DirectoryFile classes. For the DirectoryFile, the get_size() method returns sum of all the size of File objects under it.

The client code will use File pointers to manipulate the leaf and composite objects. This can be like,

Using the composite pattern we are now able to compose complex objects from primitive ones. It is now easy to add another Leaf object and still maintain the structure as before. Here, we add a new DeviceFile to the hierarchy above without changing any of our existing interfaces.

std::unique_ptr<File> dev(
        std::make_unique<DirectoryFile>("/dev", 700)
)
std::unique_ptr<File> tty(
        std::make_unique<DeviceFile>("/dev/tty", 644)
);
dev->add(std::move(tty));

Benefits of this pattern

  • The primitive objects can be composed into more complex objects and so on recursively
  • Clients can treat the Leaf and the Composite classes uniformly and hence the client code becomes simple. Clients should normally not bother if they are dealing with a Leaf or Composite. This pattern avoids unnecessary branches in the client code

Implementation Notes

  • Defining as many common operations in the Component class seems overkill but this is needed for the client to treat both the Leaf and Composite classes uniformly
  • Ordering of children might or might not be important to your use cases. You need to take this into the design
  • You need to decide on the child ownership. Shared references or an exclusive reference will suit your application based on your needs/use-case

That’s it.

For any discussion, tweet here.

You can download the C++ project, that contains various design patterns featured in this blog, here.