Intermediate

C-Sharp 8 Design Patterns Composite

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...

Level: Intermediate — prerequisites: C# and basic notions of data structures


Table of Contents

  1. Course Overview
  2. What is the Composite pattern?
  1. Structural implementation (minimal example)
  1. Alternative considerations and variants
  1. Real example: virtual file system
  1. Encapsulate the build with a Builder
  1. Existing Composites in .NET
  1. Visual Studio Project Structure
  2. Summary and key points

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

ComponentRole
ComponentDefines the common interface, including the primary operation. This is the basic abstract class.
LeafInherits from Component. Has no children. Implements the main operation for an atomic element.
CompositeInherits 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.
CustomerManipulates 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 abstract because 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 depth parameter 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 Component and pass name to base class
  • Implements PrimaryOperation: displays name with indentation proportional to depth
  • Does not have a child collection or Add/Remove methods

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
  • Add and Remove manage child collection
  • PrimaryOperation displays the name of the composite, then recursively calls PrimaryOperation on 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 PrimaryOperation at any level: on root (the whole tree), on comp2 (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 Leaf no 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

PriorityRecommended implementation
Maximum security and clarity for developersRemove 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:

  1. Self-implemented read-only property ({ get; }) — reference cannot be reassigned
  2. 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

CallCalculationResult
proj2.GetSizeInKB()6100 + 7100 = 13200 bytes13.2 KB
proj1.GetSizeInKB()2100 + 3100 + 4100 + 5100 = 14400 bytes14.4 KB
root.GetSizeInKB()13200 + 14400 = 27600 bytes27.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:

  1. Manual instantiation: repeated use of the new keyword for each object
  2. Individual initialization: the correct values must be provided for each object
  3. Correct assignment to the correct parent: you must make sure to call Add on 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:

  1. The instantiation of objects (new DirectoryItem, new FileItem)
  2. Initialization (passing values into the constructor)
  3. Assignment to the correct parent (calling Add on currentDirectory)

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
  • StackOverflowException source 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 only DirectoryItem, excluding FileItem — 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:

  • XElement inherits from XContainer
  • XContainer exposes Elements() which returns IEnumerable<XElement>
  • In other words: an XElement is itself an XElement which can contain a collection of other XElements — 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:

AppearanceSetCurrentDirectoryFindElements
AlgorithmIterative stackIterative stack
CriterionExact name of a directoryPredicate Predicate<XElement>
ResultA single DirectoryItemIEnumerable<XElement> (multiple results)
TypeBuilder methodStatic 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:

  • HasElements returns true if the element has children — so it is a composite node
  • !x.HasElements is true if 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

SectionContent
ConceptualDefinition of the pattern, motivation, structure (Component / Leaf / Composite / Client)
StructuralMinimal implementation with generic names to solidify concepts
Real ExampleVirtual file system with FileSystemItem, FileItem, DirectoryItem
BuilderEncapsulating complex construction with FileSystemBuilder
CrossingIterative algorithm with Stack to traverse the tree without recursion
.NETIdentifying 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

FeatureC# versionUse
Read-only self-implemented properties ({ get; })C# 6Name { get; } in Component
Automatic property initializersC# 6Items { get; } = new List<...>()
String interpolation ($"...")C# 6Console Messages
LINQ OfType<T>().NET 3.5+Filter DirectoryItem in SetCurrentDirectory
LINQ Sum().NET 3.5+Calculate GetSizeInKB in DirectoryItem
yield returnC# 2.0FindElements — lazy enumeration
Extension MethodsC# 3.0Extensions.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

Interested in this course?

Contact us to book it or get a custom training plan for your team.