Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · Architecture and Design in .NET

    Design Patterns in C# with Practical Examples: back to the course

    Module 3 · 3. Structural Patterns · Lesson 8 of 12

    Composite and Bridge

    Learning outcome

    You will be able to: (1) design a tree/hierarchy using Composite, (2) use Bridge so the abstraction (graphical objects, groups) can vary independently from concrete implementations (renderers, serializers), and (3) decide when combining both patterns reduces subclass explosion and improves testability.

    Intuition

    Composite answers: "I have a tree of objects (grouping + leaves) and I want to treat groups and leaves uniformly." Bridge answers: "I want an abstraction that can use multiple implementations that should evolve independently." Combine them when you have hierarchical elements (Composite) that must be rendered, serialized, or handled by multiple back-end strategies (Bridge).

    Deep dive

    Key participants in a combined solution:

    • Component (abstraction root) — common API for leaves and composites.
    • Leaf — concrete node with behavior.
    • Composite — node with children collection and operations that delegate to children.
    • Implementor (Bridge) — low-level operations (render, serialize) behind an interface.
    • ConcreteImplementor — actual renderer/serializer.

    When you combine them, each Component holds a reference to an Implementor (the bridge). The Composite delegates its work to the Implementor and iterates children, so rendering logic lives in the Implementor while structural traversal lives in Composite.

    Minimal conceptual snippets:

    Failure modes

    • Over-abstraction: if you have one renderer or no variation in implementation, Bridge is unnecessary. It adds indirection and maintenance cost.
    • Wrong responsibilities: putting traversal logic into implementors (renderers) violates Single Responsibility; keep traversal in Composite and operational details in Implementor.
    • Thread-safety: mutable shared Implementors require care when used concurrently by multiple trees.
    • Deep coupling: if each leaf binds to a different renderer type, you defeat the Bridge intent—prefer a single Implementor interface.

    Interview drill

    1) Why use Composite instead of a flat collection? (Answer: uniform treatment, recursive operations like render/size/serialize.) 2) When would you choose Bridge over simple subclassing? (Answer: when two orthogonal dimensions vary independently—e.g., many kinds of UI components × many rendering backends.) 3) Sketch a class diagram combining Composite + Bridge.

    Micro-test: Given a tree with 1 group and 3 leaves, how many Render calls happen? (Answer: depends on traversal: group start + leaves' renders + group end.)

    Revision checklist

    • Can you explain the two orthogonal axes that cause subclass explosion? (e.g., Shape × Renderer)
    • Can you move renderer-specific code into Implementor and traversal into Composite?
    • Are concrete implementors stateless (preferable) or do they require synchronization?
    • Is the API simple for clients that only need to build trees and call Render?

    Production code

    Production concerns and best practices:

    • Dependency injection: register Implementor implementations and inject where needed; avoid newing concrete implementors deep in code.
    • Immutability: prefer stateless implementors; if they hold state, scope them appropriately per operation to avoid races.
    • Batching: implementors that talk to remote services should batch operations from traversal to avoid N calls.
    • Observability: emit structured logs inside Implementor, not inside traversal, so different back-ends report uniformly.
    • Testing: replace Implementor with test doubles to assert traversal order and operations.

    Example DI registration (conceptual):

    Code walkthrough

    The canonical runnable example creates a small graphic tree and renders it twice with two renderers: a Console-like textual renderer and an HTML renderer. The root Composite exposes a SetRenderer method that assigns an implementor to the whole subtree. An extension method (shown in the executable sample using the newer extension(...) block) provides traversal helpers so you can manipulate the whole subtree without exposing internals.

    Below is a short conceptual traversal extension (the concrete extension implementation appears in the runnable Program.cs):

    Exercises

    1) Modify the example to add a Serializer implementor that outputs JSON; compare code change size vs subclassing every leaf into JsonCircle, JsonRectangle, etc. 2) Benchmark rendering with a single-threaded renderer vs a batched renderer that flushes at the end of traversal. 3) Add concurrency: allow multiple threads to render different subtrees using the same stateless implementor. Verify thread-safety.

    Executable code examples

    Composite + Bridge demo (Console + HTML renderers)

    Program.cs

    C#Runs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    // Implementor (Bridge)
    public interface IRenderer
    {
        string Name { get; }
        void RenderGroupStart(string name);
        void RenderGroupEnd(string name);
        void RenderCircle(int radius);
        void RenderRectangle(int w, int h);
        void RenderText(string text);
    }
    
    // Abstraction (Composite root for Graphics)
    public abstract class Graphic
    {
        protected IRenderer Renderer;
        public void SetRenderer(IRenderer renderer) => Renderer = renderer;
        public abstract void Render();
    }
    
    // Composite node
    public class GraphicGroup : Graphic
    {
        public string Name { get; }
        private readonly List<Graphic> _children = new();
        public IEnumerable<Graphic> Children => _children;
    
        public GraphicGroup(string name, IRenderer renderer)
        {
            Name = name;
            Renderer = renderer;
        }
    
        public void Add(Graphic child)
        {
            _children.Add(child);
        }
    
        public override void Render()
        {
            Renderer.RenderGroupStart(Name);
            foreach (var c in _children)
                c.Render();
            Renderer.RenderGroupEnd(Name);
        }
    }
    
    // Leaves
    public class Circle : Graphic
    {
        public int Radius { get; }
        public Circle(int radius, IRenderer renderer) { Radius = radius; Renderer = renderer; }
        public override void Render() => Renderer.RenderCircle(Radius);
    }
    
    public class Rectangle : Graphic
    {
        public int W { get; }
        public int H { get; }
        public Rectangle(int w, int h, IRenderer renderer) { W = w; H = h; Renderer = renderer; }
        public override void Render() => Renderer.RenderRectangle(W, H);
    }
    
    public class TextLeaf : Graphic
    {
        public string Text { get; }
        public TextLeaf(string text, IRenderer renderer) { Text = text; Renderer = renderer; }
        public override void Render() => Renderer.RenderText(Text);
    }
    
    // Concrete Implementors
    public class ConsoleRenderer : IRenderer
    {
        public string Name => "ConsoleRenderer";
        public void RenderGroupStart(string name) => Console.WriteLine($"Group Start: {name}");
        public void RenderGroupEnd(string name) => Console.WriteLine($"Group End: {name}");
        public void RenderCircle(int radius) => Console.WriteLine($"Circle radius={radius}");
        public void RenderRectangle(int w, int h) => Console.WriteLine($"Rectangle w={w} h={h}");
        public void RenderText(string text) => Console.WriteLine($"Text: {text}");
    }
    
    public class HtmlRenderer : IRenderer
    {
        public string Name => "HtmlRenderer";
        public void RenderGroupStart(string name) => Console.WriteLine($"<group name=\"{name}\">\n");
        public void RenderGroupEnd(string name) => Console.WriteLine($"</group>");
        public void RenderCircle(int radius) => Console.WriteLine($"<circle r=\"{radius}\" />");
        public void RenderRectangle(int w, int h) => Console.WriteLine($"<rect w=\"{w}\" h=\"{h}\" />");
        public void RenderText(string text) => Console.WriteLine($"<text>{text}</text>");
    }
    
    // Traversal helper as a normal extension method wrapper
    public static class GraphicExtensions
    {
        public static IEnumerable<Graphic> Traverse(this Graphic root)
        {
            var stack = new Stack<Graphic>();
            stack.Push(root);
            while (stack.Count > 0)
            {
                var node = stack.Pop();
                yield return node;
                if (node is GraphicGroup g)
                {
                    // push children in reverse to preserve order
                    foreach (var child in g.Children.Reverse())
                        stack.Push(child);
                }
            }
        }
    }
    
    class Program
    {
        static void Main()
        {
            var consoleRenderer = new ConsoleRenderer();
            var htmlRenderer = new HtmlRenderer();
    
            // Build tree using initial renderer
            var root = new GraphicGroup("Root", consoleRenderer);
            root.Add(new Circle(5, consoleRenderer));
            root.Add(new Rectangle(3, 2, consoleRenderer));
            root.Add(new TextLeaf("Hello", consoleRenderer));
    
            // Render with console-like renderer
            Console.WriteLine($"Using {consoleRenderer.Name}");
            root.Render();
    
            Console.WriteLine("---");
    
            // Switch renderer for the whole subtree using traversal extension
            foreach (var node in root.Traverse())
                node.SetRenderer(htmlRenderer);
    
            Console.WriteLine($"Switch to {htmlRenderer.Name}");
            root.Render();
    
            // Why this pattern is preferable to naive subclassing
            Console.WriteLine("Why: Bridge lets abstraction and implementation vary independently; Composite models hierarchies.");
            Console.WriteLine("Misuse: Avoid when only one renderer or no hierarchical structure exists; this adds indirection.");
        }
    }
    Sign in to mark lessons done and keep your place in the course.Sign in