02-04: Composite Design Pattern
This article Composite Design Pattern: An agile story was originally posted on my blog.
Type: Structural
Scope: Object
Symptoms: Hierarchical Data
Intent
Composite design pattern is a structural design pattern which defines part-whole relationship between objects within a tree structure.
In a complex tree structure, it's a challenge to discriminate between a leaf and branch. We need to have type check before performing any operation on the tree structure and complexity grows if we have varied types of leaf and branches. This leads to higher maintenance.
A composite design pattern helps simplify above mentioned issue by introducing a common interface which needs to be implemented by leaf and branches thereby generalizing the solution to a problem.
Components
There are 3 main components in a composite pattern
1. Component: This is the abstraction/interface for all the elements in a tree. It includes both leaf and branches. The operations performed on all the elements is defined here and this is how it eliminates the need for a type checking. The client instantiates the component class and doesn't need to know what is going inside the class.
2. Leaf: It represents the leaf element in a tree and doesn't contain any child elements. It also implements all the operations defined in a component.
3. Composite: It is composed of multiple elements as child. It does implement all the operations defined in a component and mostly delegate the execution to child elements.
Implementation
Example
Domain: Agile-Scrum
Problem Statement: We need to find a way to calculate the total story points for a project
In a typical scrum project, a project contains Epics and User stories. A project may contain a user story directly also without an Epic.
We are going to calculate total story points(34) for below hierarchy/tree structure
Solutions
Solution 1: No Pattern
We have three classes(Project, Epic and UserStory). Project contains a list of Epic and User Story. Also, there are different methods for adding/removing each type(Epic/UserStory) such as AddEpic() and AddUserStory(). This makes the class fragile and for any new type introduced, we need to introduce corresponding Add/Remove methods.
class NoComposite
{
public void Run()
{
/*
* Project
* |
* |-----Epic-1
* | |
* | |----US-1(5)
* | |
* | |----US-2(8)
* |
* |-----Epic-2
* | |
* | |----US-3(3)
* | |
* | |----US-4(8)
* |
* |
* |----US-5(2)
* |
* |----US-6(8)
*
* Total story points in a project: 34
*/
// User stories
var us1 = new UserStory(5);
var us2 = new UserStory(8);
var us3 = new UserStory(3);
var us4 = new UserStory(8);
var us5 = new UserStory(2);
var us6 = new UserStory(8);
// Epics
var epic1 = new Epic();
epic1.AddUserStory(us1);
epic1.AddUserStory(us2);
var epic2 = new Epic();
epic2.AddUserStory(us3);
epic2.AddUserStory(us4);
// Project
var project = new Project();
project.AddEpic(epic1);
project.AddEpic(epic2);
project.AddUserStory(us5);
project.AddUserStory(us6);
// Calculate total sum, it should be 34
var result = project.CalculateStoryPoints();
Console.WriteLine($"Total number of story points in a Project is {result}");
}
class UserStory
{
private int _storyPoints;
public UserStory(int storyPoints)
{
_storyPoints = storyPoints;
}
public int CalculateStoryPoints()
{
return _storyPoints;
}
}
class Epic
{
private readonly List<UserStory> _userStories;
private int _storyPoints;
public Epic()
{
_userStories = new List<UserStory>();
}
public int CalculateStoryPoints()
{
foreach (var userStory in _userStories)
{
_storyPoints += userStory.CalculateStoryPoints();
}
return _storyPoints;
}
public void AddUserStory(UserStory userStory)
{
_userStories.Add(userStory);
}
public void RemoveUserStory(UserStory userStory)
{
_userStories.Remove(userStory);
}
}
class Project
{
private readonly List<UserStory> _userStories;
private readonly List<Epic> _epics;
private int _storyPoints;
public Project()
{
_userStories = new List<UserStory>();
_epics = new List<Epic>();
}
public int CalculateStoryPoints()
{
// Add story points from all the user stories
foreach (var userStory in _userStories)
{
_storyPoints += userStory.CalculateStoryPoints();
}
// Add story points from all the epics
foreach (var epic in _epics)
{
_storyPoints += epic.CalculateStoryPoints();
}
return _storyPoints;
}
public void AddUserStory(UserStory userStory)
{
_userStories.Add(userStory);
}
public void RemoveUserStory(UserStory userStory)
{
_userStories.Remove(userStory);
}
public void AddEpic(Epic epic)
{
_epics.Add(epic);
}
public void RemoveEpic(Epic epic)
{
_epics.Remove(epic);
}
}
}
var noComposite = new NoComposite();
noComposite.Run();
Total number of story points in a Project is 34
Issues with above solution
- Tight coupling between Project and Epic/UserStory
- Add/Remove methods for each type(Epic/UserStory)
- Open Closed Principle(OCP) violation. If a new type(Features) gets introduced, we need to modify Project class implementation and write Add/Remove methods separately.
- For a new type, there will be addition of new type in CalculateStoryPoints() method.
Solution 2 : With Composite Pattern
As seen above, this is a general solution with no coupling. IScrumComponent is the interface to be implemented by all elements within a tree.
Add/Remove methods have been generalized as well. If a new type gets introduced, it just needs to be inherited from IScrumComponent and everything will work.
interface IScrumComponent
{
int CalculateStoryPoints();
}
class WithComposite
{
public void Run()
{
/*
* Project
* |
* |-----Epic-1
* | |
* | |----US-1(5)
* | |
* | |----US-2(8)
* |
* |-----Epic-2
* | |
* | |----US-3(3)
* | |
* | |----US-4(8)
* |
* |
* |----US-5(2)
* |
* |----US-6(8)
*
* Total story points in a project: 34
*/
// User stories
var us1 = new UserStory(5);
var us2 = new UserStory(8);
var us3 = new UserStory(3);
var us4 = new UserStory(8);
var us5 = new UserStory(2);
var us6 = new UserStory(8);
// Epics
var epic1 = new Epic();
epic1.AddComponent(us1);
epic1.AddComponent(us2);
var epic2 = new Epic();
epic2.AddComponent(us3);
epic2.AddComponent(us4);
// Project
var project = new Project();
project.AddComponent(epic1);
project.AddComponent(epic2);
project.AddComponent(us5);
project.AddComponent(us6);
// Calculate total sum, it should be 34
var result = project.CalculateStoryPoints();
Console.WriteLine($"Total number of story points in a Project is {result}");
}
class UserStory : IScrumComponent
{
private int _storyPoints;
public UserStory(int storyPoints)
{
_storyPoints = storyPoints;
}
public int CalculateStoryPoints()
{
return _storyPoints;
}
}
class Epic : IScrumComponent
{
private readonly List<IScrumComponent> _components;
private int _storyPoints;
public Epic()
{
_components = new List<IScrumComponent>();
}
public int CalculateStoryPoints()
{
foreach (var component in _components)
{
_storyPoints += component.CalculateStoryPoints();
}
return _storyPoints;
}
public void AddComponent(IScrumComponent component)
{
_components.Add(component);
}
public void RemoveComponent(IScrumComponent component)
{
_components.Remove(component);
}
}
class Project
{
private readonly List<IScrumComponent> _components;
private int _storyPoints;
public Project()
{
_components = new List<IScrumComponent>();
}
public int CalculateStoryPoints()
{
// Add story points from all the user stories
foreach (var component in _components)
{
_storyPoints += component.CalculateStoryPoints();
}
return _storyPoints;
}
public void AddComponent(IScrumComponent component)
{
_components.Add(component);
}
public void RemoveComponent(IScrumComponent component)
{
_components.Remove(component);
}
}
}
var withComposite = new WithComposite();
withComposite.Run();
Total number of story points in a Project is 34
Comparison - Class Diagrams
Conclusion
Composite design pattern is very useful in simplifying the complexity involved in a hierarchial/tree structure. It allows adding new types into the system without changing the client code and this makes the system very extensible. As a result, the maintenance is reduced to a great extent.
Source: https://github.com/praveenraghuvanshi/design-patterns/tree/master/02-Structural/02-04-composite
Please share your feedback as it'll encourage me to write more.
Twitter: @praveenraghuvan
Github: praveenraghuvanshi
Blog: praveenraghuvanshi.github.io
Top comments (0)