The Composite pattern. Implementation in C++
Contents
- 1. General information about the Composite pattern. Purpose. Pattern diagram
- 2. Relations
- 3. Using
- 4. Results
- 5. Implementation
- 6. Example implementation of a pattern scheme in C++
- Related topics
1. General information about the Composite pattern. Purpose. Pattern diagram
The Composite pattern refers to patterns that structure objects. The pattern is intended for arranging objects in a hierarchy of tree structures. Nodes of a tree structure can contain both leaf and composite objects. This structure of objects is called part-whole.
The general diagram of the pattern is shown in Figure 1.
Figure 1. Diagram of the Composite pattern
Figure 2 shows an example of aComposite object. As you can see from the figure, the aComposite object is composite, and the aLeaf object is individual (finite).
Figure 2. Structure of a typical composite object aComposite
According to Figure 1, the participants of the Composite pattern are the following.
1. Component class – implements the interface with the client.
The class provides:
- an interface for objects that need to be composed;
- defines a list of operations (methods) by default, common to all classes;
- defines interface for inherited classes;
- defines an interface for parent components in a recursive structure with the ability to implement it.
2. Class Leaf – leaf.
This class has the following uses:
- represents the leaf (end) nodes of a tree. These nodes have no children and are composition nodes;
- determines the behavior of individual objects in a composition.
3. The Composite class – represents a composite object.
The class has the following uses:
- sets the behavior of components that do not have children;
- saves child components;
- implements part of the operations in the Component class that manage descendants.
4. The Client class is a client class that operates composition objects through the Component interface.
⇑
2. Relations
To interact with objects in the tree structure, clients use the Component class interface. Queries are made both to leaf (individual) objects and to composite objects. When a request is made to a leaf object, the Leaf object processes the request. If a request is made to a Composite object, it forwards the request to its descendants.
⇑
3. Using
The Composite pattern is used in the following cases:
- when you need to represent the hierarchy of objects in the form of a part-whole;
- when clients need to present composite and individual (final) objects that are interpreted uniformly.
⇑
4. Results
For the Composite pattern, the following positive results can be identified, which are advantages:
- the pattern defines hierarchies of classes consisting of simple (individual, finite) objects, as well as composite objects. Simple objects form compound objects;
- the client architecture is simplified. Clients can work with both individual objects and composite objects in the same way. The client does not know which object he is working with: compound or simple. As a result, the client code is simplified (there is no need to write additional code to determine the complexity of a particular object);
- adding new types of components is easy. You can automatically add subclasses of the Composite and Leaf classes to existing structures.
The disadvantage of the pattern is that it is difficult to impose restrictions on objects. This applies to cases where only a certain set of components need to be included in a composite object.
⇑
5. Implementation
When implementing the Composite pattern, it is important to note the following implementation features:
- the need (or absence) of storing explicit references to their parents (parent classes). If there is a reference to the parent class, it makes navigation easier. The link also makes it possible to implement the Chain of Responsibility pattern;
- it may be necessary to separate components. In this case, components need to correctly reference their parent classes to avoid ambiguity;
- adding the largest number of methods to the Component class in order to cover more functionality;
- ensuring security, which is carried out by introducing additional checks into the classes that form composite and primitive objects.
⇑
6. Example implementation of a pattern scheme in C++
The code below implements the Composite pattern, the structure of which is shown in Figure 1 (see point 1).
In a composite object, the list of descendants is implemented by the list<T> class from the C++ Standard Library.
The main() function acts as a client, implementing the tree of objects shown in Figure 3.
Figure 3. Tree of objects implemented in client code
The program text in C++ is as follows.
// The Composite pattern #include <iostream> #include <list> using namespace std; class Composite; // Implementation of structure class Component { protected: public: // Constructor Component() { } // Pure virtual function virtual void Operation() = 0; // Method that adds an element - no need to do anything virtual void Add(Component* _comp) = 0; // Removes an element from the tree - this is interface only virtual void Remove(Component* _comp) = 0; // return the descendant number (starting from 0) - for the Composite class virtual Component* GetChild(int _child) = 0; // Determines whether a component is composite (Component) virtual Composite* GetComposite() { return nullptr; } }; // The Composite class - can have descendants class Composite :public Component { private: // list of descendants list<Component*> L; public: // Constructor - does not save data Composite() : Component() { L.clear(); } // There is an optional operation here void Operation() override { cout << "Composite::Operation()" << endl; } // Add component to list void Add(Component* _comp) override { L.push_back(_comp); } // Removing component void Remove(Component* _comp) { // Declare an iterator for the list L list<Component*>::iterator it = L.begin(); // Search for the _comp element while (it != L.end()) { if (*it == _comp) break; it++; } // Remove the element if it exists if (it != L.end()) { L.erase(it); } } // Returns the current component Composite* GetComposite() override { return this; } // Display list of descendants void PrintChild() { list<Component*>::iterator it = L.begin(); while (it != L.end()) { (*it)->Operation(); it++; } } // Rotate a component by its number in a node, // number starts from 0 Component* GetChild(int _child) override { list<Component*>::iterator it = L.begin(); int i = 0; while ((i < _child) && (it != L.end())) { i++; it++; } return *it; } }; // Leaf class - no descendants class Leaf : public Component { private: // Leaf class data string data; public: // Constructor Leaf(string _data) : data(_data) { } void Operation() override { cout << "Leaf.data = " << data << endl; } private: // Hide methods Add(), Remove() // you don't need to do anything here void Add(Component* _comp) override { }; // you don't need to do anything here either void Remove(Component* _comp) override { }; // there is no need to do anything here Component* GetChild(int _child) override { return nullptr; } }; void main() { /* Create a tree Composite1 -----> Leaf1 | |--> Composite2 --> Leaf2 | | | --> Leaf3 | ---> Leaf4 */ // Client Component* client = nullptr; // Create the top node Component* composite1 = new Composite; // Create leaflets Leaf1, Leaf2, Leaf3, Leaf4 Leaf* leaf1 = new Leaf("Leaf1"); Leaf* leaf2 = new Leaf("Leaf2"); Leaf* leaf3 = new Leaf("Leaf3"); Leaf* leaf4 = new Leaf("Leaf4"); // Create intermediate node Composite* composite2 = new Composite; // Add the top branch composite1->Add(leaf1); // Create a middle branch composite2->Add(leaf2); composite2->Add(leaf3); // Create a middle branch composite1->Add(composite2); // Add bottom branch composite1->Add(leaf4); // Set client to composite1 client = composite1; // Display composite1 level ((Composite*)client)->PrintChild(); // Display composite2 level cout << "------------------" << endl; ((Composite*)composite2)->PrintChild(); // Remove the branches leaf2 and leaf4 and display the tree again composite1->Remove(leaf4); composite2->Remove(leaf2); cout << "-------------------------" << endl; cout << "-------------------------" << endl; ((Composite*)composite1)->PrintChild(); cout << "-------------------------" << endl; composite2->PrintChild(); // -------------------------------------- // Exploring the GetComposite() method Component* resComposite = leaf1->GetComposite(); if (resComposite != nullptr) cout << "leaf1 => Composite" << endl; else cout << "leaf1 => Leaf" << endl; resComposite = composite2->GetComposite(); if (resComposite) cout << "composite1 => Composite" << endl; else cout << "composite1 => Leaf" << endl; // ---------------------------------------- // Release memory for all components if (leaf1) delete leaf1; if (leaf2) delete leaf2; if (leaf3) delete leaf3; if (leaf4) delete leaf4; if (composite1) delete composite1; if (composite2) delete composite2; }
⇑
Related topics
- Pattern Adapter. Review and research. Examples of implementation in C++
- Bridge pattern. Implementation of structure in C++
- Facade pattern. Implementation of structure in C++
⇑