Level: Intermediate — prerequisites: C# and basic notions of data structures
Table of Contents
- 2.1 Definition and motivation
- 2.2 Concrete example: the file system
- 2.3 The four components of the pattern
- 3.1 The Component abstract class
- 3.2 Leaf class
- 3.3 The Composite class
- 3.4 The Client — Program.cs
- 3.5 Execution result
- 4.1 Problem: Add/Remove methods on Leaf
- 4.2 Option 1: virtual methods in the base class
- 4.3 Option 2: remove Add/Remove from base class
- 4.4 Synthesis of compromises
- 5.1 FileSystemItem — abstract base class
- 5.2 FileItem — leaf node
- 5.3 DirectoryItem — composite node
- 5.4 Construction and use of composite
- 5.5 Run Results
- 6.1 Problems in manual construction
- 6.2 FileSystemBuilder
- 6.3 Iterative traversal with a Stack — SetCurrentDirectory
- 6.4 Using the Builder — Program.cs
- 6.5 JSON serialization of hierarchy
- 7.1 XElement and System.Xml.Linq
- 7.2 Data XML file — file-system.xml
- 7.3 FindElements extension method
- 7.4 XML composite consumption
1. Course Overview
This course covers the Composite design pattern in C#. The Composite pattern provides a way to work with tree structures in a consistent manner, regardless of whether you are manipulating a parent element or a child (leaf) element in that tree.
Topics covered:
- Understand what the Composite pattern is and why it is useful
- Implement your own Composite patterns
- Compose composites elegantly with Builders
- Consume composites efficiently
- Identify and use existing composites in .NET
Prerequisites: Be comfortable with C# and have basic notions of data structures.
2. What is the Composite pattern?
2.1 Definition and motivation
Deepening your knowledge of canonical design patterns in object-oriented programming is a valuable tool in your developer’s toolbox.
When you think of Composite pattern, you have to think of tree structures — that is, data structures with parent/child relationships. The word “composite” refers to the fact that we compose objects into tree structures.
Definition: The Composite pattern allows clients to interact with individual objects or compositions of objects in a uniform manner.
In this context:
- Individual objects = leaf nodes (leaf nodes), i.e. objects without children
- Object compositions = composite nodes that have children; these children can be sheets or other nested composites
2.2 Concrete example: the file system
The most common example of a tree structure is a file system.
development/
├── project1/
│ ├── p1f1.txt (feuille)
│ ├── p1f2.txt (feuille)
│ └── sub-dir1/
│ ├── p1f3.txt (feuille)
│ └── p1f4.txt (feuille)
└── project2/
├── p2f1.txt (feuille)
└── p2f2.txt (feuille)
- The files are the leaf nodes: they have no children
- directories are composite nodes: they contain children (files or other directories)
The point of the pattern: If I want to get the size of an individual file, I call GetSizeInKB() on it. If I want the size of a directory (project1, project2, or the root development), I call exactly the same GetSizeInKB() method — the composite itself takes care of recursively adding the sizes of all its children. The client does not have to know the internal details.
2.3 The four components of the pattern
| Component | Role |
|---|---|
| Component | Defines the common interface, including the primary operation. This is the basic abstract class. |
| Leaf | Inherits from Component. Has no children. Implements the main operation for an atomic element. |
| Composite | Inherits from Component. Has a collection of children of type Component (which can themselves be Leaf or Composite). Implements the main operation by delegating to its children. |
| Customer | Manipulates a Component, invokes the main operation. The client can act on any level of the hierarchy in a uniform manner. |
3. Structural implementation (minimal example)
Before the concrete examples, here is a “structural” example which illustrates the skeleton of the pattern with generic names (Component, Leaf, Composite, PrimaryOperation).
The project is a .NET Core console application. Structural classes are found in the Structural/ subdirectory.
3.1 The Component abstract class
// Structural/Component.cs
using System;
namespace CompositeDemo.Structural
{
public abstract class Component
{
public Component(string name)
{
this.Name = name;
}
// Propriété en lecture seule (C# 6+ : readonly auto-property)
public string Name { get; }
// L'opération principale — abstraite, implémentée différemment
// par Leaf et Composite
public abstract void PrimaryOperation(int depth);
}
}
Important notes:
- The class is
abstractbecause it is the base for Leaf and Composite Name { get; }is a read-only auto-implemented property (feature introduced in C# 6), whose value can only be assigned in the constructor- The
depthparameter allows you to display indentation in console output
3.2 The Leaf class
// Structural/Leaf.cs
using System;
namespace CompositeDemo.Structural
{
public class Leaf : Component
{
public Leaf(string name) : base(name)
{
}
public override void PrimaryOperation(int depth)
{
Console.WriteLine(new String('-', depth) + this.Name);
}
// Pas de méthodes Add/Remove : un nœud feuille n'a pas d'enfants
}
}
Key points:
- Inherit from
Componentand pass name to base class - Implements
PrimaryOperation: displays name with indentation proportional to depth - Does not have a child collection or
Add/Removemethods
3.3 The Composite class
// Structural/Composite.cs
using System;
using System.Collections.Generic;
namespace CompositeDemo.Structural
{
public class Composite : Component
{
private List<Component> children = new List<Component>();
public Composite(string name) : base(name)
{
}
public void Add(Component c)
{
this.children.Add(c);
}
public void Remove(Component c)
{
this.children.Remove(c);
}
public override void PrimaryOperation(int depth)
{
// Affiche le nom du composite lui-même
Console.WriteLine(new String('-', depth) + this.Name);
// Délègue récursivement à chaque enfant
foreach (var component in this.children)
{
component.PrimaryOperation(depth + 2);
}
}
}
}
Key points:
- Has a private
List<Component>for its children AddandRemovemanage child collectionPrimaryOperationdisplays the name of the composite, then recursively callsPrimaryOperationon each child with increased depth
3.4 The Client — Program.cs
The client code constructs the tree and invokes the operation:
// Dans Program.cs — méthode StructuralExample()
private static void StructuralExample()
{
// Construction de la structure arborescente
var root = new Composite("root");
root.Add(new Leaf("Leaf A"));
root.Add(new Leaf("Leaf B"));
var comp1 = new Composite("Composite C1");
comp1.Add(new Leaf("Leaf C1-A"));
comp1.Add(new Leaf("Leaf C1-B"));
var comp2 = new Composite("Composite C2");
comp2.Add(new Leaf("Leaf C2-A"));
comp1.Add(comp2); // comp2 est enfant de comp1
root.Add(comp1);
root.Add(new Leaf("Leaf C"));
// Démonstration d'ajout et suppression
var leaf = new Leaf("Leaf D");
root.Add(leaf);
root.Remove(leaf); // Leaf D retiré
// Affichage récursif depuis la racine
root.PrimaryOperation(1);
}
Important points in this code:
- We can invoke
PrimaryOperationat any level: onroot(the whole tree), oncomp2(only this subtree), or even on an individual leaf - The same method works uniformly across all levels — this is the essence of the Composite pattern
3.5 Execution result
-root
---Leaf A
---Leaf B
---Composite C1
-----Leaf C1-A
-----Leaf C1-B
-----Composite C2
-------Leaf C2-A
---Leaf C
If we call comp2.PrimaryOperation(1) instead:
-Composite C2
---Leaf C2-A
4. Alternative considerations and variations
An important aspect to keep in mind: Patterns are guides, not single prescriptive implementations. There are variations, each with their advantages and disadvantages.
4.1 Problem: Add/Remove methods on the Leaf
In the initial implementation of the course (before the final version), the Add and Remove methods were declared abstract in Component, forcing all subclasses — including Leaf — to implement them. This violates the principle that a class should only define operations that are meaningful to it.
// Problème : rien n'empêche au niveau de la compilation d'appeler Add sur un Leaf
var leafD = new Leaf("Leaf D");
leafD.Add(new Leaf("Leaf E")); // Le compilateur accepte, mais échoue à l'exécution !
This leads to a NotImplementedException error only at runtime, which is far from ideal.
4.2 Option 1: virtual methods in the base class
// Dans Component.cs
public virtual void Add(Component c)
{
throw new NotImplementedException();
}
public virtual void Remove(Component c)
{
throw new NotImplementedException();
}
- The
Leafno longer needs to implement these methods - Persistent problem: the code still compiles, but the error remains at runtime
4.3 Option 2: Remove Add/Remove from the base class
This is the recommended option in the final version of the course (reflected in the after/ source code).
// Component.cs — NE contient PAS Add/Remove
// Composite.cs — contient Add/Remove directement (sans override)
public void Add(FileSystemItem component)
{
this.Items.Add(component);
}
public void Remove(FileSystemItem component)
{
this.Items.Remove(component);
}
Advantage: the compiler detects the error at design time:
leafD.Add(new Leaf("Leaf E")); // ERREUR DE COMPILATION — ne compile pas
The class hierarchy no longer defines methods that do not make sense for all of its subclasses.
4.4 Summary of trade-offs
| Priority | Recommended implementation |
|---|---|
| Maximum security and clarity for developers | Remove Add/Remove from base class — compile-time error |
| Treat all components completely uniformly (even sheets have Add/Remove) | Keep Add/Remove abstract in base class |
In practice, we will encounter different variations of this pattern in different code bases. It is important to know how to recognize them.
5. Real example: virtual file system
Let’s move on to a concrete and significant example: the implementation of an in-memory virtual file system.
5.1 FileSystemItem — abstract base class
// FileSystemItem.cs
namespace CompositeDemo
{
public abstract class FileSystemItem
{
public FileSystemItem(string name)
{
this.Name = name;
}
public string Name { get; }
// Opération principale : retourner la taille en kilooctets
public abstract decimal GetSizeInKB();
}
}
GetSizeInKB()is the main operation — it will be invoked uniformly across files and directories- Name is stored as read-only property
5.2 FileItem — leaf node
// FileItem.cs
namespace CompositeDemo
{
public class FileItem : FileSystemItem
{
public FileItem(string name, long fileBytes) : base(name)
{
this.FileBytes = fileBytes;
}
public long FileBytes { get; }
public override decimal GetSizeInKB()
{
// Division des octets par 1000 pour obtenir les kilooctets
return decimal.Divide(this.FileBytes, 1000);
}
}
}
- Receive number of bytes in constructor
GetSizeInKB()simply converts bytes → kilobytes
5.3 DirectoryItem — composite node
// DirectoryItem.cs
using System.Collections.Generic;
using System.Linq;
namespace CompositeDemo
{
public class DirectoryItem : FileSystemItem
{
public DirectoryItem(string name) : base(name)
{
}
// Deux fonctionnalités C# 6 combinées :
// 1. Propriété auto-implémentée en lecture seule
// 2. Initialiseur de propriété automatique
public List<FileSystemItem> Items { get; } = new List<FileSystemItem>();
public void Add(FileSystemItem component)
{
this.Items.Add(component);
}
public void Remove(FileSystemItem component)
{
this.Items.Remove(component);
}
public override decimal GetSizeInKB()
{
// Somme récursive des tailles de tous les enfants
// Fonctionne car chaque enfant (FileItem ou DirectoryItem)
// implémente GetSizeInKB() uniformément
return this.Items.Sum(x => x.GetSizeInKB());
}
}
}
C# 6+ — Two features on the Items line:
- Self-implemented read-only property (
{ get; }) — reference cannot be reassigned - Automatic property initializer (
= new List<FileSystemItem>()) — the list is initialized inline
The heart of the pattern: GetSizeInKB() uses Sum with a lambda that calls GetSizeInKB() on each child. Whether the child is a FileItem or a DirectoryItem, the same interface is used — this is the uniform behavior guaranteed by the Composite pattern.
5.4 Construction and use of composite
// Dans Program.cs — méthode FileSystemComposite()
private static void FileSystemComposite()
{
// Création de la racine
var root = new DirectoryItem("development");
// Création des deux répertoires principaux
var proj1 = new DirectoryItem("project1");
var proj2 = new DirectoryItem("project2");
root.Add(proj1);
root.Add(proj2);
// Fichiers de project1
proj1.Add(new FileItem("p1f1.txt", 2100));
proj1.Add(new FileItem("p1f2.txt", 3100));
// Sous-répertoire de project1
var subDir1 = new DirectoryItem("sub-dir1");
subDir1.Add(new FileItem("p1f3.txt", 4100));
subDir1.Add(new FileItem("p1f4.txt", 5100));
proj1.Add(subDir1);
// Fichiers de project2
proj2.Add(new FileItem("p2f1.txt", 6100));
proj2.Add(new FileItem("p2f2.txt", 7100));
// Utilisation uniforme à différents niveaux
Console.WriteLine($"Total size (proj2): {proj2.GetSizeInKB()}"); // 13.2
Console.WriteLine($"Total size (proj1): {proj1.GetSizeInKB()}"); // 14.4
Console.WriteLine($"Total size (root) : {root.GetSizeInKB()}"); // 27.6
}
5.5 Execution results
| Call | Calculation | Result |
|---|---|---|
proj2.GetSizeInKB() | 6100 + 7100 = 13200 bytes | 13.2 KB |
proj1.GetSizeInKB() | 2100 + 3100 + 4100 + 5100 = 14400 bytes | 14.4 KB |
root.GetSizeInKB() | 13200 + 14400 = 27600 bytes | 27.6 KB |
This demonstrates the power of composite: we manipulate any component at any level of the hierarchy in a uniform way, whether it is a sheet or a composite with children.
6. Encapsulate the construction with a Builder
Once composite structures are built, they are very easy to use. However, the construction itself can sometimes be confusing and error-prone. A dedicated Builder solves this problem.
6.1 Manual Build Problems
Manual construction (as in the previous section) poses several challenges:
- Manual instantiation: repeated use of the
newkeyword for each object - Individual initialization: the correct values must be provided for each object
- Correct assignment to the correct parent: you must make sure to call
Addon the correct parent object. For example:
- Should
root.Add(proj1)be done on line 14 or 15? (both would work) proj1.Add(subDir1)— when exactly should we do this?
These questions reduce readability and increase the risk of errors.
6.2 FileSystemBuilder
// FileSystemBuilder.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace CompositeDemo
{
public class FileSystemBuilder
{
private DirectoryItem currentDirectory;
public FileSystemBuilder(string rootDirectory)
{
// Encapsule l'instanciation de la racine
this.Root = new DirectoryItem(rootDirectory);
// Le répertoire courant pointe initialement sur la racine
this.currentDirectory = this.Root;
}
public DirectoryItem Root { get; }
public DirectoryItem AddDirectory(string name)
{
var dir = new DirectoryItem(name); // Instanciation
this.currentDirectory.Add(dir); // Assignation au bon parent
this.currentDirectory = dir; // Mise à jour du pointeur courant
return dir;
}
public FileItem AddFile(string name, long fileBytes)
{
var file = new FileItem(name, fileBytes); // Instanciation
this.currentDirectory.Add(file); // Assignation au bon parent
return file;
}
public DirectoryItem SetCurrentDirectory(string directoryName)
{
// Traversée itérative avec une Stack (voir section suivante)
var dirStack = new Stack<DirectoryItem>();
dirStack.Push(this.Root);
while (dirStack.Any())
{
var current = dirStack.Pop();
if (current.Name == directoryName)
{
this.currentDirectory = current;
return current;
}
foreach (var item in current.Items.OfType<DirectoryItem>())
{
dirStack.Push(item);
}
}
throw new InvalidOperationException($"Directory name '{directoryName}' not found!");
}
}
}
The Builder encapsulates three key responsibilities:
- The instantiation of objects (
new DirectoryItem,new FileItem) - Initialization (passing values into the constructor)
- Assignment to the correct parent (calling
AddoncurrentDirectory)
6.3 Iterative traversal with a Stack — SetCurrentDirectory
The SetCurrentDirectory method is the heart of composite traversal. It illustrates an important algorithm.
Why avoid recursion?
- Although recursion is sometimes elegant, it can be:
- Harder to understand
- Less efficient than an iterative solution
StackOverflowExceptionsource for large trees
The iterative algorithm with a Stack:
public DirectoryItem SetCurrentDirectory(string directoryName)
{
var dirStack = new Stack<DirectoryItem>();
dirStack.Push(this.Root); // Commencer depuis la racine
while (dirStack.Any())
{
var current = dirStack.Pop(); // Dépiler le prochain répertoire
// Trouvé ?
if (current.Name == directoryName)
{
this.currentDirectory = current;
return current; // Retour immédiat
}
// Pas encore trouvé — empiler les sous-répertoires pour les explorer
// OfType<DirectoryItem>() filtre les FileItem (on ne cherche que des répertoires)
foreach (var item in current.Items.OfType<DirectoryItem>())
{
dirStack.Push(item);
}
}
// Le répertoire n'existe nulle part dans l'arbre
throw new InvalidOperationException($"Directory name '{directoryName}' not found!");
}
Important points:
OfType<DirectoryItem>()(LINQ method) filters the collection to keep onlyDirectoryItem, excludingFileItem— we only search in directories- It is a generic algorithm useful for traversing any hierarchical tree structure without recursion
The same logic is used in the FindElements extension method (see section 7.3), with the difference that instead of searching for a specific name, we pass an arbitrary predicate.
6.4 Using the Builder — Program.cs
Compare readability between manual construction and Builder:
With the Builder (much more readable):
private static void BuilderExample()
{
var builder = new FileSystemBuilder("development");
// Construire project1 avec ses fichiers et sous-répertoire
builder.AddDirectory("project1");
builder.AddFile("p1f1.txt", 2100);
builder.AddFile("p1f2.txt", 3100);
builder.AddDirectory("sub-dir"); // sub-dir devient enfant de project1 automatiquement
builder.AddFile("p1f3.txt", 4100);
builder.AddFile("p1f4.txt", 5100);
// Retourner au répertoire racine pour ajouter project2 comme frère de project1
builder.SetCurrentDirectory("development");
builder.AddDirectory("project2");
builder.AddFile("p2f1.txt", 6100);
builder.AddFile("p2f2.txt", 7100);
Console.WriteLine($"Total size (root) : {builder.Root.GetSizeInKB()}");
Console.WriteLine(JsonConvert.SerializeObject(builder.Root, Formatting.Indented));
}
The Builder uses a currentDirectory pointer which:
- Tip on root at start
- Automatically moves to the new directory created when calling
AddDirectory - Can be explicitly repositioned with
SetCurrentDirectory
Warning: Without SetCurrentDirectory("development"), AddDirectory("project2") would have created project2 as a subdirectory of sub-dir — which would be incorrect.
6.5 JSON serialization of hierarchy
The project uses Newtonsoft.Json to easily visualize the constructed hierarchy:
Console.WriteLine(JsonConvert.SerializeObject(builder.Root, Formatting.Indented));
Dependency in file .csproj:
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
The JSON output allows visual validation that the tree structure is correctly constructed.
7. Existing Composites in .NET
It is valuable to know not only how to build your own composites, but also how to recognize and use those that exist in frameworks.
Examples of ubiquitous composite structures:
- JSON Documents
- XML Documents
- HTML Documents
- UI control hierarchies (in .NET, controls often have a collection of children)
7.1 XElement and System.Xml.Linq
XElement in System.Xml.Linq is an example of a composite built into .NET.
Structure analysis:
XElementinherits fromXContainerXContainerexposesElements()which returnsIEnumerable<XElement>- In other words: an
XElementis itself anXElementwhich can contain a collection of otherXElements — this is exactly the definition of a composite
7.2 Data XML file — file-system.xml
This XML file represents the exact same file system structure used throughout the course. It is loaded from disk at runtime.
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item name="development">
<items>
<item name="project1">
<items>
<item name="p1f1.txt" fileBytes="2100"/>
<item name="p1f2.txt" fileBytes="3100"/>
<item name="sub-dir">
<item name="p1f3.txt" fileBytes="4100"/>
<item name="p1f4.txt" fileBytes="5100"/>
</item>
</items>
</item>
<item name="project2">
<items>
<item name="p2f1.txt" fileBytes="6100"/>
<item name="p2f1.txt" fileBytes="7100"/>
</items>
</item>
</items>
</item>
</root>
Configuration in .csproj to copy the file to the output directory:
<ItemGroup>
<None Update="file-system.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
7.3 FindElements extension method
A generic extension method to traverse any composite XElement with an arbitrary predicate:
// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace CompositeDemo
{
public static class Extensions
{
public static IEnumerable<XElement> FindElements(
this XElement root,
Predicate<XElement> predicate)
{
var stack = new Stack<XElement>();
stack.Push(root);
while (stack.Any())
{
var current = stack.Pop();
// Empiler tous les enfants pour les explorer ensuite
foreach (var element in current.Elements())
{
stack.Push(element);
}
// Vérifier si l'élément courant satisfait le prédicat
if (predicate(current))
{
yield return current; // Retour paresseux avec yield
}
}
}
}
}
Comparison with SetCurrentDirectory:
| Appearance | SetCurrentDirectory | FindElements |
|---|---|---|
| Algorithm | Iterative stack | Iterative stack |
| Criterion | Exact name of a directory | Predicate Predicate<XElement> |
| Result | A single DirectoryItem | IEnumerable<XElement> (multiple results) |
| Type | Builder method | Static extension method |
Using yield return makes this method lazy: the elements are produced one by one as the collection is enumerated.
7.4 XML composite consumption
// Dans Program.cs — méthode Main()
static void Main(string[] args)
{
// Chargement du fichier XML — XElement est un composite intégré à .NET
var xml = XElement.Load("file-system.xml");
// Trouver tous les nœuds feuilles :
// Un nœud est une feuille s'il N'A PAS d'éléments enfants
foreach (var leaf in xml.FindElements(x => !x.HasElements))
{
Console.WriteLine($"***** LEAF: {leaf.Attribute("name")}, {leaf.Attribute("fileBytes")}");
}
}
The predicate x => !x.HasElements:
HasElementsreturnstrueif the element has children — so it is a composite node!x.HasElementsistrueif the element has NO children — so it is a sheet
Expected output:
***** LEAF: name="p1f1.txt", fileBytes="2100"
***** LEAF: name="p1f2.txt", fileBytes="3100"
***** LEAF: name="p1f3.txt", fileBytes="4100"
***** LEAF: name="p1f4.txt", fileBytes="5100"
***** LEAF: name="p2f1.txt", fileBytes="6100"
***** LEAF: name="p2f1.txt", fileBytes="7100"
This code demonstrates how to effectively use knowledge of the Composite pattern to work with a composite built into .NET.
8. Visual Studio Project Structure
Solution: CompositeDemo.sln (Visual Studio 2019, version 16)
Project: CompositeDemo.csproj
- Target:
netcoreapp3.0(.NET Core 3.0) - Output type:
Exe(console application) - NuGet dependency:
Newtonsoft.Json 12.0.2
Code file tree:
CompositeDemo/
├── CompositeDemo.sln
└── CompositeDemo/
├── CompositeDemo.csproj
├── Program.cs ← Client (point d'entrée)
├── FileSystemItem.cs ← Component abstrait (exemple réel)
├── FileItem.cs ← Leaf (exemple réel)
├── DirectoryItem.cs ← Composite (exemple réel)
├── FileSystemBuilder.cs ← Builder pour le composite
├── Extensions.cs ← Méthode d'extension FindElements
├── file-system.xml ← Données XML (exemple .NET XElement)
└── Structural/
├── Component.cs ← Component abstrait (exemple structurel)
├── Leaf.cs ← Leaf (exemple structurel)
└── Composite.cs ← Composite (exemple structurel)
Contents of Program.cs — general structure:
// Program.cs
using CompositeDemo.Structural;
using Newtonsoft.Json;
using System;
using System.Xml.Linq;
namespace CompositeDemo
{
class Program
{
// Méthode principale — contient le code actif (XElement dans la version finale)
static void Main(string[] args)
{
var xml = XElement.Load("file-system.xml");
foreach (var leaf in xml.FindElements(x => !x.HasElements))
{
Console.WriteLine($"***** LEAF: {leaf.Attribute("name")}, {leaf.Attribute("fileBytes")}");
}
}
// Exemples extraits dans des méthodes privées pour référence
private static void BuilderExample() { /* ... */ }
private static void FileSystemComposite() { /* ... */ }
private static void StructuralExample() { /* ... */ }
}
}
9. Summary and key points
What was covered
| Section | Content |
|---|---|
| Conceptual | Definition of the pattern, motivation, structure (Component / Leaf / Composite / Client) |
| Structural | Minimal implementation with generic names to solidify concepts |
| Real Example | Virtual file system with FileSystemItem, FileItem, DirectoryItem |
| Builder | Encapsulating complex construction with FileSystemBuilder |
| Crossing | Iterative algorithm with Stack to traverse the tree without recursion |
| .NET | Identifying and using XElement as a native composite |
The traversal algorithm to remember
// Traversée itérative d'un composite avec une Stack
var stack = new Stack<T>();
stack.Push(root);
while (stack.Any())
{
var current = stack.Pop();
// Traitement de current...
foreach (var child in current.Children)
{
stack.Push(child);
}
}
This algorithm is a valuable general technique for traversing any hierarchical tree structure without recursion.
C# features used in this course
| Feature | C# version | Use |
|---|---|---|
Read-only self-implemented properties ({ get; }) | C# 6 | Name { get; } in Component |
| Automatic property initializers | C# 6 | Items { get; } = new List<...>() |
String interpolation ($"...") | C# 6 | Console Messages |
LINQ OfType<T>() | .NET 3.5+ | Filter DirectoryItem in SetCurrentDirectory |
LINQ Sum() | .NET 3.5+ | Calculate GetSizeInKB in DirectoryItem |
yield return | C# 2.0 | FindElements — lazy enumeration |
| Extension Methods | C# 3.0 | Extensions.FindElements |
Stack<T> generic | .NET 2.0+ | Iterative traversal without recursion |
The essential message
When you think of the Composite pattern, immediately think of tree structures, and more specifically the ability to manipulate a component at any level of that tree structure in a uniform way, whether that component is a sheet or a composite.
This pattern is particularly powerful because it allows:
- Write simple client code that works across the entire hierarchy
- encapsulate the complexity of recursive traversal in the composite nodes themselves
- easily expand the hierarchy by adding new types of sheets or composites without modifying the client
Deepening your understanding of all design patterns can have significant benefits when you create your own code and consume the code of others.
Search Terms
c-sharp · design · patterns · composite · testing · architecture · c# · .net · development · class · base · leaf · abstract · builder · construction · execution · methods · node · option · pattern · program.cs · remove · system · traversal