Structural Patterns

Object-Oriented Programming

Structural patterns from the Gang of Four’s “Design Patterns” (1994): Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

Structural patterns deal with object composition and relationships, helping form larger structures while maintaining flexibility.

Adapter Pattern

Purpose: Allow incompatible interfaces to work together by wrapping existing functionality.

// Legacy class we cannot modify
public class LegacyRectangle
{
    public int X { get; set; }
    public int Y { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }

    public void LegacyDraw()
    {
        Console.WriteLine($"Drawing rectangle at ({X},{Y}) with size {Width}x{Height}");
    }
}

// Modern interface we want to use
public interface IShape
{
    void Draw();
    void Move(int x, int y);
    void Resize(int width, int height);
}

// Adapter that makes LegacyRectangle work with IShape
public class RectangleAdapter : IShape
{
    private readonly LegacyRectangle legacyRectangle;

    public RectangleAdapter(LegacyRectangle legacyRectangle)
    {
        this.legacyRectangle = legacyRectangle;
    }

    public void Draw()
    {
        legacyRectangle.LegacyDraw();
    }

    public void Move(int x, int y)
    {
        legacyRectangle.X = x;
        legacyRectangle.Y = y;
    }

    public void Resize(int width, int height)
    {
        legacyRectangle.Width = width;
        legacyRectangle.Height = height;
    }
}

// Usage:
var legacyRect = new LegacyRectangle { X = 10, Y = 20, Width = 100, Height = 50 };
IShape shape = new RectangleAdapter(legacyRect);
shape.Draw();
shape.Move(30, 40);

Bridge Pattern

Purpose: Separate abstraction from implementation to avoid Cartesian product complexity.

// Implementation interface
public interface IRenderer
{
    void RenderCircle(float radius);
    void RenderRectangle(float width, float height);
}

// Concrete implementations
public class PixelRenderer : IRenderer
{
    public void RenderCircle(float radius)
    {
        Console.WriteLine($"Drawing pixels for circle with radius {radius}");
    }

    public void RenderRectangle(float width, float height)
    {
        Console.WriteLine($"Drawing pixels for rectangle {width}x{height}");
    }
}

public class VectorRenderer : IRenderer
{
    public void RenderCircle(float radius)
    {
        Console.WriteLine($"Drawing circle vector with radius {radius}");
    }

    public void RenderRectangle(float width, float height)
    {
        Console.WriteLine($"Drawing rectangle vector {width}x{height}");
    }
}

// Abstraction
public abstract class Shape
{
    protected IRenderer renderer;

    protected Shape(IRenderer renderer)
    {
        this.renderer = renderer;
    }

    public abstract void Draw();
}

// Refined abstractions
public class Circle : Shape
{
    private float radius;

    public Circle(IRenderer renderer, float radius) : base(renderer)
    {
        this.radius = radius;
    }

    public override void Draw()
    {
        renderer.RenderCircle(radius);
    }
}

public class Rectangle : Shape
{
    private float width, height;

    public Rectangle(IRenderer renderer, float width, float height) : base(renderer)
    {
        this.width = width;
        this.height = height;
    }

    public override void Draw()
    {
        renderer.RenderRectangle(width, height);
    }
}

// Usage:
IRenderer pixelRenderer = new PixelRenderer();
IRenderer vectorRenderer = new VectorRenderer();

var shapes = new Shape[]
{
    new Circle(pixelRenderer, 5),
    new Rectangle(vectorRenderer, 10, 15),
    new Circle(vectorRenderer, 3)
};

foreach (var shape in shapes)
{
    shape.Draw();
}

Composite Pattern

Purpose: Treat individual objects and collections uniformly through a common interface.

public abstract class GraphicObject
{
    public virtual string Name { get; set; } = "Group";
    public string Color { get; set; }

    private readonly Lazy<List<GraphicObject>> children = new(() => new List<GraphicObject>());
    public List<GraphicObject> Children => children.Value;

    public virtual void Draw()
    {
        Draw(0);
    }

    protected virtual void Draw(int depth)
    {
        Console.WriteLine($"{new string(' ', depth * 2)}{Name} (Color: {Color})");
        foreach (var child in Children)
        {
            child.Draw(depth + 1);
        }
    }
}

public class Circle : GraphicObject
{
    public override string Name { get; set; } = "Circle";
}

public class Square : GraphicObject
{
    public override string Name { get; set; } = "Square";
}

// Modern approach using IEnumerable for extension methods
public class GraphicGroup : IEnumerable<GraphicObject>
{
    private readonly List<GraphicObject> objects = new();
    public string Name { get; set; } = "Group";

    public void Add(GraphicObject obj) => objects.Add(obj);
    public void Remove(GraphicObject obj) => objects.Remove(obj);

    public IEnumerator<GraphicObject> GetEnumerator() => objects.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Extension methods for operations on composites
public static class GraphicExtensions
{
    public static void DrawAll(this IEnumerable<GraphicObject> graphics)
    {
        foreach (var graphic in graphics)
        {
            graphic.Draw();
        }
    }

    public static void SetColor(this IEnumerable<GraphicObject> graphics, string color)
    {
        foreach (var graphic in graphics)
        {
            graphic.Color = color;
        }
    }
}

// Usage:
var drawing = new GraphicObject { Name = "My Drawing" };
drawing.Children.Add(new Square { Color = "Red" });
drawing.Children.Add(new Circle { Color = "Yellow" });

var group = new GraphicObject { Name = "Group 1" };
group.Children.Add(new Circle { Color = "Blue" });
group.Children.Add(new Square { Color = "Green" });

drawing.Children.Add(group);
drawing.Draw();

Decorator Pattern

Purpose: Add behavior to objects dynamically without altering their structure.

// Component interface
public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

// Concrete component
public class SimpleCoffee : ICoffee
{
    public string GetDescription() => "Simple coffee";
    public decimal GetCost() => 2.00m;
}

// Base decorator
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee coffee;

    protected CoffeeDecorator(ICoffee coffee)
    {
        this.coffee = coffee;
    }

    public virtual string GetDescription() => coffee.GetDescription();
    public virtual decimal GetCost() => coffee.GetCost();
}

// Concrete decorators
public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{coffee.GetDescription()}, milk";
    public override decimal GetCost() => coffee.GetCost() + 0.50m;
}

public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{coffee.GetDescription()}, sugar";
    public override decimal GetCost() => coffee.GetCost() + 0.25m;
}

public class WhipDecorator : CoffeeDecorator
{
    public WhipDecorator(ICoffee coffee) : base(coffee) { }

    public override string GetDescription() => $"{coffee.GetDescription()}, whip";
    public override decimal GetCost() => coffee.GetCost() + 0.75m;
}

// Usage:
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhipDecorator(coffee);

Console.WriteLine($"{coffee.GetDescription()} costs ${coffee.GetCost()}");
// Output: Simple coffee, milk, sugar, whip costs $3.50

Modern Functional Approach

public static class CoffeeExtensions
{
    public static ICoffee WithMilk(this ICoffee coffee) => new MilkDecorator(coffee);
    public static ICoffee WithSugar(this ICoffee coffee) => new SugarDecorator(coffee);
    public static ICoffee WithWhip(this ICoffee coffee) => new WhipDecorator(coffee);
}

// Fluent usage:
var coffee = new SimpleCoffee()
    .WithMilk()
    .WithSugar()
    .WithWhip();

Facade Pattern

Purpose: Provide a simplified interface to complex subsystems.

// Complex subsystem classes
public class CPU
{
    public void Freeze() => Console.WriteLine("CPU: Freezing processor");
    public void Jump(long position) => Console.WriteLine($"CPU: Jumping to position {position}");
    public void Execute() => Console.WriteLine("CPU: Executing instructions");
}

public class Memory
{
    public void Load(long position, byte[] data) =>
        Console.WriteLine($"Memory: Loading {data.Length} bytes at position {position}");
}

public class HardDrive
{
    public byte[] Read(long lba, int size)
    {
        Console.WriteLine($"HardDrive: Reading {size} bytes from sector {lba}");
        return new byte[size];
    }
}

// Facade
public class ComputerFacade
{
    private readonly CPU cpu;
    private readonly Memory memory;
    private readonly HardDrive hardDrive;

    public ComputerFacade()
    {
        cpu = new CPU();
        memory = new Memory();
        hardDrive = new HardDrive();
    }

    public void StartComputer()
    {
        Console.WriteLine("Starting computer...");
        cpu.Freeze();
        memory.Load(0, hardDrive.Read(0, 1024));
        cpu.Jump(0);
        cpu.Execute();
        Console.WriteLine("Computer started successfully!");
    }
}

// Usage:
var computer = new ComputerFacade();
computer.StartComputer(); // Simple interface hiding complex operations

Flyweight Pattern

Purpose: Minimize memory usage by sharing efficiently common data among multiple objects.

// Intrinsic state (shared)
public class CharacterFlyweight
{
    private readonly char character;
    private readonly string fontFamily;
    private readonly int fontSize;

    public CharacterFlyweight(char character, string fontFamily, int fontSize)
    {
        this.character = character;
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
    }

    // Operation that uses both intrinsic and extrinsic state
    public void Display(int x, int y, string color) // x, y, color are extrinsic
    {
        Console.WriteLine($"Character: {character}, Font: {fontFamily}, Size: {fontSize}, Position: ({x},{y}), Color: {color}");
    }
}

// Flyweight factory
public class CharacterFlyweightFactory
{
    private static readonly Dictionary<string, CharacterFlyweight> flyweights = new();

    public static CharacterFlyweight GetFlyweight(char character, string fontFamily, int fontSize)
    {
        string key = $"{character}_{fontFamily}_{fontSize}";

        if (!flyweights.ContainsKey(key))
        {
            flyweights[key] = new CharacterFlyweight(character, fontFamily, fontSize);
            Console.WriteLine($"Created new flyweight for: {key}");
        }

        return flyweights[key];
    }

    public static int GetFlyweightCount() => flyweights.Count;
}

// Context that uses flyweights
public class TextDocument
{
    private readonly List<(CharacterFlyweight flyweight, int x, int y, string color)> characters = new();

    public void AddCharacter(char c, string fontFamily, int fontSize, int x, int y, string color)
    {
        var flyweight = CharacterFlyweightFactory.GetFlyweight(c, fontFamily, fontSize);
        characters.Add((flyweight, x, y, color));
    }

    public void Display()
    {
        foreach (var (flyweight, x, y, color) in characters)
        {
            flyweight.Display(x, y, color);
        }
    }
}

// Usage:
var document = new TextDocument();

// Even though we're adding many characters, only unique combinations create flyweights
document.AddCharacter('H', "Arial", 12, 0, 0, "Black");
document.AddCharacter('e', "Arial", 12, 10, 0, "Black");
document.AddCharacter('l', "Arial", 12, 20, 0, "Black");
document.AddCharacter('l', "Arial", 12, 30, 0, "Black"); // Reuses existing flyweight
document.AddCharacter('o', "Arial", 12, 40, 0, "Black");

Console.WriteLine($"Total flyweights created: {CharacterFlyweightFactory.GetFlyweightCount()}");
document.Display();

Proxy Pattern

Purpose: Control access to objects through a surrogate or placeholder.

Protection Proxy

public interface ICar
{
    void Drive();
}

public class Car : ICar
{
    public void Drive()
    {
        Console.WriteLine("Car is being driven");
    }
}

public class CarProxy : ICar
{
    private readonly Car car;
    private readonly Driver driver;

    public CarProxy(Car car, Driver driver)
    {
        this.car = car;
        this.driver = driver;
    }

    public void Drive()
    {
        if (driver.Age >= 16)
        {
            car.Drive();
        }
        else
        {
            Console.WriteLine("Driver too young to drive");
        }
    }
}

public class Driver
{
    public int Age { get; set; }
}

// Usage:
var car = new Car();
var youngDriver = new Driver { Age = 15 };
var carProxy = new CarProxy(car, youngDriver);
carProxy.Drive(); // "Driver too young to drive"

Virtual Proxy (Lazy Loading)

public interface IExpensiveObject
{
    void Process();
}

public class ExpensiveObject : IExpensiveObject
{
    public ExpensiveObject()
    {
        // Simulate expensive initialization
        Console.WriteLine("ExpensiveObject created with heavy initialization");
        Thread.Sleep(1000);
    }

    public void Process()
    {
        Console.WriteLine("Processing with ExpensiveObject");
    }
}

public class ExpensiveObjectProxy : IExpensiveObject
{
    private ExpensiveObject expensiveObject;

    public void Process()
    {
        // Lazy initialization - object created only when needed
        if (expensiveObject == null)
        {
            Console.WriteLine("Creating ExpensiveObject on first use...");
            expensiveObject = new ExpensiveObject();
        }

        expensiveObject.Process();
    }
}

// Usage:
IExpensiveObject proxy = new ExpensiveObjectProxy();
// No expensive object created yet
Console.WriteLine("Proxy created");
// Object is created only when Process is called
proxy.Process();

Caching Proxy

public interface IDataService
{
    Task<string> GetData(string key);
}

public class DatabaseService : IDataService
{
    public async Task<string> GetData(string key)
    {
        // Simulate database call
        Console.WriteLine($"Fetching data from database for key: {key}");
        await Task.Delay(1000);
        return $"Data for {key}";
    }
}

public class CachingProxy : IDataService
{
    private readonly IDataService dataService;
    private readonly Dictionary<string, string> cache = new();

    public CachingProxy(IDataService dataService)
    {
        this.dataService = dataService;
    }

    public async Task<string> GetData(string key)
    {
        if (cache.TryGetValue(key, out var cachedData))
        {
            Console.WriteLine($"Returning cached data for key: {key}");
            return cachedData;
        }

        var data = await dataService.GetData(key);
        cache[key] = data;
        return data;
    }
}

// Usage:
IDataService dataService = new DatabaseService();
IDataService proxy = new CachingProxy(dataService);

var data1 = await proxy.GetData("user123"); // Database call
var data2 = await proxy.GetData("user123"); // Cache hit

Quick Reference

Structural Pattern Comparison

Pattern Intent Problem Solved When to Use When to Avoid
Adapter Make incompatible interfaces compatible Legacy code integration, third-party library mismatch Wrapping existing classes with incompatible interfaces You control both interfaces - fix design
Bridge Separate abstraction from implementation Cartesian product explosion (N × M classes) Multiple dimensions of variation Single dimension of variation
Composite Treat individual and composite objects uniformly Tree structures, hierarchies File systems, UI components, organizational charts Flat structures
Decorator Add behavior dynamically without subclassing Static inheritance limitations Runtime behavior modification, multiple combinations Behavior known at compile time
Facade Simplify complex subsystem interfaces Too many dependencies, complex APIs Hide complexity, reduce coupling Subsystem is already simple
Flyweight Share common state to reduce memory Memory constraints with many similar objects Large number of fine-grained objects Few objects or mostly unique state
Proxy Control access to objects Expensive object creation, access control Lazy loading, caching, access control, logging Direct access is simpler

Pattern Selection Guide

Choose Adapter when:

  • Integrating legacy code
  • Working with third-party libraries with incompatible interfaces
  • Example: Adapting legacy data access to modern repository pattern

Choose Bridge when:

  • Abstraction and implementation vary independently
  • Avoiding N × M class explosion
  • Example: Shapes (Circle, Square) × Renderers (Vector, Raster)

Choose Composite when:

  • Building tree structures
  • Treating individual and groups uniformly
  • Example: File system (files and folders), UI components (controls and panels)

Choose Decorator when:

  • Need to add responsibilities dynamically
  • Subclassing is impractical (too many combinations)
  • Example: Stream decorators (BufferedStream, GZipStream), middleware pipeline

Choose Facade when:

  • Simplifying a complex subsystem
  • Decoupling clients from subsystem components
  • Example: Library initialization, complex API simplification

Choose Flyweight when:

  • Application uses many similar objects
  • Memory is a concern
  • Extrinsic state can be separated from intrinsic state
  • Example: Text editors (character rendering), game objects (particles)

Choose Proxy when:

  • Lazy initialization needed
  • Access control required
  • Caching results
  • Example: ORM lazy loading, image loading, remote service calls

Modern C# Examples

// Adapter - Common with third-party libraries
public class LegacySystemAdapter : IModernInterface
{
    private readonly LegacySystem legacy = new();

    public void ModernMethod()
    {
        legacy.OldMethod();
    }
}

// Bridge - Separate concerns
public abstract class DataSource
{
    protected IDataRenderer renderer;
    protected DataSource(IDataRenderer renderer) => this.renderer = renderer;
}

// Composite - ASP.NET Core middleware
public class CompositeMiddleware
{
    private readonly List<RequestDelegate> middlewares = new();

    public void Add(RequestDelegate middleware) => middlewares.Add(middleware);

    public async Task Invoke(HttpContext context)
    {
        foreach (var middleware in middlewares)
        {
            await middleware(context);
        }
    }
}

// Decorator - Extension methods act as lightweight decorators
public static class StringExtensions
{
    public static string ToTitleCase(this string str) => /* ... */;
    public static string Truncate(this string str, int length) => /* ... */;
}

// Facade - Simplify complex operations
public class OrderProcessingFacade
{
    private readonly IInventoryService inventory;
    private readonly IPaymentService payment;
    private readonly IShippingService shipping;
    private readonly INotificationService notification;

    public async Task ProcessOrder(Order order)
    {
        await inventory.Reserve(order.Items);
        await payment.Process(order.Total);
        await shipping.Schedule(order);
        await notification.SendConfirmation(order.CustomerEmail);
    }
}

// Proxy - Entity Framework lazy loading
public class Order
{
    public virtual ICollection<OrderItem> Items { get; set; } // Proxy created on access
}

Anti-Patterns to Avoid

Over-Decoration:

// BAD: Too many decorators make code hard to understand
var stream = new BufferedStream(
    new GZipStream(
        new CryptoStream(
            new FileStream("data.txt", FileMode.Open),
            encryptor, CryptoStreamMode.Write),
        CompressionMode.Compress));

// BETTER: Create a facade or pipeline builder
var stream = new StreamBuilder()
    .WithFile("data.txt")
    .WithEncryption(encryptor)
    .WithCompression()
    .WithBuffering()
    .Build();

Leaky Facade:

// BAD: Facade exposes internal complexity
public class PaymentFacade
{
    public CreditCardProcessor GetCreditCardProcessor() { } // Leaking internals
    public PayPalGateway GetPayPalGateway() { }
}

// GOOD: Hide implementation details
public class PaymentFacade
{
    public Task<PaymentResult> ProcessPayment(PaymentMethod method, decimal amount) { }
}

Found this guide helpful? Share it with your team:

Share on LinkedIn