Software Design Patterns: Behavioural

This post continues my series on the Gang of Four design patterns. Having covered the creational and structural patterns previously, this post turns to the behavioural patterns – those concerned with how objects communicate with one another and how responsibilities are distributed between them. All the accompanying source code for this series can be found in my design patterns repository on GitHub:
Each pattern has its own console application project, so you can clone the repository and step through the code as you read.
The behavioural patterns covered in this post are:
- Chain of Responsibility pattern
- Command pattern
- Event Aggregator pattern
- Interpreter pattern
- Iterator pattern (Iterator example, Tree example)
- Mediator pattern
- Memento pattern
- Null Object pattern
- Observer pattern (Traditional, IObserver, EventDelegate)
- Rules pattern
- State pattern
- Strategy pattern
- Template Method pattern
- Visitor pattern
Chain of Responsibility pattern
The Chain of Responsibility pattern decouples the sender of a request from the object that ultimately handles it. Rather than the sender knowing exactly which object should process a request, the request is passed along a chain of potential handlers until one of them takes responsibility for it. The easiest way to understand this pattern is with a real-world example using the expense workflow within an organisation. In this example, there is an ExpenseReport class that represents an expense report with a constructor parameter that takes in the expense amount:
public class ExpenseReport : IExpenseReport
{
public ExpenseReport(decimal total)
{
Total = total;
}
public decimal Total
{
get;
}
}There are four levels of approval - Manager, Director, Vice President and President - all instances of the ExpenseHandler class:
private static ExpenseHandler CreateReportingChain()
{
ExpenseHandler manager = new ExpenseHandler(new Employee("Manager", decimal.Zero));
ExpenseHandler director = new ExpenseHandler(new Employee("Director", new decimal(1000)));
ExpenseHandler vicePresident = new ExpenseHandler(new Employee("Vice President", new decimal(5000)));
ExpenseHandler president = new ExpenseHandler(new Employee("President", new decimal(20000)));
manager.RegisterNext(director);
director.RegisterNext(vicePresident);
vicePresident.RegisterNext(president);
return manager;
}The ExpenseHandler class itself has an Approve() and RegisterNext methods. Approve() is used to approve the expense report, while RegisterNext is used to register the next handler in the chain:
public class ExpenseHandler : IExpenseHandler
{
private readonly IExpenseApprover _approver;
private IExpenseHandler _next;
public ExpenseHandler(IExpenseApprover expenseApprover)
{
_approver = expenseApprover;
_next = EndOfChainExpenseHandler.Instance;
}
public ApprovalResponse Approve(IExpenseReport expenseReport)
{
ApprovalResponse response = _approver.ApproveExpense(expenseReport);
if (response == ApprovalResponse.BeyondApprovalLimit)
{
return _next.Approve(expenseReport);
}
return response;
}
public void RegisterNext(IExpenseHandler next)
{
_next = next;
}
}Whether an instance of the Employee class can approve the expense report depends on their level of authority. For example, a Manager can approve expenses up to a certain amount, while a Director can approve expenses up to a higher amount, and so on. This is achieved through the approvalLimit parameter in the Employee class constructor:
public class Employee : IExpenseApprover
{
private readonly decimal _approvalLimit;
public Employee(string name, decimal approvalLimit)
{
Name = name;
_approvalLimit = approvalLimit;
}
public string Name { get; private set; }
public ApprovalResponse ApproveExpense(IExpenseReport expenseReport)
{
return expenseReport.Total > _approvalLimit
? ApprovalResponse.BeyondApprovalLimit
: ApprovalResponse.Approved;
}
}An instance of an IExpenseReport is passed to the ApproveExpense method of the Employee class, which then returns an ApprovalResponse indicating whether the expense report was approved or not. Requests of increasing amounts exercise the whole chain – a 500 request is approved by the Director, a 1,250 request by the Vice President, a 5,500 request by the President, and a 25,000 request falls off the end of the chain and is denied:
private static void RequestCheapExpense(ExpenseHandler manager)
{
IExpenseReport cheapExpenseReport = new ExpenseReport(500);
ApprovalResponse cheapExpenseResponse = manager.Approve(cheapExpenseReport);
Console.WriteLine("The request for {0} was {1}.", cheapExpenseReport.Total, cheapExpenseResponse);
// The request for 500 was Approved.
}
private static void RequestMildlyCheapExpense(ExpenseHandler manager)
{
IExpenseReport mildlyCheapExpenseReport = new ExpenseReport(1250);
ApprovalResponse mildlyCheapExpenseResponse = manager.Approve(mildlyCheapExpenseReport);
Console.WriteLine("The request for {0} was {1}.", mildlyCheapExpenseReport.Total, mildlyCheapExpenseResponse);
// The request for 1250 was Approved. (amended: output comments corrected to match each request)
}
private static void RequestRatherExpensiveExpense(ExpenseHandler manager)
{
IExpenseReport ratherExpensiveExpenseReport = new ExpenseReport(5500);
ApprovalResponse ratherExpensiveExpenseResponse = manager.Approve(ratherExpensiveExpenseReport);
Console.WriteLine("The request for {0} was {1}.", ratherExpensiveExpenseReport.Total, ratherExpensiveExpenseResponse);
// The request for 5500 was Approved.
}
private static void RequestStupidlyExpensiveExpense(ExpenseHandler manager)
{
IExpenseReport stupidlyExpensiveExpenseReport = new ExpenseReport(25000);
ApprovalResponse stupidlyExpensiveResponse = manager.Approve(stupidlyExpensiveExpenseReport);
Console.WriteLine("The request for {0} was {1}.", stupidlyExpensiveExpenseReport.Total, stupidlyExpensiveResponse);
// The request for 25000 was Denied. (25000 exceeds the President's 20000 limit, so the
// request reaches the EndOfChainExpenseHandler)
}Benefits of the Chain of Responsibility pattern:
- Decoupling – the sender of a request does not need to know which object will ultimately handle it.
- Flexibility – new handlers can be added to (or removed from) the chain without changing the code that issues requests.
- Single responsibility – each handler only contains the logic for its own level of authority, making handlers easy to test in isolation.
- Configurable processing order – the chain itself can be composed at runtime rather than hard-coded into the caller.
In short, the Chain of Responsibility pattern is a good fit whenever a request needs to travel through a sequence of potential handlers, such as approval workflows, logging pipelines or middleware-style processing.
Command pattern
The Command pattern encapsulates a request as an object, thereby allowing you to parameterise clients with different requests, queue or log requests, and support undoable operations. Crucially, it separates the object that invokes an operation from the object that knows how to perform it. In this example, there are four command classes - DiveCommand, LaunchNukeCommand, MakeItSoCommand and NotFoundCommand:
// DiveCommand
public class DiveCommand : ICommand, ICommandFactory
{
public int NumberOfMetres { get; set; }
public string CommandName
{
get { return "Dive"; }
}
public string Description
{
get { return "Dive [NumberOfMetres]"; }
}
public void Execute()
{
Console.WriteLine("Diving to {0} metres...", NumberOfMetres);
}
public ICommand MakeCommand(string[] arguments)
{
return new DiveCommand
{
NumberOfMetres = int.Parse(arguments[1])
};
}
}
// LaunchNukeCommand
public class LaunchNukeCommand : ICommand, ICommandFactory
{
public int NumberOfNukes { get; set; }
public string CommandName
{
get
{
return "LaunchNuke";
}
}
public string Description
{
get
{
return "LaunchNuke [NumberOfNukes]";
}
}
public void Execute()
{
for (int i = 0; i < NumberOfNukes; i++)
{
Console.WriteLine("Launching nuke {0}", i + 1);
}
}
public ICommand MakeCommand(string[] arguments)
{
return new LaunchNukeCommand
{
NumberOfNukes = int.Parse(arguments[1])
};
}
}
// MakeItSoCommand
public class MakeItSoCommand : ICommand, ICommandFactory
{
public string CommandName
{
get { return "MakeItSo"; }
}
public string Description
{
get { return CommandName; }
}
public void Execute()
{
Console.WriteLine("Made it so!");
}
public ICommand MakeCommand(string[] arguments)
{
return new MakeItSoCommand();
}
}
// NotFoundCommand
public class NotFoundCommand : ICommand
{
public string Name { get; set; }
public void Execute()
{
Console.WriteLine("Command not found: " + Name);
}
}The CommandParser class has an IEnumerable<ICommandFactory> of _availableCommands which is populated by the constructor:
public class CommandParser
{
private readonly IEnumerable<ICommandFactory> _availableCommands;
public CommandParser(IEnumerable<ICommandFactory> availableCommands)
{
_availableCommands = availableCommands;
}
internal ICommand ParseCommand(string[] args)
{
var requestedCommandName = args[0];
var command = FindRequestedCommand(requestedCommandName);
if (command == null)
{
return new NotFoundCommand { Name = requestedCommandName };
}
return command.MakeCommand(args);
}
private ICommandFactory FindRequestedCommand(string commandName)
{
return _availableCommands
.FirstOrDefault(cmd => cmd.CommandName == commandName);
}
}A command is then invoked by the ParseCommand method which finds the requested command in the list if it exists and then invokes the ICommandFactory.MakeCommand() method. This method then returns the newly created command to the calling method, which then invokes the ICommand.Execute() method, implemented by the respective command class implementation.
public static void Main(string[] args)
{
var availableCommands = GetAvailableCommands();
if (args.Length == 0)
{
PrintUsage(availableCommands);
return;
}
var parser = new CommandParser(availableCommands);
var command = parser.ParseCommand(args);
command.Execute();
}
private static IEnumerable<ICommandFactory> GetAvailableCommands()
{
return new ICommandFactory[]
{
new DiveCommand(),
new LaunchNukeCommand(),
new MakeItSoCommand()
};
}
private static void PrintUsage(IEnumerable<ICommandFactory> availableCommands)
{
Console.WriteLine("Usage: CommandPattern CommandName Arguments");
Console.WriteLine("Commands:");
foreach (var command in availableCommands)
{
Console.WriteLine(" {0}", command.Description);
}
}Benefits of the Command pattern are:
- Separation of concerns – the invoker knows nothing about how a command is carried out, only that it can be executed.
- Extensibility – new commands can be added without touching existing parsing or dispatch logic.
- Composability – because commands are objects, they can be queued, logged, scheduled, serialised or grouped into macros.
- Graceful degradation - unknown commands can be handled uniformly, as shown by the
NotFoundCommand.
In short, the Command pattern shines wherever you need to turn actions into first-class objects – from menu systems and job queues to undo/redo stacks.
Event Aggregator pattern
Quite possibly one of the more complex patterns, the Event Aggregator pattern is used to decouple publishers and subscribers. Rather than components holding direct references to each other in order to raise notifications, they publish and subscribe through a central aggregator, which routes events to any interested parties. In this example, the SimpleEventAggregator class has a Dictionary<Type, List<WeakReference>> of _eventSubscriberLists:
public class SimpleEventAggregator : IEventAggregator
{
private readonly Dictionary<Type, List<WeakReference>> _eventSubscriberLists = new Dictionary<Type, List<WeakReference>>();
private readonly object _lock = new object();
public void Subscribe(object subscriber)
{
lock (_lock)
{
var subscriberTypes =
subscriber.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISubscriber<>));
var weakReference = new WeakReference(subscriber);
foreach (var subscriberType in subscriberTypes)
{
var subscribers = GetSubscribers(subscriberType);
subscribers.Add(weakReference);
}
}
}
public void Publish<TEvent>(TEvent eventToPublish)
{
var subscriberType = typeof(ISubscriber<>).MakeGenericType(typeof(TEvent));
var subscribers = GetSubscribers(subscriberType);
List<WeakReference> subscribersToRemove = new List<WeakReference>();
foreach (var weakSubscriber in subscribers)
{
if (weakSubscriber.IsAlive)
{
var subscriber = (ISubscriber<TEvent>)weakSubscriber.Target;
var syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
syncContext.Post(s => subscriber.OnEvent(eventToPublish), null);
}
else
{
subscribersToRemove.Add(weakSubscriber);
}
}
if (subscribersToRemove.Any())
{
lock (_lock)
{
foreach (var remove in subscribersToRemove)
{
subscribers.Remove(remove);
}
}
}
}
private List<WeakReference> GetSubscribers(Type subscriberType)
{
List<WeakReference> subscribers;
lock (_lock)
{
var found = _eventSubscriberLists.TryGetValue(subscriberType, out subscribers);
if (!found)
{
subscribers = new List<WeakReference>();
_eventSubscriberLists.Add(subscriberType, subscribers);
}
}
return subscribers;
}
}A subscriber is then added to the list via the Subscribe method which adds the subscriber to the list if it implements the ISubscriber<T> interface.
Events on orders are represented by the OrderCreated, OrderSaved and OrderSelected classes:
public class OrderCreated
{
public Order Order { get; set; }
}
public class OrderSaved
{
public Order Order { get; set; }
}
public class OrderSelected
{
public Order Order { get; set; }
}Whenever the OrdersListView.SelectOrder() method is invoked, the IEventAggregator.Publish() method is invoked:
public class OrdersListView
{
private IList<Order> _orders;
public IEventAggregator EventAggregator { get; set; }
public void SelectOrder(int orderNumber)
{
Order order = _orders.SingleOrDefault(o => o.OrderNumber == orderNumber);
if (order != null)
{
EventAggregator.Publish(new OrderSelected { Order = order });
}
}
public void SetOrders(IList<Order> orders)
{
_orders = orders;
}
}This, in turn, retrieves all the subscribers, checks the WeakReference.IsAlive property to determine whether the referenced object has not been garbage collected, and then uses SynchronizationContext.Post() to invoke the ISubscriber.OnEvent() method, activating any subscribers - in this case OrderDetailView and OrderHistoryView:
// OrderDetailView
public class OrderDetailView : ISubscriber<OrderSelected>, ISubscriber<OrderSaved>
{
public OrderDetailView(IEventAggregator eventAggregator)
{
eventAggregator.Subscribe(this);
}
public void OnEvent(OrderSelected e)
{
Console.WriteLine("OrderDetailView -> OrderSelected Event -> {0}", e.Order);
}
public void OnEvent(OrderSaved e)
{
Console.WriteLine("OrderSavedView -> OrderSaved Event -> {0}", e.Order);
}
}
// OrderHistoryView
public class OrderHistoryView : ISubscriber<OrderSelected>, ISubscriber<OrderSaved>
{
public OrderHistoryView(IEventAggregator eventAggregator)
{
eventAggregator.Subscribe(this);
}
public void OnEvent(OrderSelected e)
{
Console.WriteLine("OrderHistoryView -> OrderSelected Event -> {0}", e.Order);
}
public void OnEvent(OrderSaved e)
{
Console.WriteLine("OrderHistoryView -> OrderSaved Event -> {0}", e.Order);
}
}These are initially wired up to the SimpleEventAggregator via their respective constructor methods:
_eventAggregator = new SimpleEventAggregator();
_orderListView = new OrdersListView();
_orderDetailView = new OrderDetailView(_eventAggregator);
_orderHistoryView = new OrderHistoryView(_eventAggregator);
_orderView = new OrderView(_eventAggregator);
var orders = GenerateOrders();
_orderListView.EventAggregator = _eventAggregator;
_orderListView.SetOrders(orders);Benefits of the Event Aggregator pattern:
- Loose coupling – publishers and subscribers never reference each other directly; they only know about the aggregator.
- Memory safety - the use of
WeakReferencemeans subscribers that are garbage collected are silently dropped rather than leaking. - Centralised eventing – the routing logic lives in one place rather than being scattered across every component.
- Cross-thread delivery - dispatching via
SynchronizationContextallows events to arrive on the correct thread for UI consumers.
In short, the Event Aggregator pattern is invaluable in complex applications – particularly UI-heavy ones such as WPF or Prism applications – where many components need to react to events without becoming entangled with each other.
Interpreter pattern
The Interpreter pattern is used to simplify complex expressions by breaking them down into smaller, more manageable parts. A grammar is represented as a hierarchy of expression classes, each of which knows how to interpret its own piece of the language, and evaluating the full expression becomes a matter of recursing through that hierarchy. It is often used in compilers, rule engines and query languages to parse and evaluate expressions.
In this example we have a CondimentList and an IngredientList:
// CondimentList
public class CondimentList : IExpression
{
private readonly List<ICondiment> _condiments;
public CondimentList(List<ICondiment> condiments)
{
_condiments = condiments;
}
public void Interpret(Context context)
{
foreach (var condiment in _condiments)
{
condiment.Interpret(context);
}
}
}
// IngredientList
public class IngredientList : IExpression
{
private readonly List<IIngredient> _ingredients;
public IngredientList(List<IIngredient> ingredients)
{
_ingredients = ingredients;
}
public void Interpret(Context context)
{
foreach (var ingredient in _ingredients)
{
ingredient.Interpret(context);
}
}
}An IBread interface that extends the IExpression interface with two concrete implementations - WhiteBread and WheatBread:
// IExpression
public interface IExpression
{
void Interpret(Context context);
}
// IBread
public interface IBread : IExpression
{
}
// WhiteBread
public class WhiteBread : IBread
{
public void Interpret(Context context)
{
context.Output += string.Format(" {0} ", "White-IBread");
}
}
// WheatBread
public class WheatBread : IBread
{
public void Interpret(Context context)
{
context.Output += string.Format(" {0} ", "Wheat-IBread");
}
}Each of these classes implements the IExpression interface which has one method: Interpret() which takes a parameter of type Context.
All of this is then glued together with a Sandwich class which itself implements the IExpression interface and has a corresponding Interpret() method:
// BNF:
// <sandwhich> ::= <bread> <condimentList> <ingredientList> <condimentList> <bread>
// <condimentList> ::= { <condiment> }
// <ingredientList> ::= { <ingredient> }
// <bread> ::= <whiteBread> | <wheatBread>
// <condiment> ::= <mayoCondiment> | <mustardCondiment> | <ketchupCondiment>
// <ingredient> ::= <lettuceIngredient> | <tomatoIngredient> | <chickenIngredient>
public class Sandwich : IExpression
{
private readonly IBread _topBread;
private readonly CondimentList _topCondiments;
private readonly IngredientList _ingredients;
private readonly CondimentList _bottomCondiments;
private readonly IBread _bottomBread;
public Sandwich(IBread topBread, CondimentList topCondiments, IngredientList ingredients, CondimentList bottomCondiments, IBread bottomBread)
{
_topBread = topBread;
_topCondiments = topCondiments;
_ingredients = ingredients;
_bottomCondiments = bottomCondiments;
_bottomBread = bottomBread;
}
public void Interpret(Context context)
{
context.Output += "|";
_topBread.Interpret(context);
context.Output += "|";
context.Output += "<--";
_topCondiments.Interpret(context);
context.Output += "-";
_ingredients.Interpret(context);
context.Output += "-";
_bottomCondiments.Interpret(context);
context.Output += "-->";
context.Output += "|";
_bottomBread.Interpret(context);
context.Output += "|";
Console.WriteLine(context.Output);
}
}The Interpret() method is then responsible for invoking the Interpret() method on each of the IExpression objects in order for the respective CondimentList and IngredientList instances and then returning the result:
var sandwich = new Sandwich(
new WheatBread(),
new CondimentList(
new List<ICondiment> { new MayoCondiment(), new MustardCondiment() }),
new IngredientList(
new List<IIngredient> { new LettuceIngredient(), new ChickenIngredient() }),
new CondimentList(new List<ICondiment> { new KetchupCondiment() }),
new WheatBread());
sandwich.Interpret(new Context());Benefits of the Interpreter pattern:
- Grammar as code – each rule of the language maps to a small class, keeping individual pieces simple.
- Easy extension – new vocabulary (new breads, condiments or ingredients) can be added without altering the interpretation machinery.
- Composability - complex expressions are built by composing simpler ones, mirroring the grammar itself.
- Testability – each terminal expression can be tested in isolation against a known
Context.
In short, reach for the Interpreter pattern when you need to evaluate a simple, well-defined language – but recognise that grammars of any size quickly favour parser generators or dedicated engines instead.
Iterator pattern
The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. By externalising traversal into its own object, the same aggregate can be traversed in multiple ways – or the same traversal applied to different structures – without duplicating looping logic everywhere.
In my example this particular pattern is split into two implementations: IteratorExample and TreeExample.
IteratorExample
This example uses a flat list of stock symbols which are stored in different data structures and then iterated over differently depending on the data structure:
public static void ArrayIterationExample()
{
Console.WriteLine("--- Array iteration example start ---");
var stocks = new[] { "MSFT", "GOOG", "AAPL" };
for (int i = 0; i < stocks.Length; i++)
{
Console.WriteLine(stocks[i]);
}
Console.WriteLine("--- Array iteration example end ---");
}
public static void EnumeratorExample()
{
Console.WriteLine("--- Enumeration iteration example start ---");
var stocks = new[] { "MSFT", "GOOG", "AAPL" };
IEnumerator enumerator = stocks.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
Console.WriteLine("--- Enumeration iteration example end ---");
}
public static void ForEachExample()
{
Console.WriteLine("--- For each iteration example start ---");
var stocks = new[] { "MSFT", "GOOG", "AAPL" };
foreach (string stock in stocks)
{
Console.WriteLine(stock);
}
Console.WriteLine("--- For each iteration example end ---");
}
public static void ListIterationExample()
{
Console.WriteLine("--- List iteration example start ---");
var stocks = new SuperCollection { "MSFT", "GOOG", "AAPL" };
for (int i = 0; i < stocks.Count; i++)
{
Console.WriteLine(stocks.Get(i));
}
Console.WriteLine("--- List iteration example end ---");
}ArrayIterationExample()creates a standard array and uses aforloop to iterate over each stock symbolEnumeratorExample()also creates a standard array but uses anIEnumeratorfrom theGetEnumerator()function to iterate over each stock symbolForEachExample()also creates a standard array but uses theforeachkeyword to iterate over each stock symbolListIterationExample()creates an instance of theSuperCollectionclass which inherits fromList<string>and uses aforloop to iterate over each stock symbol
TreeExample
A slightly more complex example using a tree data structure, CreateTreeWithValues() instantiates a Tree<int> class and uses a for loop to create x number of values in a tree structure:
private static Tree<int> CreateTreeWithValues(int numberOfValues)
{
if (numberOfValues <= 0)
{
return null;
}
var tree = new Tree<int>(1);
for (int i = 2; i <= numberOfValues; i++)
{
tree.Add(i);
}
return tree;
}The Tree<T> class exposes four methods - Add(), Depth(), Children() and ToList():
Add()appends the given value to the tree, which itself is represented by an instance ofTree<T>. First, the value is added to theLeftChildif there is no value, otherwise it is added toRightChildif there is no value. Otherwise, the depth of theLeftChildtree is compared against the depth of theRightChildtree and if theRightChildtree has more items it is added to theLeftChildtree.Depth()simply looks to see if either theLeftChildorRightChildis null and returns 0 if so, otherwise it returns 1 plus the maximum depth of theLeftChildandRightChildtrees.Children()returns a list of theLeftChildandRightChildtrees.ToList()instantiates aList<T>and adds theLeftChilditems and then theRightChilditems before returning the list to the caller.
[Serializable]
public class Tree<T> : IEnumerable<T>
{
public Tree()
{
}
public Tree(T value)
{
Value = value;
}
public T Value { get; set; }
public Tree<T> LeftChild { get; set; }
public Tree<T> RightChild { get; set; }
public bool UseBreadthFirstEnumerator { get; set; }
public void Add(T value)
{
if (LeftChild == null)
{
LeftChild = new Tree<T>(value);
return;
}
if (RightChild == null)
{
RightChild = new Tree<T>(value);
return;
}
if (LeftChild.Depth() <= RightChild.Depth())
{
LeftChild.Add(value);
return;
}
RightChild.Add(value);
}
public int Depth()
{
if (LeftChild == null || RightChild == null)
{
return 0;
}
return 1 + Math.Max(LeftChild.Depth(), RightChild.Depth());
}
public IEnumerable<Tree<T>> Children()
{
if (LeftChild != null)
{
yield return LeftChild;
}
if (RightChild != null)
{
yield return RightChild;
}
}
public List<T> ToList()
{
var myList = new List<T>();
myList.Add(Value);
if (LeftChild != null)
{
myList.AddRange(LeftChild.ToList());
}
if (RightChild != null)
{
myList.AddRange(RightChild.ToList());
}
return myList;
}
public IEnumerator<T> GetEnumerator()
{
if (UseBreadthFirstEnumerator)
{
return new TreeBreadthFirstEnumerator<T>(this);
}
return new TreeEnumerator<T>(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}Tree<T> also implements IEnumerable<T> to enable iteration and implements the GetEnumerator method using two classes - TreeBreadthFirstEnumerator, which implements the IEnumerator<T> interface and uses a Queue<T> to iterate through the tree in breadth-first order:
public class TreeBreadthFirstEnumerator<T> : IEnumerator<T>
{
private readonly Queue<IEnumerator<Tree<T>>> _enumerators = new Queue<IEnumerator<Tree<T>>>();
private readonly Tree<T> _tree;
private Tree<T> _current;
public TreeBreadthFirstEnumerator(Tree<T> tree)
{
_tree = tree;
}
public T Current
{
get { return _current.Value; }
}
object IEnumerator.Current
{
get { return Current; }
}
public void Dispose()
{
}
public bool MoveNext()
{
if (_current == null)
{
Reset();
_current = _tree;
_enumerators.Enqueue(_current.Children().GetEnumerator());
return true;
}
while (_enumerators.Count > 0)
{
var enumerator = _enumerators.Peek();
if (enumerator.MoveNext())
{
_current = enumerator.Current;
_enumerators.Enqueue(_current.Children().GetEnumerator());
return true;
}
_enumerators.Dequeue();
}
return false;
}
public void Reset()
{
_current = null;
_enumerators.Clear();
}
}And TreeEnumerator which implements the IEnumerator<T> interface and uses a Stack<T> to iterate through the tree in depth-first order:
public class TreeEnumerator<T> : IEnumerator<T>
{
private readonly Stack<Tree<T>> _breadcrumb = new Stack<Tree<T>>();
private readonly Tree<T> _tree;
private Tree<T> _current;
private Tree<T> _previous;
public TreeEnumerator(Tree<T> tree)
{
_tree = tree;
}
public T Current
{
get { return _current.Value; }
}
object IEnumerator.Current
{
get { return Current; }
}
public void Dispose()
{
}
public bool MoveNext()
{
if (_current == null)
{
Reset();
_current = _tree;
return true;
}
if (_current.LeftChild != null)
{
return TraverseLeft();
}
if (_current.RightChild != null)
{
return TraverseRight();
}
return TraverseUpAndRight();
}
private bool TraverseUpAndRight()
{
if (_breadcrumb.Count > 0)
{
_previous = _current;
while (true)
{
_current = _breadcrumb.Pop();
if (_previous != _current.RightChild)
{
break;
}
}
if (_current.RightChild != null)
{
_breadcrumb.Push(_current);
_current = _current.RightChild;
return true;
}
}
return false;
}
private bool TraverseLeft()
{
_breadcrumb.Push(_current);
_current = _current.LeftChild;
return true;
}
private bool TraverseRight()
{
_breadcrumb.Push(_current);
_current = _current.RightChild;
return true;
}
public void Reset()
{
_current = null;
}
}In the example code, the Tree<T> class is used to create a binary tree structure and demonstrate the use of the IEnumerable<T> interface and the GetEnumerator method. The TreeBreadthFirstEnumerator and TreeEnumerator classes are used to iterate through the tree in breadth-first and depth-first order, respectively.
Benefits of the Iterator pattern:
- Encapsulated traversal – consumers never need to know whether they are walking an array, a list or a tree.
- Multiple traversal strategies – the same collection can offer depth-first, breadth-first or any other order side by side.
- Simultaneous traversals – because iteration state lives in the enumerator object rather than the collection, several iterations can run independently.
- Clean consumer code - the
foreachconstruct hides all of the mechanics behind a single, familiar syntax.
In short, if you are building custom collections in .NET, implementing IEnumerable<T> with tailored enumerators is the idiomatic way to control how your data is walked.
Mediator pattern
The Mediator pattern centralises communication between a set of objects into a single mediator object. Instead of colleagues referring to each other directly - which quickly produces a tangle of bidirectional dependencies – they report to the mediator, which decides how the rest of the group should respond.
Using an abstract Aircraft class to represent different types of aircraft, the Mediator pattern is demonstrated by creating an AirTrafficControl class that acts as the mediator between the aircraft and the airport. The AirTrafficControl class maintains a list of aircraft and provides a RegisterAircraftUnderGuidance() method for adding instances of Aircraft:
public class AirTrafficControl : IAirTrafficControl
{
private const int MinimumIntrusionAltitude = 1000;
private readonly IList<Aircraft> _aircraftUnderGuidance = new List<Aircraft>();
public void ReceiveAircraftLocation(Aircraft reportingAircraft)
{
foreach (Aircraft currentAircraftUnderGuidance in _aircraftUnderGuidance.Where(x => !x.Equals(reportingAircraft)))
{
if (Math.Abs(currentAircraftUnderGuidance.Altitude - reportingAircraft.Altitude) < MinimumIntrusionAltitude)
{
currentAircraftUnderGuidance.WarnOfAirspaceIntrusionBy(reportingAircraft);
reportingAircraft.Climb(MinimumIntrusionAltitude);
}
}
}
public void RegisterAircraftUnderGuidance(Aircraft aircraft)
{
if (!_aircraftUnderGuidance.Contains(aircraft))
{
_aircraftUnderGuidance.Add(aircraft);
}
}
}The five implementations of the Aircraft class - Junkers, Lancaster, Mustang, Spitfire and Stuka - each override a Ceiling property that returns the maximum altitude the aircraft can fly:
// Junkers
public class Junkers : Aircraft
{
public Junkers(string callSign, IAirTrafficControl atc)
: base(callSign, atc)
{
}
public override int Ceiling
{
get
{
return 11150;
}
}
}
// Lancaster
public class Lancaster : Aircraft
{
public Lancaster(string callSign, IAirTrafficControl atc)
: base(callSign, atc)
{
}
public override int Ceiling
{
get
{
return 21400;
}
}
}
// Mustang
public class Mustang : Aircraft
{
public Mustang(string callSign, IAirTrafficControl atc)
: base(callSign, atc)
{
}
public override int Ceiling
{
get
{
return 41900;
}
}
}
// Spitfire
public class Spitfire : Aircraft
{
public Spitfire(string callSign, IAirTrafficControl atc)
: base(callSign, atc)
{
}
public override int Ceiling
{
get
{
return 36500;
}
}
}
// Stuka
public class Stuka : Aircraft
{
public Stuka(string callSign, IAirTrafficControl atc)
: base(callSign, atc)
{
}
public override int Ceiling
{
get
{
return 29603;
}
}
}Within the base Aircraft class, an instance of the IAirTrafficControl interface is passed via the constructor. This allows the AirTrafficControl class to be notified when the Altitude property of an aircraft is set and take appropriate action, such as instructing the reporting aircraft to climb away from other traffic:
public abstract class Aircraft
{
private readonly IAirTrafficControl _atc;
private int _currentAltitude;
protected Aircraft(string callSign, IAirTrafficControl atc)
{
_atc = atc;
CallSign = callSign;
_atc.RegisterAircraftUnderGuidance(this);
}
public abstract int Ceiling { get; }
public string CallSign { get; private set; }
public int Altitude
{
get
{
return _currentAltitude;
}
set
{
_currentAltitude = value;
_atc.ReceiveAircraftLocation(this);
}
}
public void Climb(int heightToClimb)
{
Altitude += heightToClimb;
Console.WriteLine("{0} climbing to {1}ft", CallSign, Altitude);
}
public override bool Equals(object obj)
{
if (obj.GetType() != GetType())
{
return false;
}
var incoming = (Aircraft)obj;
return CallSign.Equals(incoming.CallSign);
}
public override int GetHashCode()
{
return CallSign.GetHashCode();
}
public void WarnOfAirspaceIntrusionBy(Aircraft reportingAircraft)
{
Console.WriteLine("Airspace intrusion for {0} by {1}", CallSign, reportingAircraft.CallSign);
}
}This is achieved in the IAirTrafficControl.ReceiveAircraftLocation() implementation by iterating through the list of aircraft and checking whether the absolute difference in altitude between the reporting aircraft and any other aircraft is less than the MinimumIntrusionAltitude of 1,000 feet:
var airTrafficControl = new AirTrafficControl();
var fighter1 = new Junkers("JC123", airTrafficControl);
var fighter2 = new Lancaster("LA456", airTrafficControl);
var fighter3 = new Mustang("MS789", airTrafficControl);
var fighter4 = new Spitfire("SF3121", airTrafficControl);
var fighter5 = new Stuka("ST6059", airTrafficControl);
fighter1.Altitude += 25000;
fighter2.Altitude += 25000;
fighter3.Altitude += 25000;
fighter4.Altitude += 25000;
fighter5.Altitude += 25000;If so, the Aircraft.WarnOfAirspaceIntrusionBy() method is called on the affected aircraft, and the reporting aircraft is told to climb by the MinimumIntrusionAltitude value via its Climb() method to prevent a collision.
Benefits of the Mediator pattern:
- Reduced coupling – colleagues know only the mediator, not each other, so aircraft can come and go freely.
- Centralised coordination - the collision-avoidance rules live in one place instead of being duplicated across every aircraft.
- Easier maintenance – changing how aircraft interact means editing the mediator alone.
- Clearer domain modelling – the real-world role of air traffic control maps naturally onto the code.
In short, whenever a group of objects must coordinate with one another and direct references would create a mesh of dependencies, a mediator is usually the cleaner answer.
Memento pattern
The Memento pattern captures and externalises an object’s internal state so that it can be restored later, without violating encapsulation. The originator snapshots its own state into a memento object, which a caretaker stores – and possibly stacks – without ever inspecting its contents. It is the classic backbone of undo functionality.
The best way to represent this pattern was to use a WPF application whereby a version of the InkCanvas class extends the base functionality through the use of an IMemento interface with a simple State property to capture the Stroke instances created when interacting with the canvas:
public interface IMemento
{
object State { get; set; }
}The InkCanvasWithUndo implementation exposes a CreateMemento() method that returns an IMemento instance containing the current state of the canvas, and a SetMemento() method that restores the canvas to a previous state by setting the Stroke instances to the value of the State property of the passed in IMemento instance:
public class InkCanvasWithUndo : InkCanvas
{
public IMemento CreateMemento()
{
Stroke[] copy = Strokes.ToArray();
return new InkCanvasMemento { State = copy };
}
public void SetMemento(IMemento memento)
{
Strokes = new StrokeCollection((Stroke[])memento.State);
}
public class InkCanvasMemento : IMemento
{
public object State { get; set; }
}
}To support all this, the MainWindow class of the WPF application hooks into the InkCanvasWithUndo_MouseUp event to create a Memento instance when the user releases the mouse button, and adds it to a list of Memento instances representing previous states of the canvas. The MainWindow class also hooks into the CommandBindings collection to register a method to be executed when the user presses the undo combination, CTRL+Z:
public partial class MainWindow
{
private readonly Stack<IMemento> _states = new Stack<IMemento>();
public MainWindow()
{
InitializeComponent();
CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, OnExecutedCommands));
InkCanvasWithUndo1.MouseUp += InkCanvasWithUndo_MouseUp;
StoreState();
}
private static void OnExecutedCommands(object sender, ExecutedRoutedEventArgs e)
{
var myWindow = (MainWindow)sender;
if (e.Command == ApplicationCommands.Undo)
{
myWindow.Undo();
}
}
private void StoreState()
{
var memento = InkCanvasWithUndo1.CreateMemento();
_states.Push(memento);
StatesCountLabel.Content = _states.Count;
}
private void InkCanvasWithUndo_MouseUp(object sender, MouseButtonEventArgs e)
{
StoreState();
}
private void Undo()
{
if (_states.Count > 1)
{
_states.Pop();
var lastState = _states.Peek();
InkCanvasWithUndo1.SetMemento(lastState);
}
StatesCountLabel.Content = _states.Count;
}
}This restores the canvas to a previous state by calling the Undo() method which pops the last IMemento instance from the stack of IMemento instances and calls the SetMemento() method of the InkCanvasWithUndo instance with the popped IMemento instance, causing the last action to be reverted.
Benefits of the Memento pattern:
- Undo/redo support - snapshotting state makes it trivial to walk back through a history of changes.
- Encapsulation preserved - the caretaker (
MainWindow) stores opaque mementos without needing to understand canvas internals. - Clean separation - state capture logic stays with the originator rather than leaking into the UI code.
- Simple history management - a plain
Stack<T>is all that is needed to track states.
In short, if your application needs reversible operations - editors, transactional workflows, game state – the Memento pattern gives you that with minimal coupling.
Null Object pattern
One of the more simple design patterns, the Null Object pattern provides a neutral, do-nothing implementation of an interface to stand in for the absence of a meaningful object. This lets callers avoid sprinkling null checks throughout their code, because a null object responds to every method with sensible, inert behaviour.
In the example project there is a ComputerRepository class that implements the Null Object pattern. The ComputerRepository class has a Find() method with a computerName parameter that returns a Computer object:
public class ComputerRepository
{
public ComputerBase Find(string computerName)
{
if (!string.IsNullOrWhiteSpace(computerName))
{
switch (computerName.ToLower())
{
case "apple":
return new AppleComputer();
case "linux":
return new LinuxComputer();
case "windows":
return new WindowsComputer();
}
}
return ComputerBase.Null;
}
}Internally, the Find() method checks if the computerName parameter is not null and depending on the value, returns either AppleComputer, LinuxComputer or WindowsComputer - all of which inherit from the ComputerBase class:
// AppleComputer
public class AppleComputer : ComputerBase
{
public override Guid Id
{
get
{
return Guid.NewGuid();
}
}
public override string Name
{
get
{
return "Apple computer";
}
}
public override void Start()
{
Console.WriteLine("Apple computer started");
}
public override void Stop()
{
Console.WriteLine("Apple computer stopped");
}
}
// LinuxComputer
public class LinuxComputer : ComputerBase
{
public override Guid Id
{
get
{
return Guid.NewGuid();
}
}
public override string Name
{
get
{
return "Linux computer";
}
}
public override void Start()
{
Console.WriteLine("Linux computer started");
}
public override void Stop()
{
Console.WriteLine("Linux computer stopped");
}
}
// WindowsComputer
public class WindowsComputer : ComputerBase
{
public override Guid Id
{
get
{
return Guid.NewGuid();
}
}
public override string Name
{
get
{
return "Windows computer";
}
}
public override void Start()
{
Console.WriteLine("Windows computer started");
}
public override void Stop()
{
Console.WriteLine("Windows computer stopped");
}
}The ComputerBase class itself holds a public class definition called NullComputer which inherits from ComputerBase but has no method implementations and returns empty values for the exposed properties:
public abstract class ComputerBase
{
private static readonly NullComputer _nullComputer = new NullComputer();
public static NullComputer Null
{
get
{
return _nullComputer;
}
}
public abstract Guid Id { get; }
public abstract string Name { get; }
public abstract void Start();
public abstract void Stop();
public class NullComputer : ComputerBase
{
public override Guid Id
{
get
{
return Guid.Empty;
}
}
public override string Name
{
get
{
return string.Empty;
}
}
public override void Start()
{
}
public override void Stop()
{
}
}
}If no match is found, then the Find() method returns a NullComputer object via the ComputerBase.Null property which itself holds a static instance of NullComputer.
Benefits of the Null Object pattern:
- Fewer null checks – callers never need to guard against null, simplifying every consuming method.
- Default behaviour - missing lookups degrade gracefully to no-ops rather than crashing.
- Singleton efficiency – one shared, stateless null instance serves all callers.
- Polymorphic consistency – code can operate uniformly over real and absent objects alike.
In short, wherever a “not found” result would otherwise ripple null checks through your call stack, a Null Object keeps the code clean and honest.
Observer pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object (the subject) changes state, all of its dependents (the observers) are notified automatically. It is the foundation of event-driven programming, and this example demonstrates it three ways – a traditional abstract implementation, the .NET IObserver<T> pair, and C# events and delegates.
Traditional
This example uses a StockTicker class to demonstrate the pattern which inherits from an AbstractSubject class:
public class StockTicker : AbstractSubject
{
private Stock _stock;
public Stock Stock
{
get
{
return _stock;
}
set
{
_stock = value;
Notify();
}
}
}AbstractSubject is an abstract class that defines Register(), Unregister() and Notify() methods:
public abstract class AbstractSubject
{
private readonly List<AbstractObserver> _observers = new List<AbstractObserver>();
public void Register(AbstractObserver observer)
{
_observers.Add(observer);
}
public void Unregister(AbstractObserver observer)
{
_observers.Remove(observer);
}
public void Notify()
{
foreach (var o in _observers)
{
o.Update();
}
}
}The Register() method adds an AbstractObserver to the _observers list, the Unregister() method removes an AbstractObserver from the _observers list, and the Notify() method calls the Update() method on each AbstractObserver in the observers list.
The AbstractObserver abstract class has an abstract Update() method:
public abstract class AbstractObserver
{
public abstract void Update();
}Which is itself implemented by two subclasses - GoogleObserver and MicrosoftObserver:
// GoogleObserver
public class GoogleObserver : AbstractObserver
{
public GoogleObserver(StockTicker subj)
{
DataSource = subj;
subj.Register(this);
}
private StockTicker DataSource { get; }
public override void Update()
{
decimal price = DataSource.Stock.Price;
string symbol = DataSource.Stock.Symbol;
if (symbol == "GOOG")
{
Console.WriteLine("Google's new price is: {0}", price);
}
}
}
// MicrosoftObserver
public class MicrosoftObserver : AbstractObserver
{
public MicrosoftObserver(StockTicker subj)
{
DataSource = subj;
subj.Register(this);
}
private StockTicker DataSource { get; }
public override void Update()
{
decimal price = DataSource.Stock.Price;
string symbol = DataSource.Stock.Symbol;
if (symbol == "MSFT" && price > 10.00m)
{
Console.WriteLine("Microsoft has reached the target price: {0}", price);
}
}
}When these classes are instantiated, they are passed a StockTicker object via the constructor which they use to register themselves via the StockTicker.Register() method:
StockTicker stockTicker = new StockTicker();
GoogleObserver googleObserver = new GoogleObserver(stockTicker);
MicrosoftObserver microsoftObserver = new MicrosoftObserver(stockTicker);When the corresponding Stock object instance is updated on the StockTicker object, the Notify() method is called, which in turn calls the Update() method on each AbstractObserver in the _observers list:
foreach (var stock in SampleData.GetNext())
{
stockTicker.Stock = stock;
}IObserver
This version of the pattern effectively inverts the dependency between the StockTicker and IObserver<T>. In this case, the StockTicker class has a Subscribe() method that takes an instance of an IObserver<T> and registers it with the StockTicker object, returning an Unsubscriber instance that implements IDisposable. When the Unsubscriber instance is disposed, it removes the IObserver<T> from the internal _observers list:
public class StockTicker : IObservable<Stock>
{
private readonly List<IObserver<Stock>> _observers = new List<IObserver<Stock>>();
private Stock _stock;
public Stock Stock
{
get
{
return _stock;
}
set
{
_stock = value;
Notify(_stock);
}
}
private void Notify(Stock s)
{
foreach (var o in _observers)
{
if (s.Symbol == null || s.Price < 0)
{
o.OnError(new Exception("Bad Stock Data"));
}
else
{
o.OnNext(s);
}
}
}
public IDisposable Subscribe(IObserver<Stock> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
}
return new Unsubscriber(_observers, observer);
}
private class Unsubscriber : IDisposable
{
private readonly List<IObserver<Stock>> _observers;
private readonly IObserver<Stock> _observer;
public Unsubscriber(List<IObserver<Stock>> observers, IObserver<Stock> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observer != null && _observers.Contains(_observer))
{
_observers.Remove(_observer);
}
}
}
}Internally to the StockTicker class, when the Stock property is set, the Notify() method is called which iterates over the _observers list and calls either the OnNext() method or the OnError() method if there is an issue with the stock price (null or less than 0) for each IObserver<T> instance.
Implementors of the IObserver<T> interface - GoogleMonitor and MicrosoftMonitor - then implement the OnNext() and OnError() methods and perform the appropriate actions:
// GoogleMonitor
public class GoogleMonitor : IObserver<Stock>
{
public void OnCompleted()
{
Console.WriteLine("End of trading day");
}
public void OnError(Exception error)
{
Console.WriteLine("Error occured in the stock ticker");
}
public void OnNext(Stock value)
{
if (value.Symbol == "GOOG")
{
Console.WriteLine("Google's new price is: {0}", value.Price);
}
}
}
// MicrosoftMonitor
public class MicrosoftMonitor : IObserver<Stock>
{
public void OnCompleted()
{
Console.WriteLine("End of trading day");
}
public void OnError(Exception error)
{
Console.WriteLine("Error occured in the stock ticker");
}
public void OnNext(Stock value)
{
if (value.Symbol == "MSFT" && value.Price > 10.00m)
{
Console.WriteLine("Microsoft has reached the target price: {0}", value.Price);
}
}
}When the StockTicker is instantiated, the respective monitor implementations are registered via the Subscribe() method:
StockTicker stockTicker = new StockTicker();
GoogleMonitor googleMonitor = new GoogleMonitor();
MicrosoftMonitor microsoftMonitor = new MicrosoftMonitor();
using (stockTicker.Subscribe(googleMonitor))
{
using (stockTicker.Subscribe(microsoftMonitor))
{
foreach (var stock in SampleData.GetNext())
{
stockTicker.Stock = stock;
}
}
}EventDelegate
This version of the pattern employs the event and delegate approach whereby the StockTicker class has a StockChange event that is raised when the Stock property is set:
public class StockTicker
{
private Stock _stock;
public event EventHandler<StockChangeEventArgs> StockChange;
public Stock Stock
{
get
{
return _stock;
}
set
{
_stock = value;
OnStockChange(new StockChangeEventArgs(_stock));
}
}
protected virtual void OnStockChange(StockChangeEventArgs e)
{
if (StockChange != null)
{
StockChange(this, e);
}
}
}The StockChange event is of type EventHandler<StockChangeEventArgs> and the StockChangeEventArgs class contains the Stock property:
public class StockChangeEventArgs : EventArgs
{
public StockChangeEventArgs(Stock s)
{
Stock = s;
}
public Stock Stock { get; set; }
}Any observers then register with the StockTicker class by subscribing to the StockChange event via the constructor. Internally, the logic checks against the Stock property to see whether the class - either GoogleMonitor or MicrosoftMonitor - should perform any actions off the back of the event being raised:
// GoogleMonitor
public class GoogleMonitor
{
public GoogleMonitor(StockTicker stockTicker)
{
stockTicker.StockChange += StockChangeHandler;
}
private static void StockChangeHandler(object sender, StockChangeEventArgs e)
{
CheckFilter(e.Stock);
}
private static void CheckFilter(Stock value)
{
if (value.Symbol == "GOOG")
{
Console.WriteLine("Google's new price is: {0}", value.Price);
}
}
}
// MicrosoftMonitor
public class MicrosoftMonitor
{
public MicrosoftMonitor(StockTicker stockTicker)
{
stockTicker.StockChange += StockChangeHandler;
}
private static void StockChangeHandler(object sender, StockChangeEventArgs e)
{
CheckFilter(e.Stock);
}
private static void CheckFilter(Stock value)
{
if (value.Symbol == "MSFT" && value.Price > 10.00m)
{
Console.WriteLine("Microsoft has reached the target price: {0}", value.Price);
}
}
}Registration of each monitor is similar to the traditional pattern in that the StockTicker instance is passed via the constructor:
StockTicker stockTicker = new StockTicker();
GoogleMonitor googleMonitor = new GoogleMonitor(stockTicker);
MicrosoftMonitor microsoftMonitor = new MicrosoftMonitor(stockTicker);
foreach (var stock in SampleData.GetNext())
{
stockTicker.Stock = stock;
}Benefits of the Observer pattern:
- Loose coupling – the subject knows only that it has observers, never what they do with the notification.
- Broadcast semantics – any number of observers can react to the same state change simultaneously.
- Runtime subscription – observers can attach and detach at will, as the
Unsubscriber/usingexample demonstrates. - Idiomatic C# options -
IObserver<T>, events and delegates each suit different granularity and error-handling needs.
In short, the Observer pattern underpins much of reactive programming and .NET events – mastering its variants means understanding how modern UIs and streaming systems stay in sync.
Rules pattern
The Rules pattern encapsulates each piece of business logic in its own small class behind a common interface, then composes those rules together. Rather than one sprawling method of nested conditionals, each rule is independently testable and individually replaceable, and new rules slot in without touching the others.
In this example we have an IDiscountRule interface with a single method - CalculateCustomerDiscount() that returns a decimal value:
public interface IDiscountRule
{
decimal CalculateCustomerDiscount(Customer customer);
}Multiple classes then implement this interface, including BirthdayDiscountRule, FirstTimeCustomerRule, LoyalCustomerRule, SeniorRule and VeteranRule, each with their own distinct ways of handling the Customer instance that is passed in:
// BirthdayDiscountRule
public class BirthdayDiscountRule : IDiscountRule
{
public decimal CalculateCustomerDiscount(Customer customer)
{
if (customer.DateOfBirth.Month == DateTime.Today.Month &&
customer.DateOfBirth.Day == DateTime.Today.Day)
{
return 0.10m;
}
return 0;
}
}
// FirstTimeCustomerRule
public class FirstTimeCustomerRule : IDiscountRule
{
public decimal CalculateCustomerDiscount(Customer customer)
{
if (!customer.DateOfFirstPurchase.HasValue)
{
return 0.15m;
}
return 0;
}
}
// LoyalCustomerRule
public class LoyalCustomerRule : IDiscountRule
{
private readonly decimal _discount;
private readonly int _yearsAsCustomer;
public LoyalCustomerRule(int yearsAsCustomer, decimal discount)
{
_yearsAsCustomer = yearsAsCustomer;
_discount = discount;
}
public decimal CalculateCustomerDiscount(Customer customer)
{
if (customer.DateOfFirstPurchase.HasValue)
{
if (customer.DateOfFirstPurchase.Value.AddYears(_yearsAsCustomer) <= DateTime.Today)
{
var birthdayRule = new BirthdayDiscountRule();
return _discount + birthdayRule.CalculateCustomerDiscount(customer);
}
}
return 0;
}
}
// SeniorRule
public class SeniorRule : IDiscountRule
{
public decimal CalculateCustomerDiscount(Customer customer)
{
if (customer.DateOfBirth < DateTime.Now.AddYears(-65))
{
return .05m;
}
return 0;
}
}
// VeteranRule
public class VeteranRule : IDiscountRule
{
public decimal CalculateCustomerDiscount(Customer customer)
{
if (customer.IsVeteran)
{
return 0.1m;
}
return 0;
}
}For example, BirthdayDiscountRule might check whether the customer’s birthday is today and apply a discount, while LoyalCustomerRule might check the date of when the customer first made a purchase and calculate a similar discount based on the number of years since their first purchase.
The DiscountCalculator class then uses these rules to calculate the total discount for a given customer by adding them to a List<IDiscountRule> in the default constructor. The CalculateDiscountPercentage() method then iterates through this list and returns the maximum discount returned by any of the rules:
public class DiscountCalculator
{
private readonly List<IDiscountRule> _rules = new List<IDiscountRule>();
public DiscountCalculator()
{
_rules.Add(new BirthdayDiscountRule());
_rules.Add(new SeniorRule());
_rules.Add(new VeteranRule());
_rules.Add(new LoyalCustomerRule(1, 0.10m));
_rules.Add(new LoyalCustomerRule(5, 0.12m));
_rules.Add(new LoyalCustomerRule(10, 0.20m));
_rules.Add(new FirstTimeCustomerRule());
}
public decimal CalculateDiscountPercentage(Customer customer)
{
decimal discount = 0;
foreach (var rule in _rules)
{
discount = Math.Max(rule.CalculateCustomerDiscount(customer), discount);
}
return discount;
}
}Benefits of the Rules pattern:
- Single responsibility per rule – each business condition is isolated in a tiny, focused class.
- Unit-testable logic – every rule can be exercised with simple, deterministic tests.
- Configurable composition – the calculator decides which rules run, in what combinations, without modifying the rules themselves.
- Easy to extend – a new discount policy is a new class, not a surgical edit to a monolithic conditional.
In short, when your business logic starts sprouting deeply nested if statements, the Rules pattern offers a tidy, incremental escape route.
State pattern
The State pattern allows an object to alter its behaviour when its internal state changes – the object appears to change its class at runtime. Rather than testing a state flag in every method, each state becomes a class that implements the shared interface, and transitions simply swap in a different state object.
An ICommands interface defines the common behaviour for all states, and concrete state classes implement this interface with their own behaviour:
public interface ICommands
{
bool Delete();
void Edit(string title, string desc);
void Print();
void SetState(string state);
}In the code example, four states are defined and implement the ICommands interface - Active, Closed, Proposed and Resolved - and each has its own implementation of Delete(), Edit(), Print() and SetState():
// Active
public class Active : BaseState, ICommands
{
private readonly WorkItem _owner;
public Active(WorkItem owner)
{
_owner = owner;
}
public bool Delete()
{
Console.WriteLine("Work Item is already active. Cannot Delete.");
return false;
}
public void Edit(string title, string desc)
{
_owner.Title = title;
_owner.Description = desc;
}
public void Print()
{
Print(_owner);
}
public void SetState(string state)
{
switch (state)
{
case "active":
Console.WriteLine("Work Item is already active.");
break;
case "proposed":
case "resolved":
_owner.State = state;
break;
default:
Console.WriteLine("Work Item is in an active state and cannot be set to {0}.", state);
break;
}
}
}
// Closed
public class Closed : BaseState, ICommands
{
private readonly WorkItem _owner;
public Closed(WorkItem owner)
{
_owner = owner;
}
public bool Delete()
{
return true;
}
public void Edit(string title, string desc)
{
_owner.Title = title;
_owner.Description = desc;
}
public void Print()
{
Print(_owner);
}
public void SetState(string state)
{
switch (state)
{
case "closed":
Console.WriteLine("Work Item is already closed.");
break;
case "resolved":
_owner.State = state;
break;
default:
Console.WriteLine("Work Item is in a closed state and cannot be set to {0}.", state);
break;
}
}
}
// Proposed
public class Proposed : BaseState, ICommands
{
private readonly WorkItem _owner;
public Proposed(WorkItem owner)
{
_owner = owner;
}
public bool Delete()
{
return true;
}
public void Edit(string title, string desc)
{
_owner.Title = title;
_owner.Description = desc;
}
public void Print()
{
Print(_owner);
}
public void SetState(string state)
{
switch (state)
{
case "proposed":
Console.WriteLine("Work Item is already proposed.");
break;
case "active":
_owner.State = state;
break;
default:
Console.WriteLine("Work Item is in a proposed state and cannot be set to {0}.", state);
break;
}
}
}
// Resolved
public class Resolved : BaseState, ICommands
{
private readonly WorkItem _owner;
public Resolved(WorkItem owner)
{
_owner = owner;
}
public bool Delete()
{
Console.WriteLine("Work Item is already resolved. Cannot Delete.");
return false;
}
public void Edit(string title, string desc)
{
_owner.Title = title;
_owner.Description = desc;
}
public void Print()
{
Print(_owner);
}
public void SetState(string state)
{
switch (state)
{
case "resolved":
Console.WriteLine("Work Item is already resolved.");
break;
case "active":
case "closed":
_owner.State = state;
break;
default:
Console.WriteLine("Work Item is in an resolved state and cannot be set to {0}.", state);
break;
}
}
}- In the Active state, the item cannot be deleted but can be edited and printed.
- In the Closed state, the item can be deleted, edited and printed (amended: the original text stated the item cannot be deleted, but
Closed.Delete()returnstrue). - In the Proposed state, the item can be edited, deleted and printed.
- In the Resolved state, the item cannot be deleted but can be edited and printed.
The WorkItem class has a State property that, when set, creates a new instance of the specified state and assigns it to the internal _stateCommand variable:
public class WorkItem : ICommands
{
private ICommands _stateCommands;
private string _state;
public int Id { get; set; }
public string Title { get; set; }
```csharp
public string Description { get; set; }
public string State
{
get
{
return _state;
}
set
{
_state = value.ToLower();
if (_state == "proposed")
{
_stateCommands = new States.Proposed(this);
}
if (_state == "active")
{
_stateCommands = new States.Active(this);
}
if (_state == "resolved")
{
_stateCommands = new States.Resolved(this);
}
if (_state == "closed")
{
_stateCommands = new States.Closed(this);
}
}
}
internal static WorkItem Create()
{
var wi = new WorkItem();
wi.Id = -1;
wi.State = "Proposed";
return wi;
}
public bool Delete()
{
bool canDelete = _stateCommands.Delete();
if (canDelete)
{
Console.WriteLine("Work item deleted!");
}
return canDelete;
}
public void Edit(string title, string desc)
{
_stateCommands.Edit(title, desc);
}
public void Print()
{
_stateCommands.Print();
}
public void SetState(string newstate)
{
_stateCommands.SetState(newstate.ToLower());
}
}The WorkItem class delegates all of the ICommands behaviour to the current state object, so when the State property is set, the work item’s behaviour changes accordingly – a delete attempt in the Proposed state succeeds, while the same call in the Active state is refused with a message.
Benefits of the State pattern:
- Contextual behaviour – each state’s rules live in one place rather than being smeared across every method in
switchstatements. - Open/closed compliance – adding a new state means adding a new class, not editing every existing method.
- Explicit transitions - permitted state changes are declared alongside the state that owns them, making the workflow self-documenting.
- Localised state-specific logic - bugs around what is or is not allowed in a given state become easy to locate and fix.
In short, any time you find yourself guarding methods with if (state == ...) checks, the State pattern will usually clean the situation up considerably.
Strategy pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The object using a strategy holds a reference to an abstraction and delegates the work to it, meaning the algorithm can be selected – or swapped – at runtime without the consuming code changing at all.
In this example the ICalculateOperation interface defines the common behaviour for all strategies, and concrete strategy classes implement this interface with their own behaviour. A single Calculate() method takes two parameters and returns a result:
public interface ICalculateOperation
{
int Calculate(int value1, int value2);
}Implementations of this interface include AddOperation, DivideOperation, MinusOperation, and MultiplyOperation, which perform addition, subtraction, multiplication, and division, respectively:
// AddOperation
public class AddOperation : ICalculateOperation
{
public int Calculate(int value1, int value2)
{
return value1 + value2;
}
}
// DivideOperation
public class DivideOperation : ICalculateOperation
{
public int Calculate(int value1, int value2)
{
return value1 / value2;
}
}
// MinusOperation
public class MinusOperation : ICalculateOperation
{
public int Calculate(int value1, int value2)
{
return value1 - value2;
}
}
// MultiplyOperation
public class MultiplyOperation : ICalculateOperation
{
public int Calculate(int value1, int value2)
{
return value1 * value2;
}
}When the Calculator class is instantiated, it takes an ICalculateOperation object as a parameter, which determines the behaviour of the Calculate() method:
public class Calculator
{
private readonly ICalculateOperation _calculateOperation;
public Calculator(ICalculateOperation calculateOperation)
{
_calculateOperation = calculateOperation;
}
public int Calculate(int value1, int value2)
{
return _calculateOperation.Calculate(value1, value2);
}
}This allows the Calculator object to change its behaviour based on the strategy that is passed to it:
private static void AddOperation()
{
Calculator calculator = new Calculator(new AddOperation());
int result = calculator.Calculate(NumberOne, NumberTwo);
Console.WriteLine("Add operation result: {0}", result);
}
private static void DivideOperation()
{
Calculator calculator = new Calculator(new DivideOperation());
int result = calculator.Calculate(NumberOne, NumberTwo);
Console.WriteLine("Divide operation result: {0}", result);
}
private static void MinusOperation()
{
Calculator calculator = new Calculator(new MinusOperation());
int result = calculator.Calculate(NumberOne, NumberTwo);
Console.WriteLine("Minus operation result: {0}", result);
}
private static void MultiplyOperation()
{
Calculator calculator = new Calculator(new MultiplyOperation());
int result = calculator.Calculate(NumberOne, NumberTwo);
Console.WriteLine("Multiply operation result: {0}", result);
}
public class DivideOperation : ICalculateOperation
{
public int Calculate(int value1, int value2)
{
if (value2 == 0)
{
throw new ArgumentOutOfRangeException(nameof(value2), "Cannot divide by zero.");
}
return value1 / value2;
}
}Benefits of the Strategy pattern:
- Runtime algorithm selection – the behaviour of an object can be chosen per use rather than baked in at compile time.
- Eliminated conditionals – one algorithm per class replaces sprawling branches on an operation type.
- Independent testing – every strategy can be unit tested in isolation.
- Trivial extensibility - new algorithms join the family without touching the
Calculatoror any existing strategy.
In short, the Strategy pattern is one of the most frequently applicable patterns in day-to-day development - anywhere you might reach for a switch on a behaviour type is a candidate.
Template Method pattern
The Template Method pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The base class fixes the order of operations – the template method – while abstract or virtual hooks let derived classes supply or refine individual steps without altering the overall flow.
Using an OrderShipment base class, the implementation of the Template Method pattern has three methods - an abstract GetShippingLabelFromCarrier() method and virtual VerifyShippingData() and PrintLabel() methods:
public abstract class OrderShipment
{
public string ShippingAddress { get; set; }
public string Label { get; set; }
public void Ship(StringBuilder stringBuilder)
{
VerifyShippingData();
GetShippingLabelFromCarrier();
PrintLabel(stringBuilder);
}
public virtual void VerifyShippingData()
{
if (string.IsNullOrWhiteSpace(ShippingAddress))
{
throw new ApplicationException("Invalid address");
}
}
public abstract void GetShippingLabelFromCarrier();
public virtual void PrintLabel(StringBuilder stringBuilder)
{
string label = string.Format("Label: {0}", Label);
stringBuilder.AppendLine(label);
}
}Four implementations of the OrderShipment class are provided, each with a different shipping carrier. The GetShippingLabelFromCarrier() method is abstract and must be implemented by each subclass, while the VerifyShippingData() and PrintLabel() methods are virtual and can be overridden by subclasses:
// AusPostShipment
public class AusPostShipment : OrderShipment
{
public override void VerifyShippingData()
{
if (string.IsNullOrWhiteSpace(ShippingAddress) || !ShippingAddress.ToLower().Contains("australia"))
{
throw new ApplicationException("Invalid address");
}
}
public override void GetShippingLabelFromCarrier()
{
Label = string.Format("AusPost: [{0}]", ShippingAddress);
}
public override void PrintLabel(StringBuilder stringBuilder)
{
string label = string.Format("Label: {0} (Australia)", Label);
stringBuilder.AppendLine(label);
}
}
// DhlShipment
public class DhlShipment : OrderShipment
{
public override void GetShippingLabelFromCarrier()
{
Label = string.Format("DHL: [{0}]", ShippingAddress);
}
}
// PostOfficeShipment
public class PostOfficeShipment : OrderShipment
{
public override void GetShippingLabelFromCarrier()
{
Label = string.Format("Post Office: [{0}]", ShippingAddress);
}
}
// TntShipment
public class TntShipment : OrderShipment
{
public override void GetShippingLabelFromCarrier()
{
Label = string.Format("TNT: [{0}]", ShippingAddress);
}
}The Ship() method is the template method itself: it always verifies the shipping data, obtains the label from the carrier and prints it in that order, regardless of which subclass is doing the shipping. In this example, the AusPostShipment class overrides the VerifyShippingData() method to perform additional validation specific to its shipping carrier and the PrintLabel() method to return a slightly different value, whereas the other implementations merely provide an implementation for the GetShippingLabelFromCarrier() method.
Benefits of the Template Method pattern:
- Fixed algorithm shape – the invariant sequence of steps is defined once in the base class and cannot be accidentally reordered by subclasses.
- Hooked variation points - subclasses customise precisely the steps that differ and inherit the rest.
- Reuse without duplication – the shared verification and printing logic lives in one place.
- Progressive refinement – carriers can start with the default behaviour and override selectively, as
AusPostShipmentdoes.
In short, the Template Method pattern is ideal when several classes follow the same overall procedure but vary in a few of the details.
Visitor pattern
The Visitor pattern allows you to add new operations to a class hierarchy without modifying the existing classes. It separates an algorithm from the objects on which it operates – a visitor object walks the structure and, through double dispatch, each element calls back the appropriate overloaded Visit() method on the visitor, making it easier to extend and maintain the code.
The ICarElement interface defines a single method Accept() that takes a ICarElementVisitor object as a parameter:
// ICarElement
public interface ICarElement
{
void Accept(ICarElementVisitor visitor);
}This method is implemented by all concrete classes in the hierarchy, allowing them to accept a visitor object and perform operations on them:
// Body
public class Body : ICarElement
{
public void Accept(ICarElementVisitor visitor)
{
visitor.Visit(this);
}
}
// Engine
public class Engine : ICarElement
{
public void Accept(ICarElementVisitor visitor)
{
visitor.Visit(this);
}
}
// Wheel
// Amended: removed a stray opening brace that preceded the class definition in the original
public class Wheel : ICarElement
{
public Wheel(string name)
{
Name = name;
}
public string Name { get; private set; }
public void Accept(ICarElementVisitor visitor)
{
visitor.Visit(this);
}
}This is demonstrated through the use of a concrete Car class that has an internal IList<CarElement> collection of ICarElement objects:
public class Car : ICarElement
{
private readonly IList<ICarElement> _elements;
public Car()
{
_elements = new List<ICarElement>();
_elements.Add(new Wheel("Front left"));
_elements.Add(new Wheel("Front right"));
_elements.Add(new Wheel("Back left"));
_elements.Add(new Wheel("Back right"));
_elements.Add(new Body());
_elements.Add(new Engine());
}
public void Accept(ICarElementVisitor visitor)
{
foreach (ICarElement carElement in _elements)
{
carElement.Accept(visitor);
}
visitor.Visit(this);
}
}When the Car.Accept() method is called, the corresponding Accept() method is called on each element in the collection, allowing the visitor to perform operations on each element. The ICarElementVisitor interface defines multiple Visit() methods, one for each concrete class in the hierarchy. Each Visit() method then performs a specific operation on the corresponding concrete class:
// ICarElementVisitor
public interface ICarElementVisitor
{
void Visit(Engine engine);
void Visit(Wheel wheel);
void Visit(Body body);
void Visit(Car car);
}In this example there are two concrete implementations of the ICarElementVisitor interface - CarElementDoVisitor and CarElementPrintVisitor:
// CarElementDoVisitor
public class CarElementDoVisitor : ICarElementVisitor
{
public void Visit(Engine engine)
{
Console.WriteLine("Starting my engine");
}
public void Visit(Wheel wheel)
{
Console.WriteLine("Visiting my {0} wheel", wheel.Name);
}
public void Visit(Body body)
{
Console.WriteLine("Moving my body");
}
public void Visit(Car car)
{
Console.WriteLine("Starting my car");
}
}
// CarElementPrintVisitor
public class CarElementPrintVisitor : ICarElementVisitor
{
public void Visit(Engine engine)
{
Console.WriteLine("Visiting engine");
}
public void Visit(Wheel wheel)
{
Console.WriteLine("Visiting {0} wheel", wheel.Name);
}
public void Visit(Body body)
{
Console.WriteLine("Visiting body");
}
public void Visit(Car car)
{
Console.WriteLine("Visiting car");
}
}Because both visitors satisfy the same interface, an entire new operation can be performed over the car’s structure without changing any of the ICarElement classes - that is the essence of the pattern.
Benefits of the Visitor pattern:
- New operations without recompilation of the hierarchy - behaviours are added by writing a new visitor, not by editing every element class.
- Related logic co-located - all of one operation (say, “print everything”) sits in a single class rather than being scattered across the hierarchy.
- Type-safe dispatch - overloaded
Visit()methods give compile-time checking that every element type is handled. - Natural fit for composite structures - combined with a composite like
Car, traversals and operations remain cleanly separated.
In short, when you have a stable class hierarchy but frequently evolving operations performed upon it, the Visitor pattern lets you grow the operations side indefinitely.
Wrapping up
Behavioural patterns are all about communication – who talks to whom, who owns which responsibility, and how behaviour can change over time. Whereas creational patterns address how objects come into being and structural patterns address how they compose, behavioural patterns shape the conversations between them.
As a rough guide:
- Need to decouple senders from receivers? Look at Chain of Responsibility, Command, Mediator and Observer.
- Changing behaviour over time? State, Strategy and Template Method are the usual suspects.
- Working with collections and structures? Iterator and Visitor shine here.
- Cleaning up defensive code? Null Object and Rules will take you a long way.
The full source code for every example in this post is available in my design patterns repository on GitHub – each pattern is a standalone console application you can clone and debug to watch the flow for yourself.
Related Posts
2024-02-13
2024-02-12
2024-02-11