Architecture & Patterns110 min total · 26 parts
ACID, SOLID, and Design Patterns: A Complete Software Design Reference
Part 22 of 26 · ~3 min
Behavioral Patterns: Chain of Responsibility and Iterator
Chain of Responsibility strings a series of handler objects together in a line, and each one gets first look at a request — free to act on it, or to shrug and pass it down to whoever's next — rather than cramming every rule anyone might ever need into one method that has to recognize all of them up front. Deciding whether a volunteer can be signed up for a given shift is exactly this kind of layered check, and it maps cleanly onto the capabilities split apart back in the Interface Segregation chapter:
public abstract class EligibilityHandler
{
protected EligibilityHandler? Next;
public EligibilityHandler SetNext(EligibilityHandler next) { Next = next; return next; }
public abstract EligibilityResult Check(Volunteer volunteer, Shift shift);
}
public class BackgroundCheckHandler : EligibilityHandler
{
public override EligibilityResult Check(Volunteer volunteer, Shift shift)
{
if (shift.RequiresBackgroundCheck && !volunteer.HasPassedBackgroundCheck)
return EligibilityResult.Denied("Background check required.");
return Next?.Check(volunteer, shift) ?? EligibilityResult.Approved();
}
}
public class MinimumAgeHandler : EligibilityHandler
{
public override EligibilityResult Check(Volunteer volunteer, Shift shift)
{
if (shift.MinimumAge is int min && volunteer.Age < min)
return EligibilityResult.Denied($"Must be {min}+.");
return Next?.Check(volunteer, shift) ?? EligibilityResult.Approved();
}
}
var chain = new BackgroundCheckHandler();
chain.SetNext(new MinimumAgeHandler());
var result = chain.Check(volunteer, shift);
Worth flagging, because it's the kind of thing that only shows up once a chain is genuinely long: notice every handler ends with Next?.Check(...) ?? EligibilityResult.Approved(), deliberately, rather than Next?.Check(...) alone. Written the second way, a request that reaches the end of the chain with Next set to null silently returns null instead of an explicit answer — no exception, no log line, just a check that quietly evaluated to nothing and left the caller to decide what a missing result means. A chain with no terminal case is a chain with a silent failure mode built into its very last link, and it's an easy thing to miss precisely because the chain "works" for every request that gets handled before reaching the end.
Anyone who's written ASP.NET Core middleware or an Express.js handler has already used this pattern under a different name — every step in the pipeline gets the same choice: act on the request itself, or call next() and let whatever comes after it decide.
Iterator hands a caller a way to step through a collection one element at a time, with zero visibility into whatever data structure is actually holding those elements underneath. C# gives you this as a language feature rather than something assembled by hand, through IEnumerable<T> and the yield return keyword:
public class ShiftRosterList
{
private readonly List<ShiftAssignment> _assignments = new();
public IEnumerable<ShiftAssignment> ConfirmedOnly()
{
foreach (var a in _assignments)
{
if (a.Status == "confirmed") yield return a; // lazily produces the next matching assignment
}
}
}
// Consumed with a plain foreach — no knowledge of the underlying List<ShiftAssignment> required.
foreach (var confirmed in rosterList.ConfirmedOnly())
{
Console.WriteLine(confirmed.VolunteerId);
}
What actually happens at runtime is worth being precise about, because it isn't what it looks like: nothing about this method runs to completion up front. Every time something calls MoveNext() on the iterator it produces, execution resumes exactly where it left off and advances only until the next yield return, then pauses again. A collection with a million entries and a genuinely endless source behave identically from the caller's side — neither one has to be built up in memory before the first result is usable.