Behavioral Patterns
Behavioral patterns from the Gang of Four’s “Design Patterns” (1994). Observer pattern predates GoF, originating in Smalltalk’s Model-View-Controller (MVC) architecture (1979).
Behavioral patterns focus on communication between objects and define how objects interact and distribute responsibilities.
Observer Pattern
Intent: Define one-to-many dependency so state changes are automatically communicated to dependents.
Problem Solved: Maintaining consistency between related objects without tight coupling.
Modern C# Implementation:
// Using built-in events
public class NewsAgency
{
public event Action<string> NewsPublished;
private string news;
public string News
{
get => news;
set
{
news = value;
NewsPublished?.Invoke(news);
}
}
}
// Subscribers
public class NewsChannel
{
private readonly string name;
public NewsChannel(string name, NewsAgency agency)
{
this.name = name;
agency.NewsPublished += HandleNews;
}
private void HandleNews(string news)
{
Console.WriteLine($"{name} received: {news}");
}
}
// Usage
var agency = new NewsAgency();
var cnn = new NewsChannel("CNN", agency);
var bbc = new NewsChannel("BBC", agency);
agency.News = "Breaking news!";
When to Use: Event-driven systems, pub/sub architectures, MVC/MVVM data binding. When to Avoid: Simple callbacks suffice, one-to-one relationships.
Strategy Pattern
Intent: Define family of algorithms, make them interchangeable at runtime.
Problem Solved: Eliminating conditional logic for selecting algorithms.
Example:
public interface ICompressionStrategy
{
byte[] Compress(byte[] data);
}
public class GzipCompression : ICompressionStrategy
{
public byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionMode.Compress))
{
gzip.Write(data, 0, data.Length);
}
return output.ToArray();
}
}
public class ZipCompression : ICompressionStrategy
{
public byte[] Compress(byte[] data)
{
// ZIP implementation
return data; // Simplified
}
}
public class FileCompressor
{
private ICompressionStrategy strategy;
public void SetStrategy(ICompressionStrategy strategy)
{
this.strategy = strategy;
}
public byte[] CompressFile(byte[] data)
{
return strategy.Compress(data);
}
}
// Usage
var compressor = new FileCompressor();
compressor.SetStrategy(new GzipCompression());
var compressed = compressor.CompressFile(data);
When to Use: Multiple algorithms for same task, runtime selection needed. When to Avoid: Single algorithm, no variation needed.
Command Pattern
Intent: Encapsulate requests as objects to support undo/redo, queuing, and logging.
Problem Solved: Decoupling request sender from receiver, enabling operation history.
Example:
public interface ICommand
{
void Execute();
void Undo();
}
public class TransferMoneyCommand : ICommand
{
private readonly BankAccount from;
private readonly BankAccount to;
private readonly decimal amount;
private bool executed = false;
public TransferMoneyCommand(BankAccount from, BankAccount to, decimal amount)
{
this.from = from;
this.to = to;
this.amount = amount;
}
public void Execute()
{
from.Withdraw(amount);
to.Deposit(amount);
executed = true;
}
public void Undo()
{
if (executed)
{
to.Withdraw(amount);
from.Deposit(amount);
executed = false;
}
}
}
// Command invoker with undo/redo
public class CommandManager
{
private readonly Stack<ICommand> undoStack = new();
private readonly Stack<ICommand> redoStack = new();
public void Execute(ICommand command)
{
command.Execute();
undoStack.Push(command);
redoStack.Clear();
}
public void Undo()
{
if (undoStack.Count > 0)
{
var command = undoStack.Pop();
command.Undo();
redoStack.Push(command);
}
}
public void Redo()
{
if (redoStack.Count > 0)
{
var command = redoStack.Pop();
command.Execute();
undoStack.Push(command);
}
}
}
When to Use: Undo/redo functionality, operation queuing, macro recording. When to Avoid: Simple method calls suffice.
State Pattern
Intent: Allow object to alter behavior when internal state changes.
Problem Solved: Eliminating complex state-based conditionals.
Modern Switch Expression:
public enum OrderState { Pending, Confirmed, Shipped, Delivered, Cancelled }
public enum OrderAction { Confirm, Ship, Deliver, Cancel }
public class Order
{
public OrderState State { get; private set; } = OrderState.Pending;
public void ProcessAction(OrderAction action)
{
var previousState = State;
State = (State, action) switch
{
(OrderState.Pending, OrderAction.Confirm) => OrderState.Confirmed,
(OrderState.Pending, OrderAction.Cancel) => OrderState.Cancelled,
(OrderState.Confirmed, OrderAction.Ship) => OrderState.Shipped,
(OrderState.Confirmed, OrderAction.Cancel) => OrderState.Cancelled,
(OrderState.Shipped, OrderAction.Deliver) => OrderState.Delivered,
_ => State // Invalid transition - maintain state
};
if (previousState != State)
{
Console.WriteLine($"Order state: {previousState} -> {State}");
}
}
}
When to Use: Complex state-dependent behavior, state machines. When to Avoid: Simple if/switch statements suffice.
Chain of Responsibility Pattern
Intent: Pass request along chain of handlers until one processes it.
Problem Solved: Decoupling sender from receiver, dynamic handler composition.
Example:
public abstract class Handler<T>
{
protected Handler<T> next;
public Handler<T> SetNext(Handler<T> handler)
{
next = handler;
return handler;
}
public virtual T Handle(T request)
{
return next?.Handle(request) ?? request;
}
}
public class ValidationHandler : Handler<HttpRequest>
{
public override HttpRequest Handle(HttpRequest request)
{
if (string.IsNullOrEmpty(request.Body))
{
request.AddError("Body is required");
return request;
}
return base.Handle(request);
}
}
public class AuthenticationHandler : Handler<HttpRequest>
{
public override HttpRequest Handle(HttpRequest request)
{
if (!request.Headers.ContainsKey("Authorization"))
{
request.AddError("Unauthorized");
return request;
}
return base.Handle(request);
}
}
public class LoggingHandler : Handler<HttpRequest>
{
public override HttpRequest Handle(HttpRequest request)
{
Console.WriteLine($"Processing request: {request.Path}");
return base.Handle(request);
}
}
// Usage - ASP.NET Core middleware is a real-world example
var chain = new LoggingHandler();
chain.SetNext(new AuthenticationHandler())
.SetNext(new ValidationHandler());
var result = chain.Handle(request);
When to Use: Multiple handlers, handler order matters, middleware pipelines. When to Avoid: Single handler, static handler selection.
Template Method Pattern
Intent: Define algorithm skeleton, let subclasses override specific steps.
Problem Solved: Code reuse while allowing variation in algorithm steps.
Example:
public abstract class DataProcessor
{
// Template method
public void Process()
{
var data = ReadData();
var processed = TransformData(data);
ValidateData(processed);
SaveData(processed);
}
protected abstract string ReadData();
protected abstract string TransformData(string data);
protected abstract void SaveData(string data);
// Hook method with default implementation
protected virtual void ValidateData(string data)
{
if (string.IsNullOrEmpty(data))
throw new InvalidDataException("Data cannot be empty");
}
}
public class CsvDataProcessor : DataProcessor
{
protected override string ReadData() => File.ReadAllText("data.csv");
protected override string TransformData(string data)
{
// CSV-specific transformation
return data.ToUpper();
}
protected override void SaveData(string data)
{
File.WriteAllText("output.csv", data);
}
}
public class JsonDataProcessor : DataProcessor
{
protected override string ReadData() => File.ReadAllText("data.json");
protected override string TransformData(string data)
{
// JSON-specific transformation
return JsonSerializer.Serialize(JsonSerializer.Deserialize<object>(data));
}
protected override void SaveData(string data)
{
File.WriteAllText("output.json", data);
}
}
When to Use: Common algorithm structure with varying steps. When to Avoid: No variation in steps, composition is more flexible.
Mediator Pattern
Intent: Centralize complex communications between objects.
Problem Solved: Reducing many-to-many relationships to many-to-one.
Example:
public interface IChatMediator
{
void SendMessage(string message, User sender);
void AddUser(User user);
}
public class ChatRoom : IChatMediator
{
private readonly List<User> users = new();
public void AddUser(User user)
{
users.Add(user);
user.SetMediator(this);
}
public void SendMessage(string message, User sender)
{
foreach (var user in users.Where(u => u != sender))
{
user.ReceiveMessage(message, sender.Name);
}
}
}
public class User
{
public string Name { get; }
private IChatMediator mediator;
public User(string name)
{
Name = name;
}
public void SetMediator(IChatMediator mediator)
{
this.mediator = mediator;
}
public void Send(string message)
{
Console.WriteLine($"{Name} sends: {message}");
mediator.SendMessage(message, this);
}
public void ReceiveMessage(string message, string senderName)
{
Console.WriteLine($"{Name} receives from {senderName}: {message}");
}
}
// Usage
var chatRoom = new ChatRoom();
var john = new User("John");
var jane = new User("Jane");
chatRoom.AddUser(john);
chatRoom.AddUser(jane);
john.Send("Hello!");
When to Use: Many-to-many object relationships, MediatR library for CQRS. When to Avoid: Simple one-to-one or one-to-many relationships.
Memento Pattern
Intent: Capture and restore object state without violating encapsulation.
Problem Solved: Implementing undo/redo while keeping internal state private.
Example:
// Memento
public class EditorMemento
{
public string Content { get; }
public int CursorPosition { get; }
internal EditorMemento(string content, int cursorPosition)
{
Content = content;
CursorPosition = cursorPosition;
}
}
// Originator
public class TextEditor
{
public string Content { get; private set; } = "";
public int CursorPosition { get; private set; } = 0;
public void Write(string text)
{
Content = Content.Insert(CursorPosition, text);
CursorPosition += text.Length;
}
public EditorMemento Save() => new(Content, CursorPosition);
public void Restore(EditorMemento memento)
{
Content = memento.Content;
CursorPosition = memento.CursorPosition;
}
}
// Caretaker
public class EditorHistory
{
private readonly Stack<EditorMemento> history = new();
public void Save(EditorMemento memento) => history.Push(memento);
public EditorMemento Undo()
{
return history.Count > 1 ? history.Pop() : null;
}
}
When to Use: Undo/redo functionality, snapshots, transaction rollback. When to Avoid: State is simple enough to store directly.
Visitor Pattern
Intent: Add operations to object hierarchies without modifying them.
Problem Solved: Adding new operations without changing existing classes.
Modern Pattern Matching (Preferred):
public abstract record Expression;
public record Number(double Value) : Expression;
public record Addition(Expression Left, Expression Right) : Expression;
public record Multiplication(Expression Left, Expression Right) : Expression;
public static class ExpressionExtensions
{
public static string Print(this Expression expr) => expr switch
{
Number n => n.Value.ToString(),
Addition a => $"({a.Left.Print()} + {a.Right.Print()})",
Multiplication m => $"({m.Left.Print()} * {m.Right.Print()})",
_ => throw new ArgumentException("Unknown expression")
};
public static double Evaluate(this Expression expr) => expr switch
{
Number n => n.Value,
Addition a => a.Left.Evaluate() + a.Right.Evaluate(),
Multiplication m => m.Left.Evaluate() * m.Right.Evaluate(),
_ => throw new ArgumentException("Unknown expression")
};
}
// Usage
var expr = new Addition(new Number(2), new Multiplication(new Number(3), new Number(4)));
Console.WriteLine(expr.Print()); // (2 + (3 * 4))
Console.WriteLine(expr.Evaluate()); // 14
When to Use: Operations on complex object structures, expression trees. When to Avoid: Simple structures - use pattern matching instead.
Quick Reference
Behavioral Pattern Comparison
Pattern | Intent | Problem Solved | When to Use | When to Avoid |
---|---|---|---|---|
Observer | Notify dependents of state changes | Maintaining consistency between objects | Event systems, data binding, pub/sub | Simple callbacks work |
Strategy | Interchangeable algorithms | Eliminating algorithm conditionals | Runtime algorithm selection | Single algorithm |
Command | Encapsulate requests as objects | Undo/redo, operation queuing | Macro recording, transaction systems | Simple method calls |
State | Behavior changes with state | Complex state conditionals | State machines, workflow | Simple if/switch suffices |
Chain of Responsibility | Pass request along handler chain | Decoupling sender from receiver | Middleware pipelines, request processing | Single handler |
Template Method | Algorithm skeleton in base class | Code reuse with variation | Common workflow with varying steps | No variation needed |
Mediator | Centralize object communications | Many-to-many complexity | Chat systems, CQRS (MediatR) | Simple relationships |
Memento | Capture/restore state | Undo/redo while maintaining encapsulation | Snapshots, transaction rollback | Simple state storage |
Visitor | Add operations to hierarchies | Adding operations without modification | Expression trees, AST processing | Pattern matching is simpler |
Iterator | Sequential access to elements | Collection traversal | Custom iteration logic | foreach works |
Interpreter | Define language grammar | Domain-specific languages | Simple DSLs | Complex grammars - use parser |
Pattern Selection Guide
Choose Observer when:
- One-to-many dependencies
- Automatic notification of changes
- Example: Event-driven architectures, MVVM data binding
Choose Strategy when:
- Multiple algorithms for same operation
- Algorithm varies at runtime
- Example: Payment processing (credit card, PayPal, crypto)
Choose Command when:
- Need to parameterize operations
- Queue, log, or undo operations
- Example: Text editor operations, transaction systems
Choose State when:
- Behavior depends on complex state
- Many state transitions
- Example: Order lifecycle, TCP connection states
Choose Chain of Responsibility when:
- Multiple objects can handle request
- Handler not known in advance
- Example: ASP.NET Core middleware, exception handling
Choose Template Method when:
- Algorithm structure is fixed
- Steps vary by subclass
- Example: Data import workflows (CSV, JSON, XML)
Choose Mediator when:
- Complex object interactions
- Decoupling communicating objects
- Example: MediatR for CQRS, chat applications
Choose Memento when:
- Need to save/restore state
- Encapsulation must be maintained
- Example: Game save states, document revision history
Avoid Visitor - use pattern matching instead in modern C#
Modern C# Alternatives
// Instead of Observer - use events
public event EventHandler<DataChangedEventArgs> DataChanged;
// Instead of Strategy - use delegates/Func
public void Process(Func<Data, Result> strategy) => strategy(data);
// Instead of State - use switch expressions
var nextState = (currentState, action) switch
{
(State.A, Action.X) => State.B,
(State.B, Action.Y) => State.C,
_ => currentState
};
// Instead of Template Method - use composition
public class DataProcessor
{
private readonly IReader reader;
private readonly ITransformer transformer;
private readonly IWriter writer;
public void Process()
{
var data = reader.Read();
var transformed = transformer.Transform(data);
writer.Write(transformed);
}
}
// Instead of Visitor - use pattern matching
public string Process(Expression expr) => expr switch
{
Number n => ProcessNumber(n),
Addition a => ProcessAddition(a),
_ => throw new NotSupportedException()
};
Found this guide helpful? Share it with your team:
Share on LinkedIn