CodeOath
← All posts
Architecture & Patterns80 min total · 26 parts

ACID, SOLID, and Common Design Patterns: A Software Design Reference

Contents — Part 22 of 26: Behavioral Patterns: Chain of Responsibility and Iterator
Part 22 of 26 · ~2 min

Behavioral Patterns: Chain of Responsibility and Iterator

Chain of Responsibility passes a request along a chain of handler objects, where each one decides either to handle it or pass it to the next handler in the chain — avoiding one big class or method that has to know about every possible kind of request up front.

public abstract class SupportHandler
{
    protected SupportHandler? Next;
    public SupportHandler SetNext(SupportHandler next) { Next = next; return next; }
    public abstract void Handle(SupportTicket ticket);
}

public class TierOneSupport : SupportHandler
{
    public override void Handle(SupportTicket ticket)
    {
        if (ticket.Severity <= 1) Console.WriteLine("Resolved by Tier 1");
        else Next?.Handle(ticket);
    }
}

public class TierTwoSupport : SupportHandler
{
    public override void Handle(SupportTicket ticket)
    {
        if (ticket.Severity <= 2) Console.WriteLine("Resolved by Tier 2");
        else Next?.Handle(ticket);
    }
}

var tierOne = new TierOneSupport();
tierOne.SetNext(new TierTwoSupport());
tierOne.Handle(new SupportTicket { Severity = 2 }); // "Resolved by Tier 2"

Middleware pipelines (ASP.NET Core's request pipeline, Express.js middleware) are Chain of Responsibility in practice — each middleware either handles the request or calls next() to pass it further down the chain.

Iterator provides a way to access the elements of a collection sequentially without exposing how that collection is actually stored internally — in C#, this is built directly into the language via IEnumerable<T> and yield return, rather than something you typically hand-roll:

public class OddNumberCollection
{
    private readonly List<int> _numbers = new() { 1, 2, 3, 4, 5, 6, 7, 8 };

    public IEnumerable<int> GetOddNumbers()
    {
        foreach (var n in _numbers)
        {
            if (n % 2 != 0) yield return n; // lazily produces the next matching element
        }
    }
}

// Consumed with a plain foreach — no knowledge of the underlying List<int> required.
foreach (var odd in new OddNumberCollection().GetOddNumbers())
{
    Console.WriteLine(odd);
}

yield return is worth understanding precisely: it doesn't build the whole sequence up front — each call to MoveNext() on the resulting iterator runs the method body only up to the next yield return, which is why iterating a yield-based sequence over a huge or even infinite source doesn't require holding it all in memory at once.