In the previous article we spoke about Adapter Pattern. Today we will cover the second design pattern from the Structural category; the Composite Pattern.
Agenda
- What is Composite Pattern?
- When to use Composite Pattern. We will see an example and try to understand the problems with traditional approaches.
- How does a Composite Pattern solve this problem?
- The various components involved in the composite pattern.
- Implement a Composite Pattern using a .Net console based application.
- Class Diagram.
Previous Articles
- DesignPatterns: Introduction
- LearnDesign Pattern - Singleton Pattern
- LearnDesign Pattern - Factory Method Pattern
- LearnDesign Pattern - Abstract Factory Pattern
- LearnDesign Pattern - Builder Pattern
- LearnDesign Pattern - Prototype Pattern
- LearnDesign Pattern - Adapter Pattern
What is Composite Pattern?
Before we dive into the Composite pattern, I must first define composite pattern:
- As per the GOF "This pattern composes objects into tree structures to represent part-whole hierarchies. Composites let the clients treat individual objects and compositions of objects uniformly".
- In simple words, a composite pattern treats a group of objects in the same way as a single instance of an object.
When to use?
Many times data is organized in a tree structure (for example directories in a computer) and developers must understand the differences between leaf and branch objects.
(Which one is a file and which one is a directory) which makes the code more complex and can lead to errors.
Practical Example
Let's take an example of an application where we have to create an ASP.Net panel control (dynamically). Which in turn contains other UIControls as children. (Each of which can be another Panel.)
At the end you may end up with a complex object tree, where every node may act as a leaf node or as a branch which may contain other leaf nodes.
Let's try to build an application for the above sample without using a Composite Pattern.
Step 1
Create a TextBox class as:
public class Textbox
{
public string Name{get;set;}
public int Width{get;set;}
public int Height{get;set;}
public void Render()
{
Console.WriteLine("Textbox(" + Name + ") Rendered");
}
}
Step 2
Create a Panel class as:
Approach I
public class Panel
{
private List<Object> ChildControls = new List<Object>();
public string Name{get;set;}
public int Width{get;set;}
public int Height{get;set;}
public void RenderPanel()
{
Console.WriteLine("Child Control for Panel(" + Name + ") Render start");
foreach(Object item in ChildControls)
{
Panel objPanel = item as Panel;
if(objPanel == null)
{
Textbox objTextbpx = item as Textbox;
objTextbpx.RenderTextbox();
}
else
{
objPanel.RenderPanel();
}
}
Console.WriteLine("Child Control for Panel(" + Name + ") Render End");
}
public void CreateChildControl(Object control)
{
ChildControls.Add(control);
}
public void DeleteChildControl(Object control)
{
ChildControls.Remove(control);
}
}
Approach II
Maintain a separate list, one for the Panel and one for the Texbox.
(Download the sample code for the full code demonstration.)
Step 3
Write a client code as:
Approach I
Panel objRoot = new Panel();
objRoot.Name = "Panel1";
//Create One Textbox and One Panel inside Root Panel
Textbox objChild1 = new Textbox();
objChild1.Name = "Textbox1.1";
objRoot.CreateChildControl(objChild1);
Panel objChild2 = new Panel();
objChild2.Name = "Panel1.1";
objRoot.CreateChildControl(objChild2);
//Create 3 Textboxes inside SubPanel
Textbox objChild21 = new Textbox();
objChild21.Name = "Textbox1.1.1";
objChild2.CreateChildControl(objChild21);
Textbox objChild22 = new Textbox();
objChild22.Name = "Textbox1.1.2";
objChild2.CreateChildControl(objChild22);
Textbox objChild23 = new Textbox();
objChild23.Name = "Textbox1.1.3";
objChild2.CreateChildControl(objChild23);
//Render Mail Panel
objRoot.RenderPanel();
Approach II
Fill each list (List of panels and list of strings) individually.
Problem
-
A problem with the first approach is it's not type-safe (we can pass any object other than TexBox or Panel, which can result in an exception).
-
The second approach is type-safe but not extensible; entry of any new UI requires recompilation of the code.
-
The biggest problem with both approaches is, there is no strict rule on the Render method, the Panel has a RenderPanel method whereas the Textbox has a RenderTextbox method.
Solution - Composite Pattern
A Composite Pattern treats individual objects (TextBox) and group of objects (Panel) similarly since this pattern forces both of them to be inherited from a common interface - IWebControl.
Components involved in the Composite Pattern are:
-
Component - Declares an interface for objects in the composition - Here IWebControl
-
Leaf- Represents leaf objects in the composition. (One with no Childs) - Here TextBox.
-
Composite- Represents composite objects in the composition (One with Childs) - Here Panel.
Code Walkthrough
Step 1
Create a Component (Interface) IWebControl as:
public interface IWebControl
{
string Name{get;set;}
int Width{get;set;}
int Height{get;set;}
void Render();
void CreateChildControl(IWebControl control);
void DeleteChildControl(IWebControl control);
}
Step 2
Recreate a TextBox as:
public class Textbox : IWebControl
{
public string Name{get;set;}
public int Width{get;set;}
public int Height{get;set;}
public void Render()
{
Console.WriteLine( "Textbox(" + Name + ") Rendered");
}
public void CreateChildControl(IWebControl control)
{
throw new Exception("Adding Child Control to Textbox is not possible");
}
public void DeleteChildControl(IWebControl control)
{
throw new Exception("Removing Child Control from Textbox is not possible");
}
}
Step 3
Recreate a Panel as:
public class Panel : IWebControl
{
private List<IWebControl> ChildControls=new List<IWebControl>();
public string Name{get;set;}
public int Width{get;set;}
public int Height{get;set;}
public void Render()
{
Console.WriteLine("Child Control for Panel(" + Name + ") Render start");
foreach(IWebControl item in ChildControls)
{
item.Render();
}
Console.WriteLine("Child Control for Panel(" + Name + ") Render End");
}
public void CreateChildControl(IWebControl control)
{
ChildControls.Add(control);
}
public void DeleteChildControl(IWebControl control)
{
ChildControls.Remove(control);
}
}
Step 4
WebControl objRoot = new Panel();
objRoot.Name = "Panel1";
IWebControl objChild1 = new Textbox();
objChild1.Name = "Textbox1.1";
objRoot.CreateChildControl(objChild1);
IWebControl objChild2 = new Panel();
objChild2.Name = "Panel1.1";
objRoot.CreateChildControl(objChild2);
IWebControl objChild21 = new Textbox();
objChild21.Name = "Textbox1.1.1";
objChild2.CreateChildControl(objChild21);
IWebControl objChild22 = new Textbox();
objChild22.Name = "Textbox1.1.2";
objChild2.CreateChildControl(objChild22);
IWebControl objChild23 = new Textbox();
objChild23.Name = "Textbox1.1.3";
objChild2.CreateChildControl(objChild23);
objRoot.Render();
Output
Note: Symbols in the output such as "{"and "}" are just written purposely for the sake of visual understanding.
Class Diagram
Hope you enjoyed reading this article.
Stay tuned for the next pattern and good day.