Introduction
The composite pattern provides a way to work with tree structures. In other words, data structures with parent/child relationships. For example, JSON, HTML, XML. In the .NET framework, XElement is developed using a composite design pattern. You know it's important when the creator is doing it.
A real-world example would be the file system.
As you can see, the directory can contain multiple directories or files in it. However, files can not contain more files or directories as they are considered as leaf nodes. The composite pattern enables clients to interact with individual objects or compositions of objects in a uniform fashion. (Composition: has - a relationship) Now the composite pattern is not only about modelling tree structures, but in particular, enabling clients to act on those tree structures uniformly. Let's understand what does that means by our file structure example. If I want to get the size of an individual file, then I could have a method on this individual file to give me its size, and it returns me the number of bytes in the file. But what if I wanted the size of a directory, say Directory Blog? I just have to call a method on Directory Blog and as a consumer, I don't want to have to worry about what's going on through internal logic to get the total size of the directory Blog.
Conceptually, this is what the composite pattern is enabling, whether I want to act upon the granular level or an individual leaf node. The composite pattern will enable us to deal with all this uniformly. Take a look at the conceptual diagram.
Let's go ahead into code and develop one .NET core console application for our file structure example. We will try to get the size of a file or a directory.
First, add a component class
- namespace CompositeDesignPattern.FileSystem
- {
- public abstract class FileSystemItem
- {
- #region Properties
- public string Name { get; }
- #endregion
-
- #region Constructor
- public FileSystemItem(string name)
- {
- this.Name = name;
- }
- #endregion
-
- #region Methods
- public abstract decimal GetSizeinKB();
- #endregion
- }
- }
Then, we need to take care of the leaf node.
- namespace CompositeDesignPattern.FileSystem
- {
- public class FileItem : FileSystemItem
- {
- #region Properties
- public long FileBytes { get; }
- #endregion
-
- #region COnstructor
- public FileItem(string name, long fileBytes) : base(name)
- {
- this.FileBytes = fileBytes;
- }
-
- #endregion
- public override decimal GetSizeinKB()
- {
- return decimal.Divide(this.FileBytes , 1000);
- }
- }
- }
At last, let's go ahead and add the composite class.
- using System.Collections.Generic;
- using System.Linq;
-
- namespace CompositeDesignPattern.FileSystem
- {
- class Directory : FileSystemItem
- {
- #region Properties
-
-
-
-
-
- public List<FileSystemItem> childrens { get; } = new List<FileSystemItem>();
- #endregion
-
- #region COnstructor
- public Directory(string name) : base(name)
- {
-
- }
- #endregion
-
- #region Methods
-
- public override decimal GetSizeinKB()
- {
-
- return this.childrens.Sum(x => x.GetSizeinKB());
- }
-
- public void Add(FileSystemItem newNode)
- {
- this.childrens.Add(newNode);
- }
- public void Remove(FileSystemItem deleteNode)
- {
- this.childrens.Remove(deleteNode);
- }
- #endregion
- }
- }
Finally, our caller class:
- using CompositeDesignPattern.FileSystem;
- using CompositeDesignPattern.Structural;
- using System;
-
- namespace CompositeDesignPattern
- {
- class Program
- {
-
- static void Main(string[] args)
- {
-
- var root = new Directory("Root");
-
-
- var folder1 = new Directory("Folder1");
- var folder2 = new Directory("Folder2");
-
-
- root.Add(folder1);
- root.Add(folder2);
-
-
- folder1.Add(new FileItem("MyBook.txt", 12000));
- folder1.Add(new FileItem("MyVideo.mkv", 1000000));
-
-
- var subfolder1 = new Directory("Sub Folder1");
- subfolder1.Add(new FileItem("MyMusic.mp3", 20000));
- subfolder1.Add(new FileItem("MyResume.pdf", 18000));
- folder1.Add(subfolder1);
-
-
- folder2.Add(new FileItem("AndroidApp.apk", 250000));
- folder2.Add(new FileItem("WPFApp.exe", 87000000));
-
- Console.WriteLine($"Total size of (root): { root.GetSizeinKB() } KB");
- Console.WriteLine($"Total size of (folder 1): { folder1.GetSizeinKB() }KB");
- Console.WriteLine($"Total size of (folder 2): { folder2.GetSizeinKB() }KB");
- }
- }
- }
As you can see, the caller only needs to call GetSizeinKB() function to get the size of any directory or a file.
Now run your program and check the output... It works perfectly.
Everything works well, except that the caller class is pretty messed up. We are adding up files without a proper order, plus every time we have to initialize a directory or file, we have to use a new keyword, which looks pretty messy.
What we can do is we can add a builder class in the middle. Here's how you can design a builder pattern:
- using System;
- using System.Collections.Generic;
- using System.Linq;
-
- namespace CompositeDesignPattern.FileSystem
- {
- public class FileSystemBuilder
- {
- internal Directory Root { get; }
- internal Directory CurrentDirectory { get; set; }
- #region Properties
- #endregion
-
- #region Constructor
- public FileSystemBuilder(string rootDirectory)
- {
-
- this.Root = new Directory(rootDirectory);
- this.CurrentDirectory = Root;
- }
-
- internal FileItem AddFile(string name, int size)
- {
- var file = new FileItem(name, size);
- CurrentDirectory.Add(file);
- return file;
- }
-
- internal Directory SetCurrentDirectory(string currentDirName)
- {
- var dirStack = new Stack<Directory>();
- dirStack.Push(this.Root);
- while (dirStack.Any())
- {
-
- var currentDir = dirStack.Pop();
-
- if(currentDirName == currentDir.Name)
- {
- this.CurrentDirectory = currentDir;
- return currentDir;
- }
-
-
-
- foreach (var item in currentDir.childrens.OfType<Directory>())
- {
-
- dirStack.Push(item);
- }
- }
-
- throw new InvalidOperationException($"Directory name: '{ currentDirName }' not found!");
- }
-
- internal Directory AddDirectory(string name)
- {
- var dir = new Directory(name);
- this.CurrentDirectory.Add(dir);
- this.CurrentDirectory = dir;
- return dir;
- }
- #endregion
- }
- }
As you can see, we have encapsulated composite within our Builder class CompositeDesignPattern.
Also, notice that in our method SetCurrentDirectory(). We are not performing a recursive to search a directory, as it is not an optimal solution if the directory is large enough. Rather, we have used an iterative stack-based solution.
Now see how our calling class looks:
- using CompositeDesignPattern.FileSystem;
- using CompositeDesignPattern.Structural;
- using Newtonsoft.Json;
- using System;
-
- namespace CompositeDesignPattern
- {
- class Program
- {
-
- static void Main(string[] args)
- {
-
- var builder = new FileSystemBuilder("root");
- builder.AddDirectory("Folder1");
- builder.AddFile("MyBook.txt", 12000);
- builder.AddFile("MyVideo.mkv", 1000000);
- builder.AddDirectory("SubFolder");
- builder.AddFile("MyMusic.mp3", 20000);
- builder.AddFile("MyResume.pdf", 18000);
- builder.SetCurrentDirectory("root");
- builder.AddDirectory("Folder1");
- builder.AddFile("AndroidApp.apk", 250000);
- builder.AddFile("WPFApp.exe", 87000000);
-
- Console.WriteLine($"Total size of (root): { builder.Root.GetSizeinKB() } KB");
-
- Console.WriteLine(JsonConvert.SerializeObject(builder.Root, Formatting.Indented));
- }
- }
- }
We will display the object in JSON format to see it's tree-like structure.
Now when you run this app, you'll get the following output:
Now that's how you roll. Understand conceptually first, then it will easy to implement.
I sincerely hope you enjoyed this blog and that you're inspired to apply what you've learned to your own applications. Thank you.
Connect with me: