Skip to main content

9.4 - Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe not just patterns of objects or classes but also the patterns of communication between them. These patterns characterize complex control flow that's difficult to follow at run-time.

9.4.1 - Chain of Responsibility Pattern

The Chain of Responsibility pattern passes requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

Basic Implementation

// PROBLEM: We need to process different types of requests, but we don't know in advance
// which handler should process a particular request. We want to pass the request along
// a chain of handlers until one of them handles it.

/// <summary>
/// Handler interface that defines methods for building a chain and handling requests
/// This is the core interface for the Chain of Responsibility pattern
/// </summary>
public interface IHandler
{
/// <summary>
/// Sets the next handler in the chain
/// </summary>
/// <param name="handler">The next handler to call if this handler can't process the request</param>
/// <returns>The next handler, to allow for method chaining</returns>
IHandler SetNext(IHandler handler);

/// <summary>
/// Handles a request or passes it to the next handler in the chain
/// </summary>
/// <param name="request">The request to be handled</param>
/// <returns>The response from the handler that processed the request, or null if no handler could process it</returns>
object Handle(object request);
}

/// <summary>
/// Abstract base handler that implements the chaining behavior
/// This class provides the default implementation for building the chain
/// </summary>
public abstract class AbstractHandler : IHandler
{
// Reference to the next handler in the chain
private IHandler _nextHandler;

/// <summary>
/// Sets the next handler in the chain and returns it to allow for method chaining
/// </summary>
/// <param name="handler">The next handler in the chain</param>
/// <returns>The next handler, allowing for method chaining like h1.SetNext(h2).SetNext(h3)</returns>
public IHandler SetNext(IHandler handler)
{
_nextHandler = handler;
return handler; // Returning the handler enables chaining
}

/// <summary>
/// Default implementation that passes the request to the next handler if one exists
/// </summary>
/// <param name="request">The request to be handled</param>
/// <returns>The result from the handler that processed the request, or null if none could handle it</returns>
public virtual object Handle(object request)
{
// If there's a next handler, pass the request along
if (_nextHandler != null)
{
return _nextHandler.Handle(request);
}

// If we're the last handler and couldn't process it, return null
// This indicates that the request wasn't handled by any handler in the chain
return null;
}
}

/// <summary>
/// Concrete handler that processes specific requests
/// </summary>
public class ConcreteHandler1 : AbstractHandler
{
/// <summary>
/// Handles requests that match "Request1" or passes them to the next handler
/// </summary>
/// <param name="request">The request to be handled</param>
/// <returns>A response if this handler can process the request, otherwise passes to the next handler</returns>
public override object Handle(object request)
{
// Check if this handler can process the request
if (request.ToString() == "Request1")
{
// This handler can process the request, so handle it and return a response
return $"Handler1: I'll handle the {request.ToString()}";
}

// This handler can't process the request, so pass it to the next handler in the chain
// The base.Handle() call uses the implementation from AbstractHandler
return base.Handle(request);
}
}

/// <summary>
/// Concrete handler that processes specific requests
/// </summary>
public class ConcreteHandler2 : AbstractHandler
{
/// <summary>
/// Handles requests that match "Request2" or passes them to the next handler
/// </summary>
/// <param name="request">The request to be handled</param>
/// <returns>A response if this handler can process the request, otherwise passes to the next handler</returns>
public override object Handle(object request)
{
// Check if this handler can process the request
if (request.ToString() == "Request2")
{
// This handler can process the request, so handle it and return a response
return $"Handler2: I'll handle the {request.ToString()}";
}

// This handler can't process the request, so pass it to the next handler in the chain
return base.Handle(request);
}
}

/// <summary>
/// Concrete handler that processes specific requests
/// </summary>
public class ConcreteHandler3 : AbstractHandler
{
/// <summary>
/// Handles requests that match "Request3" or passes them to the next handler
/// </summary>
/// <param name="request">The request to be handled</param>
/// <returns>A response if this handler can process the request, otherwise passes to the next handler</returns>
public override object Handle(object request)
{
// Check if this handler can process the request
if (request.ToString() == "Request3")
{
// This handler can process the request, so handle it and return a response
return $"Handler3: I'll handle the {request.ToString()}";
}

// This handler can't process the request, so pass it to the next handler in the chain
return base.Handle(request);
}
}

// USAGE EXAMPLE:
// // Create the chain of handlers
// var handler1 = new ConcreteHandler1();
// var handler2 = new ConcreteHandler2();
// var handler3 = new ConcreteHandler3();
//
// // Build the chain using method chaining
// handler1.SetNext(handler2).SetNext(handler3);
//
// // Process different requests
// Console.WriteLine(handler1.Handle("Request1")); // Handled by handler1
// Console.WriteLine(handler1.Handle("Request2")); // Passed to and handled by handler2
// Console.WriteLine(handler1.Handle("Request3")); // Passed to and handled by handler3
// Console.WriteLine(handler1.Handle("Request4")); // Not handled by any handler, returns null

Support Ticket Example

// REAL-WORLD PROBLEM: We need a support ticket system that automatically routes tickets
// to the appropriate support level based on complexity, with the ability to escalate if needed.

/// <summary>
/// Represents a customer support ticket in the system
/// </summary>
public class SupportTicket
{
/// <summary>
/// Gets the unique identifier for the ticket
/// </summary>
public int Id { get; }

/// <summary>
/// Gets the name of the customer who submitted the ticket
/// </summary>
public string CustomerName { get; }

/// <summary>
/// Gets the description of the customer's issue
/// </summary>
public string Issue { get; }

/// <summary>
/// Gets the complexity/severity level of the ticket
/// </summary>
public SupportLevel Level { get; }

/// <summary>
/// Initializes a new instance of the SupportTicket class
/// </summary>
/// <param name="id">The unique identifier for the ticket</param>
/// <param name="customerName">The name of the customer</param>
/// <param name="issue">The description of the issue</param>
/// <param name="level">The complexity/severity level of the ticket</param>
public SupportTicket(int id, string customerName, string issue, SupportLevel level)
{
Id = id;
CustomerName = customerName;
Issue = issue;
Level = level;
}
}

/// <summary>
/// Defines the different levels of support complexity/severity
/// </summary>
public enum SupportLevel
{
/// <summary>
/// Basic issues that can be handled by first-level support
/// </summary>
Basic,

/// <summary>
/// Intermediate issues that require more expertise
/// </summary>
Intermediate,

/// <summary>
/// Advanced issues that require specialized knowledge
/// </summary>
Advanced
}

/// <summary>
/// Handler interface for the support ticket chain of responsibility
/// </summary>
public interface ISupportHandler
{
/// <summary>
/// Sets the next handler in the support chain
/// </summary>
/// <param name="handler">The next support handler</param>
/// <returns>The next handler for method chaining</returns>
ISupportHandler SetNext(ISupportHandler handler);

/// <summary>
/// Handles a support ticket or passes it to the next handler
/// </summary>
/// <param name="ticket">The support ticket to handle</param>
void HandleTicket(SupportTicket ticket);
}

/// <summary>
/// Abstract base handler that implements the chaining behavior for support handlers
/// </summary>
public abstract class SupportHandler : ISupportHandler
{
// Reference to the next handler in the support chain
protected ISupportHandler _nextHandler;

/// <summary>
/// Sets the next handler in the support chain
/// </summary>
/// <param name="handler">The next support handler</param>
/// <returns>The next handler for method chaining</returns>
public ISupportHandler SetNext(ISupportHandler handler)
{
_nextHandler = handler;
return handler;
}

/// <summary>
/// Abstract method that must be implemented by concrete handlers
/// </summary>
/// <param name="ticket">The support ticket to handle</param>
public abstract void HandleTicket(SupportTicket ticket);
}

/// <summary>
/// First level support handler for basic issues
/// </summary>
public class FirstLevelSupport : SupportHandler
{
/// <summary>
/// Handles basic support tickets or escalates more complex ones
/// </summary>
/// <param name="ticket">The support ticket to handle</param>
public override void HandleTicket(SupportTicket ticket)
{
// Check if this is a basic level ticket that we can handle
if (ticket.Level == SupportLevel.Basic)
{
// Handle the ticket at this support level
Console.WriteLine($"First Level Support handling ticket #{ticket.Id} for {ticket.CustomerName}");
Console.WriteLine($"Issue: {ticket.Issue}");

// In a real system, we would:
// 1. Log the handling of the ticket
// 2. Update the ticket status in the database
// 3. Possibly send an email to the customer
}
else if (_nextHandler != null)
{
// This ticket is too complex for this level, escalate to the next level
Console.WriteLine($"First Level Support escalating ticket #{ticket.Id} to next level");
_nextHandler.HandleTicket(ticket);
}
else
{
// We're the last handler but can't handle this ticket
Console.WriteLine($"No handler available for ticket #{ticket.Id}");
}
}
}

/// <summary>
/// Second level support handler for intermediate issues
/// </summary>
public class SecondLevelSupport : SupportHandler
{
/// <summary>
/// Handles intermediate support tickets or escalates advanced ones
/// </summary>
/// <param name="ticket">The support ticket to handle</param>
public override void HandleTicket(SupportTicket ticket)
{
// Check if this is an intermediate level ticket that we can handle
if (ticket.Level == SupportLevel.Intermediate)
{
// Handle the ticket at this support level
Console.WriteLine($"Second Level Support handling ticket #{ticket.Id} for {ticket.CustomerName}");
Console.WriteLine($"Issue: {ticket.Issue}");

// In a real system, we would apply more advanced troubleshooting
}
else if (_nextHandler != null)
{
// This ticket is too complex for this level, escalate to the next level
Console.WriteLine($"Second Level Support escalating ticket #{ticket.Id} to next level");
_nextHandler.HandleTicket(ticket);
}
else
{
// We're the last handler but can't handle this ticket
Console.WriteLine($"No handler available for ticket #{ticket.Id}");
}
}
}

/// <summary>
/// Third level support handler for advanced issues
/// </summary>
public class ThirdLevelSupport : SupportHandler
{
/// <summary>
/// Handles advanced support tickets
/// </summary>
/// <param name="ticket">The support ticket to handle</param>
public override void HandleTicket(SupportTicket ticket)
{
// Check if this is an advanced level ticket that we can handle
if (ticket.Level == SupportLevel.Advanced)
{
// Handle the ticket at this support level
Console.WriteLine($"Third Level Support handling ticket #{ticket.Id} for {ticket.CustomerName}");
Console.WriteLine($"Issue: {ticket.Issue}");

// In a real system, this might involve specialized engineers
}
else if (_nextHandler != null)
{
// This is unusual - we're the highest level but can't handle it
// Maybe there's a specialized team beyond us
Console.WriteLine($"Third Level Support escalating ticket #{ticket.Id}");
_nextHandler.HandleTicket(ticket);
}
else
{
// We're the last handler but can't handle this ticket
Console.WriteLine($"No handler available for ticket #{ticket.Id}");
}
}
}

// USAGE EXAMPLE:
// // Create the support chain
// var firstLevel = new FirstLevelSupport();
// var secondLevel = new SecondLevelSupport();
// var thirdLevel = new ThirdLevelSupport();
//
// // Set up the chain of responsibility
// firstLevel.SetNext(secondLevel).SetNext(thirdLevel);
//
// // Create tickets of different levels
// var basicTicket = new SupportTicket(1, "John Doe", "Can't log in", SupportLevel.Basic);
// var intermediateTicket = new SupportTicket(2, "Jane Smith", "Database connection error", SupportLevel.Intermediate);
// var advancedTicket = new SupportTicket(3, "Bob Johnson", "Server cluster failure", SupportLevel.Advanced);
//
// // Process the tickets - each will be handled by the appropriate level
// firstLevel.HandleTicket(basicTicket); // Handled by first level
// firstLevel.HandleTicket(intermediateTicket); // Passed to second level
// firstLevel.HandleTicket(advancedTicket); // Passed to third level

When to Use

  • When more than one object may handle a request, and the handler isn't known a priori
  • When you want to issue a request to one of several objects without specifying the receiver explicitly
  • When the set of objects that can handle a request should be specified dynamically

9.4.2 - Command Pattern

The Command pattern turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as method arguments, delay or queue a request's execution, and support undoable operations.

Basic Implementation

// PROBLEM: We need to decouple the object that invokes an operation from the one that performs it.
// This allows us to parameterize objects with operations, queue operations, and support undoable operations.

/// <summary>
/// Command interface that declares a method for executing a command
/// This is the core interface for the Command pattern
/// </summary>
public interface ICommand
{
/// <summary>
/// Executes the command, typically by calling methods on the receiver
/// </summary>
void Execute();

/// <summary>
/// Undoes the command, restoring the previous state
/// </summary>
/// <remarks>
/// This is optional but enables undo functionality
/// </remarks>
void Undo();
}

/// <summary>
/// Receiver class that knows how to perform the actual operations
/// This class contains the business logic that the commands will use
/// </summary>
public class Receiver
{
/// <summary>
/// Performs some action that a command might request
/// </summary>
/// <param name="parameter">Optional parameter for the action</param>
public void Action(string parameter = null)
{
// This is where the actual work happens
Console.WriteLine($"Receiver: Performing action {(parameter ?? "without parameters")}");

// In a real application, this might:
// - Update a database
// - Modify a document
// - Send a message
// - etc.
}

/// <summary>
/// Reverses the action previously performed
/// </summary>
public void UndoAction()
{
Console.WriteLine("Receiver: Undoing previous action");

// In a real application, this would restore the previous state
}
}

/// <summary>
/// Concrete command that implements the ICommand interface
/// This class encapsulates a request as an object
/// </summary>
public class ConcreteCommand : ICommand
{
// Reference to the receiver that will perform the actual work
private readonly Receiver _receiver;

// Parameters needed for the command execution
private readonly string _parameter;

/// <summary>
/// Initializes a new instance of the ConcreteCommand class
/// </summary>
/// <param name="receiver">The receiver that will perform the actual work</param>
/// <param name="parameter">Optional parameter for the command</param>
public ConcreteCommand(Receiver receiver, string parameter = null)
{
_receiver = receiver ?? throw new ArgumentNullException(nameof(receiver));
_parameter = parameter;
}

/// <summary>
/// Executes the command by calling the appropriate method on the receiver
/// </summary>
public void Execute()
{
// The command delegates the work to the receiver
Console.WriteLine($"ConcreteCommand: Calling receiver with parameter '{_parameter}'");
_receiver.Action(_parameter);
}

/// <summary>
/// Undoes the command by calling the appropriate method on the receiver
/// </summary>
public void Undo()
{
Console.WriteLine("ConcreteCommand: Undoing action");
_receiver.UndoAction();
}
}

/// <summary>
/// Invoker class that asks the command to carry out the request
/// This class is responsible for initiating the command execution
/// </summary>
public class Invoker
{
// The command that will be executed
private ICommand _command;

// History of commands for undo functionality
private readonly Stack<ICommand> _commandHistory = new Stack<ICommand>();

/// <summary>
/// Sets the command that will be executed
/// </summary>
/// <param name="command">The command to execute</param>
public void SetCommand(ICommand command)
{
_command = command ?? throw new ArgumentNullException(nameof(command));
}

/// <summary>
/// Executes the current command and adds it to the history
/// </summary>
public void ExecuteCommand()
{
if (_command == null)
{
throw new InvalidOperationException("No command set. Call SetCommand first.");
}

Console.WriteLine("Invoker: Executing command");
_command.Execute();

// Add the command to the history for potential undo
_commandHistory.Push(_command);
}

/// <summary>
/// Undoes the last executed command
/// </summary>
public void UndoLastCommand()
{
if (_commandHistory.Count == 0)
{
Console.WriteLine("Invoker: No commands to undo");
return;
}

var lastCommand = _commandHistory.Pop();
Console.WriteLine("Invoker: Undoing last command");
lastCommand.Undo();
}
}

// USAGE EXAMPLE:
// // Create the receiver
// var receiver = new Receiver();
//
// // Create commands
// var command1 = new ConcreteCommand(receiver, "Command 1");
// var command2 = new ConcreteCommand(receiver, "Command 2");
//
// // Create the invoker
// var invoker = new Invoker();
//
// // Execute commands
// invoker.SetCommand(command1);
// invoker.ExecuteCommand();
//
// invoker.SetCommand(command2);
// invoker.ExecuteCommand();
//
// // Undo the last command
// invoker.UndoLastCommand();

Remote Control Example with Undo

// REAL-WORLD PROBLEM: We need a universal remote control that can operate multiple devices
// (lights, TV, stereo, etc.) with support for undo functionality.

/// <summary>
/// Command interface that declares methods for executing and undoing a command
/// </summary>
public interface ICommand
{
/// <summary>
/// Executes the command
/// </summary>
void Execute();

/// <summary>
/// Undoes the command, restoring the previous state
/// </summary>
void Undo();
}

/// <summary>
/// Receiver class that represents a light that can be turned on and off
/// This is one of the devices our remote control will operate
/// </summary>
public class Light
{
// The location of the light (e.g., "Living Room", "Kitchen")
private readonly string _location;

/// <summary>
/// Initializes a new instance of the Light class
/// </summary>
/// <param name="location">The location of the light</param>
public Light(string location)
{
_location = location ?? throw new ArgumentNullException(nameof(location));
}

/// <summary>
/// Turns the light on
/// </summary>
public void On()
{
Console.WriteLine($"{_location} light is ON");

// In a real system, this might control actual hardware
// through a home automation interface
}

/// <summary>
/// Turns the light off
/// </summary>
public void Off()
{
Console.WriteLine($"{_location} light is OFF");

// In a real system, this might control actual hardware
// through a home automation interface
}
}

/// <summary>
/// Concrete command that turns a light on
/// </summary>
public class LightOnCommand : ICommand
{
// Reference to the light that this command will control
private readonly Light _light;

/// <summary>
/// Initializes a new instance of the LightOnCommand class
/// </summary>
/// <param name="light">The light to control</param>
public LightOnCommand(Light light)
{
_light = light ?? throw new ArgumentNullException(nameof(light));
}

/// <summary>
/// Executes the command by turning the light on
/// </summary>
public void Execute()
{
_light.On();
}

/// <summary>
/// Undoes the command by turning the light off
/// </summary>
public void Undo()
{
_light.Off();
}
}

/// <summary>
/// Concrete command that turns a light off
/// </summary>
public class LightOffCommand : ICommand
{
// Reference to the light that this command will control
private readonly Light _light;

/// <summary>
/// Initializes a new instance of the LightOffCommand class
/// </summary>
/// <param name="light">The light to control</param>
public LightOffCommand(Light light)
{
_light = light ?? throw new ArgumentNullException(nameof(light));
}

/// <summary>
/// Executes the command by turning the light off
/// </summary>
public void Execute()
{
_light.Off();
}

/// <summary>
/// Undoes the command by turning the light on
/// </summary>
public void Undo()
{
_light.On();
}
}

/// <summary>
/// Null Object Pattern implementation - a command that does nothing
/// This avoids null checks when a slot hasn't been programmed yet
/// </summary>
public class NoCommand : ICommand
{
/// <summary>
/// Does nothing when executed
/// </summary>
public void Execute() { }

/// <summary>
/// Does nothing when undone
/// </summary>
public void Undo() { }
}

/// <summary>
/// Invoker class that represents a remote control with multiple buttons
/// This is the client that will use the commands
/// </summary>
public class RemoteControl
{
// Arrays to hold the on and off commands for each button
private readonly ICommand[] _onCommands;
private readonly ICommand[] _offCommands;

// Tracks the last command executed for undo functionality
private ICommand _undoCommand;

/// <summary>
/// Initializes a new instance of the RemoteControl class
/// </summary>
public RemoteControl()
{
// Initialize the arrays with 7 slots (buttons)
_onCommands = new ICommand[7];
_offCommands = new ICommand[7];

// Create a no-operation command as a default
ICommand noCommand = new NoCommand();

// Set all slots to the no-operation command initially
for (int i = 0; i < 7; i++)
{
_onCommands[i] = noCommand;
_offCommands[i] = noCommand;
}

// Initialize the undo command to the no-operation command
_undoCommand = noCommand;
}

/// <summary>
/// Sets the commands for a specific button slot
/// </summary>
/// <param name="slot">The button slot (0-6)</param>
/// <param name="onCommand">The command to execute when the ON button is pressed</param>
/// <param name="offCommand">The command to execute when the OFF button is pressed</param>
public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
{
if (slot < 0 || slot >= _onCommands.Length)
{
throw new ArgumentOutOfRangeException(nameof(slot), "Slot must be between 0 and 6");
}

_onCommands[slot] = onCommand ?? throw new ArgumentNullException(nameof(onCommand));
_offCommands[slot] = offCommand ?? throw new ArgumentNullException(nameof(offCommand));
}

/// <summary>
/// Simulates pressing the ON button for a specific slot
/// </summary>
/// <param name="slot">The button slot (0-6)</param>
public void OnButtonPushed(int slot)
{
if (slot < 0 || slot >= _onCommands.Length)
{
throw new ArgumentOutOfRangeException(nameof(slot), "Slot must be between 0 and 6");
}

// Execute the command and store it for potential undo
_onCommands[slot].Execute();
_undoCommand = _onCommands[slot];
}

/// <summary>
/// Simulates pressing the OFF button for a specific slot
/// </summary>
/// <param name="slot">The button slot (0-6)</param>
public void OffButtonPushed(int slot)
{
if (slot < 0 || slot >= _offCommands.Length)
{
throw new ArgumentOutOfRangeException(nameof(slot), "Slot must be between 0 and 6");
}

// Execute the command and store it for potential undo
_offCommands[slot].Execute();
_undoCommand = _offCommands[slot];
}

/// <summary>
/// Simulates pressing the UNDO button
/// </summary>
public void UndoButtonPushed()
{
// Undo the last command that was executed
_undoCommand.Undo();
}

/// <summary>
/// Returns a string representation of the remote control's current configuration
/// </summary>
/// <returns>A string showing all the commands assigned to each button</returns>
public override string ToString()
{
var stringBuilder = new System.Text.StringBuilder();
stringBuilder.AppendLine("------ Remote Control ------");

for (int i = 0; i < _onCommands.Length; i++)
{
stringBuilder.AppendLine($"[Slot {i}] {_onCommands[i].GetType().Name} | {_offCommands[i].GetType().Name}");
}

stringBuilder.AppendLine($"[Undo] {_undoCommand.GetType().Name}");

return stringBuilder.ToString();
}
}

// USAGE EXAMPLE:
// // Create the receivers (devices)
// var livingRoomLight = new Light("Living Room");
// var kitchenLight = new Light("Kitchen");
//
// // Create the commands
// var livingRoomLightOn = new LightOnCommand(livingRoomLight);
// var livingRoomLightOff = new LightOffCommand(livingRoomLight);
// var kitchenLightOn = new LightOnCommand(kitchenLight);
// var kitchenLightOff = new LightOffCommand(kitchenLight);
//
// // Create the remote control (invoker)
// var remote = new RemoteControl();
//
// // Set up the remote
// remote.SetCommand(0, livingRoomLightOn, livingRoomLightOff);
// remote.SetCommand(1, kitchenLightOn, kitchenLightOff);
//
// // Use the remote
// Console.WriteLine(remote);
// remote.OnButtonPushed(0); // Turn on living room light
// remote.OffButtonPushed(0); // Turn off living room light
// remote.OnButtonPushed(1); // Turn on kitchen light
// remote.UndoButtonPushed(); // Undo last command (turn off kitchen light)

When to Use

  • When you want to parameterize objects with operations
  • When you want to queue operations, schedule their execution, or execute them remotely
  • When you want to implement reversible operations
  • When you want to structure a system around high-level operations built on primitive operations

9.4.3 - Interpreter Pattern

The Interpreter pattern defines a grammatical representation for a language and an interpreter to interpret the grammar. It's used to interpret a particular language or notation.

Basic Implementation

// Abstract expression
public interface IExpression
{
int Interpret(Dictionary<string, int> context);
}

// Terminal expressions
public class NumberExpression : IExpression
{
private int _number;

public NumberExpression(int number)
{
_number = number;
}

public int Interpret(Dictionary<string, int> context)
{
return _number;
}
}

public class VariableExpression : IExpression
{
private string _name;

public VariableExpression(string name)
{
_name = name;
}

public int Interpret(Dictionary<string, int> context)
{
if (context.ContainsKey(_name))
{
return context[_name];
}
return 0;
}
}

// Non-terminal expressions
public class AddExpression : IExpression
{
private IExpression _left;
private IExpression _right;

public AddExpression(IExpression left, IExpression right)
{
_left = left;
_right = right;
}

public int Interpret(Dictionary<string, int> context)
{
return _left.Interpret(context) + _right.Interpret(context);
}
}

public class SubtractExpression : IExpression
{
private IExpression _left;
private IExpression _right;

public SubtractExpression(IExpression left, IExpression right)
{
_left = left;
_right = right;
}

public int Interpret(Dictionary<string, int> context)
{
return _left.Interpret(context) - _right.Interpret(context);
}
}

Simple Calculator Example

// Parser class to build the expression tree
public class ExpressionParser
{
public IExpression Parse(string expression)
{
var tokens = expression.Split(' ');
return ParseExpression(tokens, 0, tokens.Length);
}

private IExpression ParseExpression(string[] tokens, int start, int end)
{
if (end - start == 1)
{
// Single token - either a number or a variable
string token = tokens[start];
if (int.TryParse(token, out int number))
{
return new NumberExpression(number);
}
else
{
return new VariableExpression(token);
}
}

// Find the operator with the lowest precedence
int parenthesesCount = 0;
int operatorIndex = -1;
int lowestPrecedence = int.MaxValue;

for (int i = start; i < end; i++)
{
string token = tokens[i];

if (token == "(")
{
parenthesesCount++;
}
else if (token == ")")
{
parenthesesCount--;
}
else if (parenthesesCount == 0)
{
int precedence = GetOperatorPrecedence(token);
if (precedence > 0 && precedence <= lowestPrecedence)
{
lowestPrecedence = precedence;
operatorIndex = i;
}
}
}

if (operatorIndex != -1)
{
IExpression left = ParseExpression(tokens, start, operatorIndex);
IExpression right = ParseExpression(tokens, operatorIndex + 1, end);

switch (tokens[operatorIndex])
{
case "+":
return new AddExpression(left, right);
case "-":
return new SubtractExpression(left, right);
default:
throw new ArgumentException($"Unknown operator: {tokens[operatorIndex]}");
}
}

// Handle parentheses
if (tokens[start] == "(" && tokens[end - 1] == ")")
{
return ParseExpression(tokens, start + 1, end - 1);
}

throw new ArgumentException($"Invalid expression: {string.Join(" ", tokens)}");
}

private int GetOperatorPrecedence(string token)
{
switch (token)
{
case "+":
case "-":
return 1;
default:
return 0;
}
}
}

When to Use

  • When you need to interpret a language with a simple grammar
  • When you want to represent the grammar of a language as a syntax tree
  • When efficiency is not a critical concern (interpreters can be slow)
  • When you have a well-defined grammar that doesn't change frequently

9.4.4 - Iterator Pattern

The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Basic Implementation

// Iterator interface
public interface IIterator<T>
{
bool HasNext();
T Next();
}

// Aggregate interface
public interface IAggregate<T>
{
IIterator<T> CreateIterator();
}

// Concrete iterator
public class ConcreteIterator<T> : IIterator<T>
{
private readonly List<T> _collection;
private int _position = 0;

public ConcreteIterator(List<T> collection)
{
_collection = collection;
}

public bool HasNext()
{
return _position < _collection.Count;
}

public T Next()
{
if (!HasNext())
{
throw new InvalidOperationException("No more elements");
}

return _collection[_position++];
}
}

// Concrete aggregate
public class ConcreteAggregate<T> : IAggregate<T>
{
private readonly List<T> _items = new List<T>();

public void Add(T item)
{
_items.Add(item);
}

public IIterator<T> CreateIterator()
{
return new ConcreteIterator<T>(_items);
}
}

Using C# Built-in Iterators

// Custom collection with built-in iterator support
public class WordCollection : IEnumerable<string>
{
private List<string> _collection = new List<string>();

public void Add(string item)
{
_collection.Add(item);
}

public void Remove(string item)
{
_collection.Remove(item);
}

// Standard iterator implementation using yield
public IEnumerator<string> GetEnumerator()
{
foreach (var item in _collection)
{
yield return item;
}
}

// Required by IEnumerable
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

// Custom iterator that returns words in reverse order
public IEnumerable<string> GetReversed()
{
for (int i = _collection.Count - 1; i >= 0; i--)
{
yield return _collection[i];
}
}

// Custom iterator that returns only words starting with a specific letter
public IEnumerable<string> GetWordsStartingWith(char letter)
{
foreach (var word in _collection)
{
if (word.StartsWith(letter, StringComparison.OrdinalIgnoreCase))
{
yield return word;
}
}
}
}

When to Use

  • When you want to access an aggregate object's contents without exposing its internal representation
  • When you want to support multiple traversals of aggregate objects
  • When you want to provide a uniform interface for traversing different aggregate structures

9.4.5 - Mediator Pattern

The Mediator pattern defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly, allowing you to vary their interaction independently.

Basic Implementation

// Mediator interface
public interface IMediator
{
void Notify(object sender, string ev);
}

// Concrete mediator
public class ConcreteMediator : IMediator
{
private Component1 _component1;
private Component2 _component2;

public ConcreteMediator(Component1 component1, Component2 component2)
{
_component1 = component1;
_component1.SetMediator(this);

_component2 = component2;
_component2.SetMediator(this);
}

public void Notify(object sender, string ev)
{
if (ev == "A")
{
Console.WriteLine("Mediator reacts on A and triggers the following operations:");
_component2.DoC();
}
else if (ev == "D")
{
Console.WriteLine("Mediator reacts on D and triggers the following operations:");
_component1.DoB();
_component2.DoC();
}
}
}

// Base component
public class BaseComponent
{
protected IMediator _mediator;

public BaseComponent(IMediator mediator = null)
{
_mediator = mediator;
}

public void SetMediator(IMediator mediator)
{
_mediator = mediator;
}
}

// Concrete components
public class Component1 : BaseComponent
{
public void DoA()
{
Console.WriteLine("Component 1 does A");
_mediator.Notify(this, "A");
}

public void DoB()
{
Console.WriteLine("Component 1 does B");
}
}

public class Component2 : BaseComponent
{
public void DoC()
{
Console.WriteLine("Component 2 does C");
}

public void DoD()
{
Console.WriteLine("Component 2 does D");
_mediator.Notify(this, "D");
}
}

Chat Room Example

// Mediator interface
public interface IChatRoom
{
void Register(User user);
void Send(string message, User sender, User recipient = null);
}

// Concrete mediator
public class ChatRoom : IChatRoom
{
private Dictionary<string, User> _users = new Dictionary<string, User>();

public void Register(User user)
{
if (!_users.ContainsKey(user.Name))
{
_users[user.Name] = user;
user.ChatRoom = this;
}
}

public void Send(string message, User sender, User recipient = null)
{
// If recipient is specified, send private message
if (recipient != null)
{
recipient.Receive(message, sender);
}
// Otherwise, broadcast to all users except sender
else
{
foreach (var user in _users.Values)
{
if (user != sender)
{
user.Receive(message, sender);
}
}
}
}
}

// User class
public abstract class User
{
public string Name { get; }
public IChatRoom ChatRoom { get; set; }

public User(string name)
{
Name = name;
}

public void Send(string message)
{
ChatRoom.Send(message, this);
}

public void SendPrivate(string message, User recipient)
{
ChatRoom.Send(message, this, recipient);
}

public abstract void Receive(string message, User sender);
}

// Concrete user types
public class ChatUser : User
{
public ChatUser(string name) : base(name) { }

public override void Receive(string message, User sender)
{
Console.WriteLine($"{Name} received from {sender.Name}: {message}");
}
}

public class AdminUser : User
{
public AdminUser(string name) : base(name) { }

public override void Receive(string message, User sender)
{
Console.WriteLine($"ADMIN {Name} received from {sender.Name}: {message}");
}

public void Announce(string announcement)
{
Console.WriteLine($"ANNOUNCEMENT from {Name}: {announcement}");
ChatRoom.Send($"ANNOUNCEMENT: {announcement}", this);
}
}

When to Use

  • When a set of objects communicate in well-defined but complex ways
  • When reusing an object is difficult because it communicates with many other objects
  • When you want to customize behavior distributed between several classes without creating too many subclasses

9.4.6 - Memento Pattern

The Memento pattern lets you save and restore the previous state of an object without revealing the details of its implementation.

Basic Implementation

// Memento class
public class Memento
{
private string _state;

public Memento(string state)
{
_state = state;
}

public string GetState()
{
return _state;
}
}

// Originator class
public class Originator
{
private string _state;

public void SetState(string state)
{
Console.WriteLine($"Originator: Setting state to {state}");
_state = state;
}

public Memento SaveState()
{
Console.WriteLine("Originator: Saving to Memento");
return new Memento(_state);
}

public void RestoreState(Memento memento)
{
_state = memento.GetState();
Console.WriteLine($"Originator: State after restoring from Memento: {_state}");
}
}

// Caretaker class
public class Caretaker
{
private List<Memento> _mementos = new List<Memento>();
private Originator _originator;

public Caretaker(Originator originator)
{
_originator = originator;
}

public void Backup()
{
Console.WriteLine("Caretaker: Saving Originator's state");
_mementos.Add(_originator.SaveState());
}

public void Undo()
{
if (_mementos.Count == 0)
{
return;
}

var memento = _mementos[_mementos.Count - 1];
_mementos.Remove(memento);

Console.WriteLine("Caretaker: Restoring state");
_originator.RestoreState(memento);
}

public void ShowHistory()
{
Console.WriteLine("Caretaker: Here's the list of mementos:");

foreach (var memento in _mementos)
{
Console.WriteLine(memento.GetState());
}
}
}

Text Editor Example

// Memento class
public class EditorState
{
public string Content { get; }
public int CursorPosition { get; }
public DateTime Timestamp { get; }

public EditorState(string content, int cursorPosition)
{
Content = content;
CursorPosition = cursorPosition;
Timestamp = DateTime.Now;
}
}

// Originator
public class TextEditor
{
private string _content;
private int _cursorPosition;

public string Content
{
get => _content;
set
{
_content = value;
Console.WriteLine($"Content updated: {_content}");
}
}

public int CursorPosition
{
get => _cursorPosition;
set
{
_cursorPosition = value;
Console.WriteLine($"Cursor moved to position: {_cursorPosition}");
}
}

public EditorState CreateState()
{
return new EditorState(_content, _cursorPosition);
}

public void RestoreState(EditorState state)
{
_content = state.Content;
_cursorPosition = state.CursorPosition;
Console.WriteLine($"State restored: Content='{_content}', Cursor={_cursorPosition}");
}

public void Type(string text)
{
if (_cursorPosition >= 0 && _cursorPosition <= _content.Length)
{
_content = _content.Insert(_cursorPosition, text);
_cursorPosition += text.Length;
Console.WriteLine($"Typed '{text}'. New content: '{_content}'");
}
}

public void Delete(int count)
{
if (_cursorPosition >= count && _content.Length >= count)
{
_content = _content.Remove(_cursorPosition - count, count);
_cursorPosition -= count;
Console.WriteLine($"Deleted {count} characters. New content: '{_content}'");
}
}
}

// Caretaker
public class History
{
private List<EditorState> _states = new List<EditorState>();
private TextEditor _editor;

public History(TextEditor editor)
{
_editor = editor;
}

public void Save()
{
Console.WriteLine("History: Saving editor state");
_states.Add(_editor.CreateState());
}

public void Undo()
{
if (_states.Count == 0)
{
Console.WriteLine("History: No states to undo");
return;
}

var state = _states[_states.Count - 1];
_states.RemoveAt(_states.Count - 1);

Console.WriteLine("History: Restoring previous state");
_editor.RestoreState(state);
}

public void ShowHistory()
{
Console.WriteLine("History: Editor state history");
for (int i = 0; i < _states.Count; i++)
{
var state = _states[i];
Console.WriteLine($"{i}: '{state.Content}' (Cursor: {state.CursorPosition}, Time: {state.Timestamp})");
}
}
}

When to Use

  • When you need to create snapshots of an object's state to be able to restore it later
  • When direct access to an object's fields/getters/setters would violate its encapsulation
  • When you want to implement undo/redo functionality

9.4.7 - Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Basic Implementation

// Subject interface
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}

// Observer interface
public interface IObserver
{
void Update(ISubject subject);
}

// Concrete subject
public class ConcreteSubject : ISubject
{
private List<IObserver> _observers = new List<IObserver>();
private int _state;

public int State
{
get => _state;
set
{
_state = value;
Notify();
}
}

public void Attach(IObserver observer)
{
Console.WriteLine("Subject: Attached an observer");
_observers.Add(observer);
}

public void Detach(IObserver observer)
{
_observers.Remove(observer);
Console.WriteLine("Subject: Detached an observer");
}

public void Notify()
{
Console.WriteLine("Subject: Notifying observers...");

foreach (var observer in _observers)
{
observer.Update(this);
}
}
}

// Concrete observers
public class ConcreteObserverA : IObserver
{
public void Update(ISubject subject)
{
if (subject is ConcreteSubject concreteSubject)
{
Console.WriteLine($"ConcreteObserverA: Reacted to the event. New state: {concreteSubject.State}");
}
}
}

public class ConcreteObserverB : IObserver
{
public void Update(ISubject subject)
{
if (subject is ConcreteSubject concreteSubject)
{
Console.WriteLine($"ConcreteObserverB: Reacted to the event. New state: {concreteSubject.State}");
}
}
}

Weather Station Example

// Subject interface
public interface IWeatherStation
{
void RegisterObserver(IWeatherObserver observer);
void RemoveObserver(IWeatherObserver observer);
void NotifyObservers();
}

// Observer interface
public interface IWeatherObserver
{
void Update(float temperature, float humidity, float pressure);
}

// Concrete subject
public class WeatherStation : IWeatherStation
{
private List<IWeatherObserver> _observers = new List<IWeatherObserver>();
private float _temperature;
private float _humidity;
private float _pressure;

public void RegisterObserver(IWeatherObserver observer)
{
_observers.Add(observer);
}

public void RemoveObserver(IWeatherObserver observer)
{
_observers.Remove(observer);
}

public void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_temperature, _humidity, _pressure);
}
}

public void SetMeasurements(float temperature, float humidity, float pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
MeasurementsChanged();
}

private void MeasurementsChanged()
{
NotifyObservers();
}
}

// Concrete observers
public class CurrentConditionsDisplay : IWeatherObserver
{
private float _temperature;
private float _humidity;

public void Update(float temperature, float humidity, float pressure)
{
_temperature = temperature;
_humidity = humidity;
Display();
}

public void Display()
{
Console.WriteLine($"Current conditions: {_temperature}F degrees and {_humidity}% humidity");
}
}

public class StatisticsDisplay : IWeatherObserver
{
private float _maxTemp = float.MinValue;
private float _minTemp = float.MaxValue;
private float _tempSum = 0.0f;
private int _numReadings = 0;

public void Update(float temperature, float humidity, float pressure)
{
_tempSum += temperature;
_numReadings++;

if (temperature > _maxTemp)
{
_maxTemp = temperature;
}

if (temperature < _minTemp)
{
_minTemp = temperature;
}

Display();
}

public void Display()
{
Console.WriteLine($"Avg/Max/Min temperature = {_tempSum / _numReadings}/{_maxTemp}/{_minTemp}");
}
}

public class ForecastDisplay : IWeatherObserver
{
private float _currentPressure = 29.92f;
private float _lastPressure;

public void Update(float temperature, float humidity, float pressure)
{
_lastPressure = _currentPressure;
_currentPressure = pressure;
Display();
}

public void Display()
{
Console.Write("Forecast: ");
if (_currentPressure > _lastPressure)
{
Console.WriteLine("Improving weather on the way!");
}
else if (_currentPressure == _lastPressure)
{
Console.WriteLine("More of the same");
}
else
{
Console.WriteLine("Watch out for cooler, rainy weather");
}
}
}

Using C# Events

// Weather data class with events
public class WeatherData
{
// Event declaration
public event EventHandler<WeatherEventArgs> WeatherChanged;

private float _temperature;
private float _humidity;
private float _pressure;

public void SetMeasurements(float temperature, float humidity, float pressure)
{
_temperature = temperature;
_humidity = humidity;
_pressure = pressure;
OnWeatherChanged();
}

protected virtual void OnWeatherChanged()
{
WeatherChanged?.Invoke(this, new WeatherEventArgs
{
Temperature = _temperature,
Humidity = _humidity,
Pressure = _pressure
});
}
}

// Event arguments
public class WeatherEventArgs : EventArgs
{
public float Temperature { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
}

// Observer using events
public class WeatherDisplay
{
private string _displayName;

public WeatherDisplay(string displayName, WeatherData weatherData)
{
_displayName = displayName;

// Subscribe to the event
weatherData.WeatherChanged += OnWeatherChanged;
}

private void OnWeatherChanged(object sender, WeatherEventArgs e)
{
Console.WriteLine($"{_displayName} Display: Temperature={e.Temperature}F, " +
$"Humidity={e.Humidity}%, Pressure={e.Pressure}");
}
}

When to Use

  • When changes to one object require changing others, and you don't know how many objects need to change
  • When an object should be able to notify other objects without making assumptions about who these objects are
  • When a change to one object requires changing others, and you don't know how many objects need to be changed

9.4.8 - State Pattern

The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

Basic Implementation

// State interface
public interface IState
{
void Handle(Context context);
}

// Concrete states
public class ConcreteStateA : IState
{
public void Handle(Context context)
{
Console.WriteLine("ConcreteStateA handles the request");
context.State = new ConcreteStateB();
}
}

public class ConcreteStateB : IState
{
public void Handle(Context context)
{
Console.WriteLine("ConcreteStateB handles the request");
context.State = new ConcreteStateA();
}
}

// Context
public class Context
{
private IState _state;

public Context(IState initialState)
{
_state = initialState;
}

public IState State
{
get => _state;
set
{
Console.WriteLine($"Context: Transitioning to {value.GetType().Name}");
_state = value;
}
}

public void Request()
{
_state.Handle(this);
}
}

Vending Machine Example

// State interface
public interface IVendingMachineState
{
void InsertCoin(VendingMachine machine);
void EjectCoin(VendingMachine machine);
void SelectProduct(VendingMachine machine, string productCode);
void DispenseProduct(VendingMachine machine);
}

// Concrete states
public class NoCoinState : IVendingMachineState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Coin inserted");
machine.State = new HasCoinState();
}

public void EjectCoin(VendingMachine machine)
{
Console.WriteLine("No coin to eject");
}

public void SelectProduct(VendingMachine machine, string productCode)
{
Console.WriteLine("Please insert a coin first");
}

public void DispenseProduct(VendingMachine machine)
{
Console.WriteLine("Please insert a coin first");
}
}

public class HasCoinState : IVendingMachineState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Coin already inserted");
}

public void EjectCoin(VendingMachine machine)
{
Console.WriteLine("Coin ejected");
machine.State = new NoCoinState();
}

public void SelectProduct(VendingMachine machine, string productCode)
{
bool productExists = machine.CheckProductAvailability(productCode);

if (productExists)
{
Console.WriteLine($"Product {productCode} selected");
machine.SelectedProduct = productCode;
machine.State = new ProductSelectedState();
}
else
{
Console.WriteLine($"Product {productCode} not available");
}
}

public void DispenseProduct(VendingMachine machine)
{
Console.WriteLine("Please select a product first");
}
}

public class ProductSelectedState : IVendingMachineState
{
public void InsertCoin(VendingMachine machine)
{
Console.WriteLine("Coin already inserted");
}

public void EjectCoin(VendingMachine machine)
{
Console.WriteLine("Coin ejected");
machine.SelectedProduct = null;
machine.State = new NoCoinState();
}

public void SelectProduct(VendingMachine machine, string productCode)
{
Console.WriteLine($"Changing selection from {machine.SelectedProduct} to {productCode}");

bool productExists = machine.CheckProductAvailability(productCode);

if (productExists)
{
machine.SelectedProduct = productCode;
}
else
{
Console.WriteLine($"Product {productCode} not available");
}
}

public void DispenseProduct(VendingMachine machine)
{
Console.WriteLine($"Dispensing product {machine.SelectedProduct}");
machine.DispenseProductFromInventory(machine.SelectedProduct);
machine.SelectedProduct = null;
machine.State = new NoCoinState();
}
}

// Context
public class VendingMachine
{
private Dictionary<string, int> _inventory = new Dictionary<string, int>();
private IVendingMachineState _state;

public string SelectedProduct { get; set; }

public IVendingMachineState State
{
get => _state;
set
{
Console.WriteLine($"State changing to: {value.GetType().Name}");
_state = value;
}
}

public VendingMachine()
{
// Initialize with some products
_inventory.Add("A1", 5); // 5 items of product A1
_inventory.Add("B2", 3); // 3 items of product B2
_inventory.Add("C3", 2); // 2 items of product C3

// Initial state
_state = new NoCoinState();
}

public void InsertCoin()
{
_state.InsertCoin(this);
}

public void EjectCoin()
{
_state.EjectCoin(this);
}

public void SelectProduct(string productCode)
{
_state.SelectProduct(this, productCode);
}

public void DispenseProduct()
{
_state.DispenseProduct(this);
}

public bool CheckProductAvailability(string productCode)
{
return _inventory.ContainsKey(productCode) && _inventory[productCode] > 0;
}

public void DispenseProductFromInventory(string productCode)
{
if (CheckProductAvailability(productCode))
{
_inventory[productCode]--;
}
}

public void DisplayInventory()
{
Console.WriteLine("Current Inventory:");
foreach (var item in _inventory)
{
Console.WriteLine($"{item.Key}: {item.Value} remaining");
}
}
}

When to Use

  • When an object's behavior depends on its state, and it must change its behavior at runtime depending on that state
  • When operations have large, multipart conditional statements that depend on the object's state
  • When state-specific behavior should be localized in separate classes

9.4.9 - Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

Basic Implementation

// Strategy interface
public interface IStrategy
{
object DoAlgorithm(object data);
}

// Concrete strategies
public class ConcreteStrategyA : IStrategy
{
public object DoAlgorithm(object data)
{
var list = data as List<int>;
list.Sort();

return list;
}
}

public class ConcreteStrategyB : IStrategy
{
public object DoAlgorithm(object data)
{
var list = data as List<int>;
list.Sort();
list.Reverse();

return list;
}
}

// Context
public class Context
{
private IStrategy _strategy;

public Context() { }

public Context(IStrategy strategy)
{
_strategy = strategy;
}

public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}

public void DoSomeBusinessLogic()
{
Console.WriteLine("Context: Sorting data using the strategy");
var result = _strategy.DoAlgorithm(new List<int> { 1, 10, 5, 4, 2, 7 });

string resultStr = string.Join(",", result as List<int>);
Console.WriteLine(resultStr);
}
}

Payment Processing Example

// Payment strategy interface
public interface IPaymentStrategy
{
void Pay(decimal amount);
}

// Concrete strategies
public class CreditCardPayment : IPaymentStrategy
{
private string _name;
private string _cardNumber;
private string _cvv;
private string _expiryDate;

public CreditCardPayment(string name, string cardNumber, string cvv, string expiryDate)
{
_name = name;
_cardNumber = cardNumber;
_cvv = cvv;
_expiryDate = expiryDate;
}

public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using Credit Card ({MaskCardNumber(_cardNumber)})");
}

private string MaskCardNumber(string cardNumber)
{
return "XXXX-XXXX-XXXX-" + cardNumber.Substring(cardNumber.Length - 4);
}
}

public class PayPalPayment : IPaymentStrategy
{
private string _email;
private string _password;

public PayPalPayment(string email, string password)
{
_email = email;
_password = password;
}

public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using PayPal account ({_email})");
}
}

public class BankTransferPayment : IPaymentStrategy
{
private string _accountName;
private string _bankName;
private string _accountNumber;

public BankTransferPayment(string accountName, string bankName, string accountNumber)
{
_accountName = accountName;
_bankName = bankName;
_accountNumber = accountNumber;
}

public void Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} using Bank Transfer to account {MaskAccountNumber(_accountNumber)} at {_bankName}");
}

private string MaskAccountNumber(string accountNumber)
{
return "XXXXX" + accountNumber.Substring(accountNumber.Length - 4);
}
}

// Context
public class ShoppingCart
{
private List<Item> _items = new List<Item>();

public void AddItem(Item item)
{
_items.Add(item);
}

public decimal CalculateTotal()
{
decimal total = 0;
foreach (var item in _items)
{
total += item.Price;
}
return total;
}

public void Pay(IPaymentStrategy paymentStrategy)
{
decimal amount = CalculateTotal();
paymentStrategy.Pay(amount);
}
}

// Item class
public class Item
{
public string Name { get; }
public decimal Price { get; }

public Item(string name, decimal price)
{
Name = name;
Price = price;
}
}

When to Use

  • When you want to define a family of algorithms
  • When you need to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime
  • When you want to isolate the implementation details of an algorithm from the code that uses it
  • When you have a class with a behavior that appears as a conditional statement over several operations

9.4.10 - Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

Basic Implementation

// Abstract class with template method
public abstract class AbstractClass
{
// Template method
public void TemplateMethod()
{
BaseOperation1();
RequiredOperation1();
BaseOperation2();
Hook1();
RequiredOperation2();
BaseOperation3();
Hook2();
}

// Operations already implemented
protected void BaseOperation1()
{
Console.WriteLine("AbstractClass: I am doing the bulk of the work");
}

protected void BaseOperation2()
{
Console.WriteLine("AbstractClass: But I let subclasses override some operations");
}

protected void BaseOperation3()
{
Console.WriteLine("AbstractClass: But I am doing the bulk of the work anyway");
}

// Operations that subclasses must implement
protected abstract void RequiredOperation1();
protected abstract void RequiredOperation2();

// Hooks - optional operations with default implementation
protected virtual void Hook1() { }
protected virtual void Hook2() { }
}

// Concrete implementations
public class ConcreteClass1 : AbstractClass
{
protected override void RequiredOperation1()
{
Console.WriteLine("ConcreteClass1: Implemented RequiredOperation1");
}

protected override void RequiredOperation2()
{
Console.WriteLine("ConcreteClass1: Implemented RequiredOperation2");
}
}

public class ConcreteClass2 : AbstractClass
{
protected override void RequiredOperation1()
{
Console.WriteLine("ConcreteClass2: Implemented RequiredOperation1");
}

protected override void RequiredOperation2()
{
Console.WriteLine("ConcreteClass2: Implemented RequiredOperation2");
}

protected override void Hook1()
{
Console.WriteLine("ConcreteClass2: Overridden Hook1");
}
}

Document Processing Example

// Abstract document processor
public abstract class DocumentProcessor
{
// Template method
public void ProcessDocument(string document)
{
Console.WriteLine("Starting document processing...");

string content = ReadDocument(document);
string processedContent = ProcessContent(content);

if (ShouldApplyWatermark())
{
processedContent = ApplyWatermark(processedContent);
}

if (ShouldEncrypt())
{
processedContent = EncryptContent(processedContent);
}

SaveDocument(processedContent);
NotifyProcessingComplete();

Console.WriteLine("Document processing completed.");
}

// Common operations
protected string ReadDocument(string document)
{
Console.WriteLine($"Reading document: {document}");
// In a real implementation, this would read the file
return $"Content of {document}";
}

protected void SaveDocument(string content)
{
Console.WriteLine("Saving processed document");
// In a real implementation, this would save the file
}

protected void NotifyProcessingComplete()
{
Console.WriteLine("Sending notification about completed processing");
}

// Operations that must be implemented by subclasses
protected abstract string ProcessContent(string content);

// Hook methods with default implementations
protected virtual string ApplyWatermark(string content)
{
return content + " [WATERMARKED]";
}

protected virtual string EncryptContent(string content)
{
return content + " [ENCRYPTED]";
}

protected virtual bool ShouldApplyWatermark()
{
return false;
}

protected virtual bool ShouldEncrypt()
{
return false;
}
}

// Concrete implementations
public class PdfProcessor : DocumentProcessor
{
protected override string ProcessContent(string content)
{
Console.WriteLine("Processing PDF content");
return content + " [PDF PROCESSED]";
}

protected override bool ShouldApplyWatermark()
{
return true;
}
}

public class WordProcessor : DocumentProcessor
{
protected override string ProcessContent(string content)
{
Console.WriteLine("Processing Word document content");
return content + " [WORD PROCESSED]";
}
}

public class ConfidentialDocumentProcessor : DocumentProcessor
{
protected override string ProcessContent(string content)
{
Console.WriteLine("Processing confidential document content");
return content + " [CONFIDENTIAL PROCESSED]";
}

protected override bool ShouldApplyWatermark()
{
return true;
}

protected override bool ShouldEncrypt()
{
return true;
}

protected override string ApplyWatermark(string content)
{
return content + " [CONFIDENTIAL WATERMARK]";
}
}

When to Use

  • When you want to let clients extend only particular steps of an algorithm, but not the whole algorithm or its structure
  • When you have several classes that contain almost identical algorithms with some minor differences
  • When you want to control at which points subclassing is allowed

9.4.11 - Visitor Pattern

The Visitor pattern lets you separate algorithms from the objects on which they operate. It allows adding new operations to existing object structures without modifying those structures.

Basic Implementation

// Visitor interface
public interface IVisitor
{
void VisitConcreteElementA(ConcreteElementA element);
void VisitConcreteElementB(ConcreteElementB element);
}

// Element interface
public interface IElement
{
void Accept(IVisitor visitor);
}

// Concrete elements
public class ConcreteElementA : IElement
{
public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementA(this);
}

public string ExclusiveMethodOfConcreteElementA()
{
return "A";
}
}

public class ConcreteElementB : IElement
{
public void Accept(IVisitor visitor)
{
visitor.VisitConcreteElementB(this);
}

public string SpecialMethodOfConcreteElementB()
{
return "B";
}
}

// Concrete visitors
public class ConcreteVisitor1 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA element)
{
Console.WriteLine($"ConcreteVisitor1: {element.ExclusiveMethodOfConcreteElementA()}");
}

public void VisitConcreteElementB(ConcreteElementB element)
{
Console.WriteLine($"ConcreteVisitor1: {element.SpecialMethodOfConcreteElementB()}");
}
}

public class ConcreteVisitor2 : IVisitor
{
public void VisitConcreteElementA(ConcreteElementA element)
{
Console.WriteLine($"ConcreteVisitor2: {element.ExclusiveMethodOfConcreteElementA()}");
}

public void VisitConcreteElementB(ConcreteElementB element)
{
Console.WriteLine($"ConcreteVisitor2: {element.SpecialMethodOfConcreteElementB()}");
}
}

// Object structure
public class ObjectStructure
{
private List<IElement> _elements = new List<IElement>();

public void Attach(IElement element)
{
_elements.Add(element);
}

public void Detach(IElement element)
{
_elements.Remove(element);
}

public void Accept(IVisitor visitor)
{
foreach (var element in _elements)
{
element.Accept(visitor);
}
}
}

Document Object Model Example

// Element interface
public interface IDocumentElement
{
void Accept(IDocumentVisitor visitor);
}

// Concrete elements
public class TextElement : IDocumentElement
{
public string Text { get; }

public TextElement(string text)
{
Text = text;
}

public void Accept(IDocumentVisitor visitor)
{
visitor.VisitText(this);
}
}

public class ImageElement : IDocumentElement
{
public string Source { get; }
public int Width { get; }
public int Height { get; }

public ImageElement(string source, int width, int height)
{
Source = source;
Width = width;
Height = height;
}

public void Accept(IDocumentVisitor visitor)
{
visitor.VisitImage(this);
}
}

public class TableElement : IDocumentElement
{
public int Rows { get; }
public int Columns { get; }
public List<string> Data { get; }

public TableElement(int rows, int columns, List<string> data)
{
Rows = rows;
Columns = columns;
Data = data;
}

public void Accept(IDocumentVisitor visitor)
{
visitor.VisitTable(this);
}
}

// Visitor interface
public interface IDocumentVisitor
{
void VisitText(TextElement element);
void VisitImage(ImageElement element);
void VisitTable(TableElement element);
}

// Concrete visitors
public class HtmlExportVisitor : IDocumentVisitor
{
private StringBuilder _result = new StringBuilder();

public void VisitText(TextElement element)
{
_result.AppendLine($"<p>{element.Text}</p>");
}

public void VisitImage(ImageElement element)
{
_result.AppendLine($"<img src=\"{element.Source}\" width=\"{element.Width}\" height=\"{element.Height}\" />");
}

public void VisitTable(TableElement element)
{
_result.AppendLine("<table>");

int cellIndex = 0;
for (int i = 0; i < element.Rows; i++)
{
_result.AppendLine(" <tr>");

for (int j = 0; j < element.Columns; j++)
{
string cellData = cellIndex < element.Data.Count ? element.Data[cellIndex] : "";
_result.AppendLine($" <td>{cellData}</td>");
cellIndex++;
}

_result.AppendLine(" </tr>");
}

_result.AppendLine("</table>");
}

public string GetResult()
{
return _result.ToString();
}
}

public class MarkdownExportVisitor : IDocumentVisitor
{
private StringBuilder _result = new StringBuilder();

public void VisitText(TextElement element)
{
_result.AppendLine(element.Text);
_result.AppendLine();
}

public void VisitImage(ImageElement element)
{
_result.AppendLine($"![Image]({element.Source})");
_result.AppendLine();
}

public void VisitTable(TableElement element)
{
// Header row
for (int j = 0; j < element.Columns; j++)
{
_result.Append("| Column " + (j + 1) + " ");
}
_result.AppendLine("|");

// Separator row
for (int j = 0; j < element.Columns; j++)
{
_result.Append("| --- ");
}
_result.AppendLine("|");

// Data rows
int cellIndex = 0;
for (int i = 0; i < element.Rows; i++)
{
for (int j = 0; j < element.Columns; j++)
{
string cellData = cellIndex < element.Data.Count ? element.Data[cellIndex] : "";
_result.Append("| " + cellData + " ");
cellIndex++;
}
_result.AppendLine("|");
}

_result.AppendLine();
}

public string GetResult()
{
return _result.ToString();
}
}

// Document class
public class Document
{
private List<IDocumentElement> _elements = new List<IDocumentElement>();

public void Add(IDocumentElement element)
{
_elements.Add(element);
}

public void Remove(IDocumentElement element)
{
_elements.Remove(element);
}

public void Accept(IDocumentVisitor visitor)
{
foreach (var element in _elements)
{
element.Accept(visitor);
}
}
}

When to Use

  • When you need to perform operations on all elements of a complex object structure (like a tree)
  • When you want to add new operations to existing object structures without modifying those structures
  • When the object structure classes rarely change but you often want to define new operations over the structure
  • When related operations are scattered across multiple classes, and you want to consolidate them

9.4.11 - Understanding Behavioral Patterns: A Beginner's Guide

Behavioral patterns focus on how objects communicate with each other. They help distribute responsibilities between objects in a way that makes your code more flexible, maintainable, and reusable. Let's explore these patterns in a way that's easy to understand for beginners.

The Purpose of Behavioral Patterns

Think of behavioral patterns as the "social rules" for how objects interact with each other. Just like people follow certain protocols when communicating, objects in your code can follow patterns that make their interactions more effective. These patterns help you:

  1. Define clear communication channels: Establish how objects send messages to each other
  2. Distribute responsibilities: Assign specific roles to different objects
  3. Reduce tight coupling: Make objects less dependent on each other's internal details
  4. Encapsulate behaviors: Package related behaviors together in a way that's easy to reuse

When to Use Each Behavioral Pattern

  1. Chain of Responsibility Pattern

    • Use when: You want to pass a request along a chain of handlers until one of them handles it
    • Real-world analogy: Customer support escalation (basic → intermediate → advanced)
    • Key benefit: Decouples senders from receivers and allows multiple objects to handle a request
    • Example scenario: Processing support tickets, approval workflows, event handling
  2. Command Pattern

    • Use when: You want to turn a request into a stand-alone object
    • Real-world analogy: Restaurant order slip that contains all details of a customer's order
    • Key benefit: Decouples the object that invokes an operation from the one that performs it
    • Example scenario: GUI buttons, macro recording, transaction processing, undo functionality
  3. Interpreter Pattern

    • Use when: You need to interpret a specialized language or notation
    • Real-world analogy: A translator who converts one language to another
    • Key benefit: Provides a way to evaluate language grammar or expressions
    • Example scenario: SQL parsers, regular expression engines, mathematical expression evaluators
  4. Iterator Pattern

    • Use when: You need to access elements of a collection without exposing its internal structure
    • Real-world analogy: A TV remote that lets you cycle through channels without knowing how they're stored
    • Key benefit: Provides a standard way to traverse different collections
    • Example scenario: Traversing lists, trees, or custom data structures
  5. Mediator Pattern

    • Use when: You want to reduce dependencies between objects by making them communicate indirectly
    • Real-world analogy: Air traffic control tower coordinating multiple planes
    • Key benefit: Reduces coupling between components by centralizing communication
    • Example scenario: Chat rooms, event management systems, complex UI forms
  6. Memento Pattern

    • Use when: You need to capture and restore an object's internal state
    • Real-world analogy: Taking a snapshot that can be used to restore a previous state
    • Key benefit: Provides the ability to restore an object to its previous state
    • Example scenario: Implementing undo/redo, saving game states, creating checkpoints
  7. Observer Pattern

    • Use when: You need a way for objects to be notified of changes in other objects
    • Real-world analogy: Subscribers receiving notifications when a newspaper publishes new content
    • Key benefit: Defines a one-to-many dependency between objects
    • Example scenario: Event handling systems, MVC architecture, reactive programming
  8. State Pattern

    • Use when: An object's behavior should change based on its internal state
    • Real-world analogy: Traffic light changing behavior based on its current color
    • Key benefit: Encapsulates state-specific behavior in separate classes
    • Example scenario: Workflow systems, game character states, vending machines
  9. Strategy Pattern

    • Use when: You want to define a family of interchangeable algorithms
    • Real-world analogy: Choosing different routes to reach a destination based on traffic conditions
    • Key benefit: Allows algorithms to vary independently from clients that use them
    • Example scenario: Sorting algorithms, payment processing methods, compression techniques
  10. Template Method Pattern

    • Use when: You want to define the skeleton of an algorithm, deferring some steps to subclasses
    • Real-world analogy: A cooking recipe with some steps that can be customized
    • Key benefit: Lets subclasses redefine certain steps of an algorithm without changing its structure
    • Example scenario: Document generation, data processing pipelines, framework hooks
  11. Visitor Pattern

    • Use when: You need to perform operations on elements of a complex object structure
    • Real-world analogy: A building inspector visiting each room in a building
    • Key benefit: Lets you add new operations without modifying the classes of the elements
    • Example scenario: Document object models, abstract syntax trees, complex data structures

Common Misconceptions

  1. "Behavioral patterns are only about inheritance"

    • While some behavioral patterns use inheritance, many focus on object composition and delegation.
  2. "These patterns are too complex for small projects"

    • Even small projects can benefit from well-applied behavioral patterns, especially for parts that are likely to change.
  3. "Using patterns means writing more code"

    • Initially yes, but patterns often reduce code in the long run by making it more reusable and maintainable.

Practical Implementation Tips

  1. For Chain of Responsibility:

    • Design handlers to be self-contained and focused on a single responsibility.
    • Consider making the chain dynamic so handlers can be added or removed at runtime.
  2. For Command:

    • Keep commands simple and focused on a single action.
    • Consider using the Composite pattern with Command to support macros (sequences of commands).
  3. For Iterator:

    • Use the built-in C# iteration mechanisms (IEnumerable, foreach) when possible.
    • Consider implementing both internal and external iterators for different use cases.
  4. For Observer:

    • Consider using C#'s built-in event mechanism for simple observer implementations.
    • Be careful about memory leaks caused by observers not being properly unregistered.
  5. For State:

    • Ensure state transitions are clearly defined and controlled.
    • Consider using a state machine library for complex state management.
  6. For Strategy:

    • Use interfaces to define the strategy contract.
    • Consider using lambda expressions for simple strategies.

Choosing Between Behavioral Patterns

Sometimes it can be difficult to choose which behavioral pattern to use. Here's a simple decision guide:

  • If you need to pass a request through a series of processing objects: Chain of Responsibility
  • If you need to encapsulate a request as an object: Command
  • If you need to interpret a language or notation: Interpreter
  • If you need to traverse a collection without exposing its structure: Iterator
  • If you need to reduce direct dependencies between objects: Mediator
  • If you need to capture and restore object state: Memento
  • If you need objects to be notified of changes in other objects: Observer
  • If you need an object to change its behavior based on its state: State
  • If you need to select algorithms at runtime: Strategy
  • If you need to define the skeleton of an algorithm with customizable steps: Template Method
  • If you need to perform operations across a complex object structure: Visitor

Remember, design patterns are tools, not rules. Use them when they solve a problem, not just because they exist.