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