Patterns. The Composite pattern. Implementation in C++

The Composite pattern. Implementation in C++


Contents


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.

Pattern Composite. Structure diagram

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).

The Composite pattern. Structure of a typical composite object aComposite

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.

The Composite pattern. Tree of objects implemented in client code

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