Menu Close

Object Oriented Design Pattern: Composite

Explanation

The Composite design pattern is a structural pattern that makes it able to nest objects inside each other. Pretty much in the same way as a file system like Windows explorer works, where a directory can contain files but also other directories which can contains files and directories, ad infinitum. Menu systems and GUI libraries tend to use this design patterns as well.

An analogy out of the real world, would be carton storage boxes which may contain products (or papers) but also other other storage boxes with products.

Use cases

  • You want to organize objects into an hierarchical way.
  • You want to create a structure that works like a tree, that branches out.

Pitfalls

  • If too many objects are nested, it can give performance problems.
  • The recursive nature of the design pattern can be complex.

Example

Suppose we want to create a project management system where a project has Task Groups with Tasks. Each Task Group can consist out of several Task and Task Groups.

Class Diagram

Class diagram for the composition pattern

The class setup for the pattern is quite simple, the Component class is the parent for both Task and TaskGroup which can have a name.

namespace DesignPatterns.Composite
{
    public abstract class Component
    {
        public string Name { get; set; }

        public Component(string name = "")
        {
            this.Name = name;
        }
    }
}
C#

The Task class needs nothing in it besides the constructor for this bare example:

namespace DesignPatterns.Composite
{
    public class Task : Component
    {
        public Task(string name) : base(name)
        {
        }
    }
}
C#

The TaskGroup holds a List with components which can either be a Task or a TaskGroup. It has a method to add either of those to the list.

namespace DesignPatterns.Composite
{
    public class TaskGroup : Component
    {        

        public List<Component> Components { get; set; } = new List<Component>();

        public TaskGroup(string name = "") : base(name)
        {
        }

        public void AddComponent(Component component)
        {
            Components.Add(component);
        }
    }
}
C#

So far it’s pretty straightforward so far for a bare minimum of the implementation. Thereupon we can utilize the Program class to use the pattern in the following way.

using DesignPatterns.Composite;
using Task = DesignPatterns.Composite.Task;

class Program
{
    static void Main(string[] args)
    {
        TaskGroup tree = new TaskGroup("Main");

        tree.AddComponent(new Task("Task 1"));

        TaskGroup dir1 = new TaskGroup("Directory 1");
        dir1.AddComponent(new TaskGroup("Directory 1.1"));
        dir1.AddComponent(new Task("Task 1A"));
        dir1.AddComponent(new Task("Task 1B"));
        dir1.AddComponent(new Task("Task 1C"));
        tree.AddComponent(dir1);

        TaskGroup dir2 = new TaskGroup("Directory 2");
        dir2.AddComponent(new Task("Task 2"));
        dir2.AddComponent(new Task("Task 2a"));
        tree.AddComponent(dir2);

        PrintTree(tree);
    }
    
    private static void PrintTree(Component component, int depth = 0)
    {
        PrintSpaces(depth);

        if (component is Task)
        {
            Console.WriteLine(" - " + component.Name);
        }
        else if (component is TaskGroup)
        {                        
            TaskGroup taskComponent = (TaskGroup)component;
            Console.WriteLine(" - " + component.Name + " holds: ");
            depth++;
            taskComponent.Components.ForEach(c => {                
                PrintTree(c, depth);
            });
        }
    }
    
    private static void PrintSpaces(int n)
    {
        for (int i = 0; i < n; i++)
        {
            Console.Write("  ");
        }
    }
}
C#

This will recursively print out:

 - Main holds:
- Task 1
- Directory 1 holds:
- Directory 1.1 holds:
- Task 1A
- Task 1B
- Task 1C
- Directory 2 holds:
- Task 2
- Task 2a

(Directory 1.1 actually holds nothing inside)

A randomized fill

To automate and randomize the fill we could write another method, this is the altered Program class:

using DesignPatterns.Composite;
using Task = DesignPatterns.Composite.Task;

class Program
{

    static void Main(string[] args)
    {
        TaskGroup tree = new TaskGroup("Main");
        RandomAddTasksAndTaskGroups(tree, 6);
        PrintTree(tree);
    }

    private static void RandomAddTasksAndTaskGroups(TaskGroup parent, int amount, int iter = 0)
    {
        for (int i = 0; i < amount; i++)
        {            
            int r = new Random().Next();
            if (r % 2 == 0)
            {
                parent.AddComponent(new Task($"Task {r} iteration {iter}"));
            }
            else
            {
                TaskGroup taskGroup = new TaskGroup($"Group {r} iteration {iter}");
                parent.AddComponent(taskGroup);
                RandomAddTasksAndTaskGroups(taskGroup, --amount, ++iter);
            }
        }
    }

    private static void PrintTree(Component component, int depth = 0)
    {
        PrintSpaces(depth);

        if (component is Task)
        {
            Console.WriteLine(" - " + component.Name);
        }
        else if (component is TaskGroup)
        {                        
            TaskGroup taskComponent = (TaskGroup)component;
            Console.WriteLine(" - " + component.Name + " holds: ");
            depth++;
            taskComponent.Components.ForEach(c => {                
                PrintTree(c, depth);
            });
        }
    }

    private static void PrintSpaces(int n)
    {
        for (int i = 0; i < n; i++)
        {
            Console.Write("  ");
        }
    }
}
C#

Which will print something like:

 - Main holds:
- Task 250865174 iteration 0
- Group 971857745 iteration 0 holds:
- Group 1100164475 iteration 1 holds:
- Group 1861743321 iteration 2 holds:
- Task 331176128 iteration 3
- Task 1791571910 iteration 3
- Task 237986552 iteration 3
- Group 606163025 iteration 3 holds:
- Group 658879687 iteration 4 holds:
- Group 569930811 iteration 5 holds:
- Task 243750304 iteration 2
- Task 2121723596 iteration 2
- Task 11196408 iteration 2
- Task 901766000 iteration 1
- Group 674928019 iteration 1 holds:
- Task 642463348 iteration 2
- Group 40743609 iteration 2 holds:
- Task 256288708 iteration 3
- Group 768642849 iteration 3 holds:
- Task 1982158458 iteration 4
- Group 895528113 iteration 4 holds:
- Group 34698039 iteration 5 holds:
- Task 1335523034 iteration 3

Conclusion

The composition design pattern is best used when you need a tree structure. This specific example lacks certain methods, such as RemoveAt(), ListTasks() and other sorts of functionality which makes maintaining the structure easier.

References

Freeman, E., Bates, B., & Sierra, K. (2004). Head first design patterns.

Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.

Wikipedia contributors. (2024, 10 september). Software design pattern. Wikipedia. https://en.wikipedia.org/wiki/Software_design_pattern

Related Posts