Read the article
Class structure explained: The strategy interface (IAllowanceStrategy) defines a single method: CalculateAllowance(baseAmount, context). Concrete strategies (CompanyAAllowanceStrategy, CompanyBAllowanceStrategy, CompanyCAllowanceStrategy) implement that interface with different rules (e.g. housing + transport, lump sum, performance bonus). The context (Salary) holds a reference to IAllowanceStrategy (injected via constructor), holds the base amount, and calls _allowanceStrategy.CalculateAllowance(BaseAmount, context) when it needs the total. The context never branches on company or strategy type; it only knows the interface. To add a new company or policy, add a new strategy class and wire it (e.g. via DI or a factory by company id); Salary stays unchanged.
Full working example: Salary and company-specific allowances
Scenario: You need to compute total salary (base + allowances) for employees. Different companies use different allowance rules: Company A uses housing (15% of base) + fixed transport; Company B uses a single benefits lump sum (20% of base); Company C uses a performance bonus (10% of base if rating ≥ 4). The Salary type must work for all companies without holding company-specific fields. The Strategy pattern lets each company supply its own IAllowanceStrategy implementation; Salary only delegates to the strategy.
1. Strategy interface and context data
namespace StrategyExample.Salary;
public interface IAllowanceStrategy
{
decimal CalculateAllowance(decimal baseAmount, SalaryContext context);
}
public class SalaryContext
{
public string CompanyId { get; set; } = "";
public string EmployeeId { get; set; } = "";
public int YearsOfService { get; set; }
public decimal PerformanceRating { get; set; }
}
2. Concrete strategies (one per company policy)
namespace StrategyExample.Salary;
public class CompanyAAllowanceStrategy : IAllowanceStrategy
{
public decimal CalculateAllowance(decimal baseAmount, SalaryContext context)
{
var housing = baseAmount * 0.15m;
var transport = 500m;
return housing + transport;
}
}
public class CompanyBAllowanceStrategy : IAllowanceStrategy
{
public decimal CalculateAllowance(decimal baseAmount, SalaryContext context) =>
baseAmount * 0.20m;
}
public class CompanyCAllowanceStrategy : IAllowanceStrategy
{
public decimal CalculateAllowance(decimal baseAmount, SalaryContext context) =>
context.PerformanceRating >= 4.0m ? baseAmount * 0.10m : 0m;
}
3. Context: Salary uses the strategy
namespace StrategyExample.Salary;
public class Salary
{
private readonly IAllowanceStrategy _allowanceStrategy;
public decimal BaseAmount { get; set; }
public Salary(IAllowanceStrategy allowanceStrategy) => _allowanceStrategy = allowanceStrategy;
public decimal GetTotal(SalaryContext context) =>
BaseAmount + _allowanceStrategy.CalculateAllowance(BaseAmount, context);
}
// Usage:
// var salary = new Salary(new CompanyAAllowanceStrategy()) { BaseAmount = 5000 };
// var total = salary.GetTotal(new SalaryContext { CompanyId = "A", EmployeeId = "E1" }); // 5000 + 1250 = 6250
How this code fits together: (1) The context (Salary) receives an IAllowanceStrategy in its constructor (in production, resolve by company id from a factory or DI). (2) Salary holds only BaseAmount; it has no allowance-specific properties. (3) When the caller needs the total, it calls GetTotal(context). Salary computes allowance = _allowanceStrategy.CalculateAllowance(BaseAmount, context) and returns BaseAmount + allowance. (4) Each concrete strategy implements CalculateAllowance with one company’s rules; Salary never branches on company type. (5) To support a new company, add a new class that implements IAllowanceStrategy and register it; Salary and existing strategies are unchanged.
When to use Strategy: Use whenever you have multiple interchangeable algorithms (allowance rules, pricing, tax, validation, serialization) and you want to add or change them without changing the type that uses them. In .NET, register strategies in DI and inject a single strategy or a factory that selects by key (company id, tenant, feature flag).
Class structure explained: The diagram shows the main types. The subject (here OrderNotifier or OrderService) holds a list of observers and exposes Subscribe(observer) so that any component can register; it returns an IDisposable so callers can unsubscribe. When something meaningful happens (e.g. an order is created), the subject calls NotifyOrderCreated (or equivalent), which loops over all observers and calls their notification method (OnNext or OnOrderCreated). Concrete observers (WarehouseObserver, AnalyticsObserver) implement the same interface but do different work—one reserves stock, the other records analytics. The subject never depends on concrete observer types; it only knows the interface. That keeps the subject decoupled from who reacts and lets you add or remove observers without changing the subject.
Full working example: Order created notifications
1. Event payload and observer interface
namespace ObserverExample;
public record OrderCreatedEvent(Guid OrderId, string CustomerId, decimal Total, DateTime OccurredAt);
public interface IOrderObserver
{
void OnOrderCreated(OrderCreatedEvent evt);
}
2. Subject: order service that notifies observers
namespace ObserverExample;
public class OrderService
{
private readonly List<IOrderObserver> _observers = new();
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo) => _repo = repo;
public IDisposable Subscribe(IOrderObserver observer)
{
_observers.Add(observer);
return new Subscription(() => _observers.Remove(observer));
}
public async Task<Order> CreateOrderAsync(string customerId, decimal total, CancellationToken ct = default)
{
var order = new Order { Id = Guid.NewGuid(), CustomerId = customerId, Total = total };
await _repo.AddAsync(order, ct);
var evt = new OrderCreatedEvent(order.Id, order.CustomerId, order.Total, DateTime.UtcNow);
foreach (var observer in _observers.ToList())
observer.OnOrderCreated(evt);
return order;
}
private sealed class Subscription : IDisposable
{
private readonly Action _unsubscribe;
public Subscription(Action unsubscribe) => _unsubscribe = unsubscribe;
public void Dispose() => _unsubscribe?.Invoke();
}
}
3. Concrete observers
namespace ObserverExample;
public class WarehouseObserver : IOrderObserver
{
private readonly IWarehouseService _warehouse;
public WarehouseObserver(IWarehouseService warehouse) => _warehouse = warehouse;
public void OnOrderCreated(OrderCreatedEvent evt) =>
_warehouse.ReserveForOrderAsync(evt.OrderId, evt.Total).AsTask().GetAwaiter().GetResult();
}
public class AnalyticsObserver : IOrderObserver
{
private readonly IAnalyticsService _analytics;
public AnalyticsObserver(IAnalyticsService analytics) => _analytics = analytics;
public void OnOrderCreated(OrderCreatedEvent evt) =>
_analytics.TrackOrderCreated(evt.OrderId, evt.CustomerId, evt.Total);
}
4. Using .NET events (alternative)
public class OrderServiceWithEvents
{
public event EventHandler<OrderCreatedEvent>? OrderCreated;
public async Task<Order> CreateOrderAsync(string customerId, decimal total, CancellationToken ct = default)
{
var order = new Order { Id = Guid.NewGuid(), CustomerId = customerId, Total = total };
await _repo.AddAsync(order, ct);
OrderCreated?.Invoke(this, new OrderCreatedEvent(order.Id, order.CustomerId, order.Total, DateTime.UtcNow));
return order;
}
}
// Subscriber: orderService.OrderCreated += (_, e) => warehouse.ReserveForOrder(e.OrderId);
How this code fits together: (1) At startup or when a screen loads, one or more observers call Subscribe(observer) and receive an IDisposable. (2) When CreateOrderAsync runs, the order is saved to the repository, then an OrderCreatedEvent is built with the new order’s data. (3) The service iterates over _observers.ToList() and calls OnOrderCreated(evt) on each observer. (4) WarehouseObserver calls the warehouse service to reserve stock; AnalyticsObserver sends the event to analytics. (5) When a subscriber is done (e.g. view closed), it calls Dispose() on the subscription so it is removed from the list.
Explanation of the code: OrderCreatedEvent is a small, immutable DTO so all observers get the same data. Subscribe returns IDisposable so callers can unsubscribe and avoid leaks. Using _observers.ToList() before iterating avoids modification-during-iteration if an observer unsubscribes inside its handler. The inner Subscription class simply runs the unsubscribe action when Dispose is called. The OrderServiceWithEvents variant uses C# events instead of a list; subscribers use += and -= ; the subject invokes OrderCreated?.Invoke(this, evt).
Practical example: In an e-commerce checkout, when an order is placed you might notify: (1) Inventory to reserve items, (2) CRM to update the customer’s order history, (3) Analytics to record the sale, (4) Email service to send a confirmation. The order service stays unaware of these; it just raises one event. In a microservices setup, the same idea applies with a message broker: the order service publishes OrderCreated to a topic, and multiple subscribers (warehouse, analytics, notifications) consume it independently. At BAT we used this pattern for order-created: one event, multiple handlers (fulfillment, reporting, audit).
When to use Observer: Domain events, UI binding, audit trails, integration with external systems. Prefer IObservable<T> when you need composition, cancellation, or Rx-style operators; use C# events when the surface is small and local.
Class structure explained: The diagram shows the main types. A command (CreateOrderCommand) is a small object that carries the data for one request (e.g. customer ID and items). It implements a marker or generic ICommand so the dispatcher can route it. A handler (CreateOrderHandler) implements ICommandHandler<TCommand, TResponse>: it receives the command and performs the work (e.g. create order, save to repository). The sender (controller, queue consumer) does not call the handler directly; it sends the command to a dispatcher (or MediatR), which finds the right handler and invokes Handle(cmd). The sender and handler are decoupled: the sender only knows the command; the handler only knows how to process that command. That enables queuing, logging, undo (if the command stores enough to reverse), and CQRS.
Full working example: CQRS-style commands
1. Command and handler interfaces
namespace CommandExample;
public interface ICommand { }
public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand
{
Task<TResponse> Handle(TCommand command, CancellationToken ct = default);
}
2. Create order command and handler
namespace CommandExample;
public record CreateOrderCommand(string CustomerId, List<LineItemDto> Items) : ICommand;
public record LineItemDto(string ProductId, int Quantity, decimal UnitPrice);
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, OrderResult>
{
private readonly IOrderRepository _repo;
public CreateOrderHandler(IOrderRepository repo) => _repo = repo;
public async Task<OrderResult> Handle(CreateOrderCommand cmd, CancellationToken ct = default)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = cmd.CustomerId,
Lines = cmd.Items.Select(i => new OrderLine(i.ProductId, i.Quantity, i.UnitPrice)).ToList(),
Status = "Created"
};
await _repo.AddAsync(order, ct);
return new OrderResult(order.Id, order.Status);
}
}
public record OrderResult(Guid OrderId, string Status);
3. Undoable command (for undo/redo)
public interface IUndoableCommand : ICommand
{
void Undo();
}
public class SetOrderStatusCommand : IUndoableCommand
{
public Guid OrderId { get; init; }
public string NewStatus { get; init; } = "";
private string? _previousStatus;
public void Execute(OrderRepository repo)
{
var order = repo.Get(OrderId);
_previousStatus = order.Status;
order.Status = NewStatus;
}
public void Undo()
{
var order = _repo.Get(OrderId);
order.Status = _previousStatus!;
}
}
4. MediatR-style registration
// builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CreateOrderHandler>());
// Send: await _mediator.Send(new CreateOrderCommand(customerId, items));
How this code fits together: (1) The sender (e.g. API controller) creates a CreateOrderCommand(customerId, items) and sends it to a dispatcher (e.g. IMediator.Send(command)). (2) The dispatcher finds the handler registered for CreateOrderCommand (e.g. CreateOrderHandler) and resolves it from DI. (3) The handler’s Handle(cmd) runs: it builds an Order from the command, saves it via _repo.AddAsync, and returns OrderResult. (4) The sender gets back OrderResult without knowing the handler or repository. For undo, a command like SetOrderStatusCommand stores _previousStatus in Execute and restores it in Undo.
Explanation of the code: CreateOrderCommand is a record so it is immutable and easy to serialize (for queues or audit). The handler is generic: ICommandHandler<CreateOrderCommand, OrderResult> so one handler per command type. The handler receives IOrderRepository via constructor; the command itself has no behaviour—only data. IUndoableCommand adds Undo() so you can store commands in a stack and pop/undo when the user requests it.
Practical example: In an API you might have CreateOrderCommand, UpdateOrderCommand, CancelOrderCommand. Each has a handler; the controller only calls _mediator.Send(command). In a job queue, commands are serialized and enqueued; a worker deserializes and dispatches to the handler. For audit, you log every command (and optionally the result) so you can replay or debug. At BAT we used command/handler for CQRS: write operations were commands; handlers updated the domain and published events. Undo/redo in a UI is the same idea: each user action is a command; undo pops the stack and calls Undo().
When to use Command: CQRS, job queues, undo/redo, audit logs, API handlers. Keep commands small and handlers focused on one use case.
Class structure explained: The diagram shows the main types. The context (Order) holds the current state (IOrderState) and exposes operations like Submit(), Ship(), Cancel(). Instead of implementing logic itself, the context delegates to the current state: State.Submit(this), State.Ship(this). Each concrete state (DraftState, SubmittedState, ShippedState) implements the same interface but with different behaviour: in DraftState, Submit transitions to SubmittedState; Ship throws. In SubmittedState, Ship transitions to ShippedState. Transitions are done by the state calling order.SetState(new SubmittedState()) so the context never branches on state type—the state objects encapsulate both behaviour and transition logic.
Full working example: Order workflow
1. State interface and context
namespace StateExample;
public interface IOrderState
{
void Submit(Order order);
void Ship(Order order);
void Cancel(Order order);
string StatusName { get; }
}
public class Order
{
public Guid Id { get; set; }
public string CustomerId { get; set; } = "";
internal IOrderState State { get; private set; }
public Order() => State = new DraftState();
public string Status => State.StatusName;
public void SetState(IOrderState state) => State = state;
public void Submit() => State.Submit(this);
public void Ship() => State.Ship(this);
public void Cancel() => State.Cancel(this);
}
2. Concrete states
namespace StateExample;
public class DraftState : IOrderState
{
public string StatusName => "Draft";
public void Submit(Order order) => order.SetState(new SubmittedState());
public void Ship(Order order) => throw new InvalidOperationException("Cannot ship a draft order.");
public void Cancel(Order order) { /* already draft; no-op or remove */ }
}
public class SubmittedState : IOrderState
{
public string StatusName => "Submitted";
public void Submit(Order order) { /* no-op */ }
public void Ship(Order order) => order.SetState(new ShippedState());
public void Cancel(Order order) => order.SetState(new DraftState());
}
public class ShippedState : IOrderState
{
public string StatusName => "Shipped";
public void Submit(Order order) { }
public void Ship(Order order) { }
public void Cancel(Order order) => throw new InvalidOperationException("Cannot cancel a shipped order.");
}
Usage
var order = new Order { Id = Guid.NewGuid(), CustomerId = "C1" };
Console.WriteLine(order.Status); // Draft
order.Submit();
Console.WriteLine(order.Status); // Submitted
order.Ship();
Console.WriteLine(order.Status); // Shipped
How this code fits together: (1) The context (Order) starts with State = new DraftState(). (2) When the user calls order.Submit(), the context delegates to State.Submit(this). (3) DraftState.Submit(order) calls order.SetState(new SubmittedState()), so the context’s state changes. (4) Later, order.Ship() calls State.Ship(this); SubmittedState.Ship(order) transitions to ShippedState. (5) Invalid actions (e.g. Ship from DraftState) throw or no-op; each state owns its rules.
Explanation of the code: Order does not contain if (status == Draft); it only holds State and forwards calls. SetState is internal (or public) so state classes can transition the context. Each state class is small: one responsibility (e.g. “Draft behaviour”). StatusName on the interface lets the context expose the current state name without casting. New states (e.g. CancelledState) are added by creating a new class and wiring transitions; the context code does not change.
Practical example: In an order management system, an order moves Draft → Submitted → Shipped (or Draft → Cancelled). In a document workflow you might have Draft → InReview → Approved → Published. In a connection component you might have Disconnected → Connecting → Connected → Reconnecting. Each state knows what it allows and what the next state is. At BAT we used the State pattern for order lifecycle so adding a new status (e.g. “OnHold”) meant adding a new state class and updating transitions in the relevant states—no giant switch in the order entity.
When to use State: Workflows (order, document, approval), connection state, wizards. Keep transition logic inside state classes and the context thin.
Class structure explained: The diagram shows the main types. The abstract base class (DataImporter) defines the template method (ImportAsync) that fixes the algorithm: validate → load → transform → save. It calls abstract methods (LoadAsync, SaveAsync) that subclasses must implement, and hook methods (ValidateAsync, TransformAsync) that are virtual with empty or default implementations so subclasses can optionally override them. Concrete subclasses (CsvImporter, JsonImporter) implement the abstract steps (e.g. LoadAsync reads from file, SaveAsync writes to repository) and may override hooks (e.g. CsvImporter overrides TransformAsync to parse CSV to JSON). The base class never knows the concrete type; it only calls the abstract and virtual methods. The flow is fixed; only the steps vary.
Full working example: ETL pipeline
1. Abstract base with template and hooks
namespace TemplateMethodExample;
public abstract class DataImporter
{
public async Task ImportAsync(CancellationToken ct = default)
{
await ValidateAsync(ct);
var raw = await LoadAsync(ct);
var transformed = await TransformAsync(raw, ct);
await SaveAsync(transformed, ct);
}
protected virtual Task ValidateAsync(CancellationToken ct) => Task.CompletedTask; // hook
protected abstract Task<byte[]> LoadAsync(CancellationToken ct);
protected virtual Task<byte[]> TransformAsync(byte[] raw, CancellationToken ct) => Task.FromResult(raw); // hook
protected abstract Task SaveAsync(byte[] data, CancellationToken ct);
}
2. Concrete importer: CSV
namespace TemplateMethodExample;
public class CsvImporter : DataImporter
{
private readonly string _path;
private readonly IRepository _repo;
public CsvImporter(string path, IRepository repo) { _path = path; _repo = repo; }
protected override Task<byte[]> LoadAsync(CancellationToken ct) =>
File.ReadAllBytesAsync(_path, ct);
protected override Task<byte[]> TransformAsync(byte[] raw, CancellationToken ct)
{
var text = Encoding.UTF8.GetString(raw);
var rows = text.Split('
').Select(line => line.Split(','));
var json = JsonSerializer.SerializeToUtf8Bytes(rows);
return Task.FromResult(json);
}
protected override Task SaveAsync(byte[] data, CancellationToken ct) =>
_repo.SaveAsync("csv_import", data, ct);
}
3. Concrete importer: JSON (reuses hook)
public class JsonImporter : DataImporter
{
private readonly string _path;
private readonly IRepository _repo;
public JsonImporter(string path, IRepository repo) { _path = path; _repo = repo; }
protected override Task<byte[]> LoadAsync(CancellationToken ct) =>
File.ReadAllBytesAsync(_path, ct);
protected override Task SaveAsync(byte[] data, CancellationToken ct) =>
_repo.SaveAsync("json_import", data, ct);
// TransformAsync uses default (identity) from base
}
How this code fits together: (1) The caller invokes importer.ImportAsync() on a concrete instance (e.g. CsvImporter). (2) The template method in the base runs: first ValidateAsync (hook; default no-op), then LoadAsync (abstract; CsvImporter reads the file), then TransformAsync (hook; CsvImporter parses CSV to JSON, JsonImporter uses default identity), then SaveAsync (abstract; both write to repository). (3) Each step is polymorphic: the base calls this.LoadAsync() and the runtime invokes the overriding method. (4) New importers (e.g. ExcelImporter) add a new subclass and implement only LoadAsync and SaveAsync (and optionally TransformAsync).
Explanation of the code: ImportAsync is the template method: it defines the skeleton and calls the steps in order. Abstract methods (LoadAsync, SaveAsync) force subclasses to fill in the variable parts. Hook methods (ValidateAsync, TransformAsync) have default implementations (empty or identity) so subclasses can override only when needed—JsonImporter does not override TransformAsync. Using protected keeps the steps invisible to callers; only ImportAsync is public.
Practical example: In a data pipeline you might have CSV → DB, Excel → DB, API → DB: same flow (validate, load, transform, save), different sources and transforms. In report generation the template might be fetch data → apply filters → render → export; subclasses vary the source (SQL, API) and format (PDF, Excel). In tests you might have a FakeImporter that overrides LoadAsync and SaveAsync to use in-memory data. At BAT we used Template Method for ETL jobs so adding a new file type (e.g. Parquet) meant one new subclass; the orchestration and error handling stayed in the base.
When to use Template Method: ETL, report generation, data migration. Prefer composition (Strategy) if you need to swap entire algorithms at runtime; use Template Method when the algorithm structure is fixed and only steps vary.
Class structure explained: The handler interface (IRequestHandler<T>) defines SetNext(next) (to build the chain) and HandleAsync(request) (to process or pass along). An abstract base (RequestHandlerBase<T>) holds _next and provides CallNextAsync(request) so concrete handlers can pass the request to the next link. Concrete handlers (ValidationHandler, AuthHandler, LoggingHandler) implement HandleAsync: they do their work (e.g. validate, check auth, log), then call CallNextAsync to pass the request down the chain, or return false to stop. The client builds the chain (validation.SetNext(auth).SetNext(logging)) and sends the request to the first handler only. Order of handlers is determined by how you link them, not by the sender.
Full working example: Create-order pipeline (validate → auth → log)
Scenario: A CreateOrderRequest must pass through validation (customer ID and item count valid), then auth (caller is authenticated), then logging (record the request). If validation or auth fails, the chain stops and the request is not logged or processed. Each step is a separate handler; the API only calls the first handler.
1. Handler interface and base
namespace ChainOfResponsibilityExample;
public interface IRequestHandler<T>
{
IRequestHandler<T> SetNext(IRequestHandler<T> next);
Task<bool> HandleAsync(T request, CancellationToken ct = default);
}
public abstract class RequestHandlerBase<T> : IRequestHandler<T>
{
private IRequestHandler<T>? _next;
public IRequestHandler<T> SetNext(IRequestHandler<T> next) { _next = next; return next; }
protected async Task<bool> CallNextAsync(T request, CancellationToken ct) =>
_next != null ? await _next.HandleAsync(request, ct) : true;
public abstract Task<bool> HandleAsync(T request, CancellationToken ct = default);
}
public class CreateOrderRequest { public string CustomerId { get; set; } = ""; public int ItemCount { get; set; } }
2. Concrete handlers
namespace ChainOfResponsibilityExample;
public class ValidationHandler : RequestHandlerBase<CreateOrderRequest>
{
public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.CustomerId)) return false;
if (request.ItemCount <= 0) return false;
return await CallNextAsync(request, ct);
}
}
public class AuthHandler : RequestHandlerBase<CreateOrderRequest>
{
public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
{
// In production: resolve current user and check permissions
// if (!_authService.IsAuthenticated()) return false;
return await CallNextAsync(request, ct);
}
}
public class LoggingHandler : RequestHandlerBase<CreateOrderRequest>
{
public override async Task<bool> HandleAsync(CreateOrderRequest request, CancellationToken ct)
{
// Log request (e.g. _logger.LogInfo("CreateOrder {CustomerId}", request.CustomerId));
return await CallNextAsync(request, ct);
}
}
3. Building and running the chain
// Build chain: validation → auth → logging
var validation = new ValidationHandler();
var auth = new AuthHandler();
var logging = new LoggingHandler();
validation.SetNext(auth).SetNext(logging);
var request = new CreateOrderRequest { CustomerId = "C1", ItemCount = 2 };
bool success = await validation.HandleAsync(request);
How this code fits together: (1) The client builds the chain by calling validation.SetNext(auth) and auth.SetNext(logging); SetNext returns the next handler so you can chain calls. (2) The client sends the request only to the first handler: validation.HandleAsync(request). (3) ValidationHandler checks CustomerId and ItemCount; if invalid it returns false and the chain stops. If valid it calls CallNextAsync(request), which invokes auth.HandleAsync(request). (4) AuthHandler performs its check and calls CallNextAsync; LoggingHandler logs and calls CallNextAsync; with no further handler, CallNextAsync returns true. (5) The client gets a single bool indicating success or failure; it does not know which handler failed or how many ran. To add a new step (e.g. rate limit), add a new handler class and insert it in the chain; no other code changes.
When to use Chain of Responsibility: Use when a request must pass through multiple handlers in a fixed or configurable order (middleware, validation → auth → logging, or approval workflows) and you want to add or reorder handlers without changing the sender. Avoid when you have a single handler or a simple linear flow that does not need this flexibility.
Class structure explained: The mediator (IChatMediator / ChatRoom) holds references to all colleagues (participants). Colleagues hold only a reference to the mediator, not to each other. When a colleague needs to communicate (e.g. send a message), it calls mediator.Send(message, this). The mediator decides how to route that—e.g. broadcast to all other participants. So the flow is: colleague → mediator → other colleagues. That keeps colleagues decoupled and lets you change routing or add new participant types in one place.
Full working example: Chat room
1. Mediator interface and concrete mediator
namespace MediatorExample;
public interface IChatMediator
{
void Send(string message, Participant from);
void Register(Participant participant);
}
public class ChatRoom : IChatMediator
{
private readonly List<Participant> _participants = new();
public void Register(Participant p) { _participants.Add(p); }
public void Send(string message, Participant from)
{
foreach (var p in _participants.Where(x => x != from))
p.Receive(from.Name, message);
}
}
2. Colleague: Participant
namespace MediatorExample;
public class Participant
{
public string Name { get; }
private readonly IChatMediator _mediator;
public Participant(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
_mediator.Register(this);
}
public void Send(string message) => _mediator.Send(message, this);
public void Receive(string from, string message) =>
Console.WriteLine($"{from} -> {Name}: {message}");
}
3. Usage
var room = new ChatRoom();
var alice = new Participant("Alice", room);
var bob = new Participant("Bob", room);
var carol = new Participant("Carol", room);
alice.Send("Hi everyone"); // Bob and Carol receive. Alice does not.
bob.Send("Hello Alice"); // Alice and Carol receive.
How this code fits together: (1) You create one mediator (ChatRoom) and then create participants with that mediator; each participant registers itself in the constructor. (2) When a participant calls Send(message), it delegates to _mediator.Send(message, this). (3) The mediator iterates over all registered participants (excluding the sender) and calls Receive(from, message) on each. Participants never call each other directly; all coordination goes through the mediator.
When to use Mediator: Use when you have many objects that need to coordinate (chat users, wizard steps, form fields that depend on each other) and you want to avoid a web of direct references. Avoid when you only have two or three objects or when the interaction is trivial; the mediator should not become a god object with too much logic.
Class structure explained: The originator (Editor) is the object whose state we snapshot. It exposes Save() (or CreateMemento()) which returns a memento—an opaque object holding a copy of the originator’s state. The originator also exposes Restore(memento) to restore its state from a memento. The caretaker (e.g. UndoStack) holds a list of mementos and, on undo, calls originator.Restore(memento). The caretaker never reads or changes the memento’s contents; only the originator knows how to create and interpret mementos. That keeps state encapsulation and makes undo/redo safe.
Full working example: Undoable editor with caretaker
1. Memento and originator (Editor)
namespace MementoExample;
public class EditorMemento
{
public string Text { get; }
public EditorMemento(string text) => Text = text;
}
public class Editor
{
public string Text { get; set; } = "";
public EditorMemento Save() => new EditorMemento(Text);
public void Restore(EditorMemento m) => Text = m.Text;
}
2. Caretaker: undo stack
namespace MementoExample;
public class UndoStack
{
private readonly Editor _editor;
private readonly Stack<EditorMemento> _history = new();
public UndoStack(Editor editor) => _editor = editor;
public void Snapshot()
{
_history.Push(_editor.Save());
}
public void Undo()
{
if (_history.Count == 0) return;
_history.Pop(); // discard current
if (_history.Count == 0) return;
var previous = _history.Pop();
_editor.Restore(previous);
_history.Push(previous); // keep it for next undo
}
}
3. Usage
var editor = new Editor();
var undo = new UndoStack(editor);
editor.Text = "Hello";
undo.Snapshot();
editor.Text = "Hello world";
undo.Snapshot();
editor.Text = "Hello world!";
undo.Snapshot();
undo.Undo(); // back to "Hello world"
undo.Undo(); // back to "Hello"
How this code fits together: (1) The originator (Editor) holds the mutable state (Text) and exposes Save() and Restore(m). Save() returns a memento that only the editor knows how to create and interpret. (2) The caretaker (UndoStack) holds a stack of mementos. Before each user edit you call Snapshot(), which pushes editor.Save() onto the stack. (3) On undo, the caretaker pops the current memento (optional), restores the previous one via editor.Restore(previous), and does not read the memento’s internals. The editor remains the only object that knows the structure of the memento.
When to use Memento: Use when you need undo/redo, checkpoints, or save/restore and want to keep the object’s state encapsulated (callers cannot read or tamper with the snapshot). Avoid when state is trivial or when you need to expose state for other reasons; keep mementos small and immutable.
Class structure explained: The aggregate (IEnumerable<T> / RangeEnumerable) represents the collection or sequence. It exposes GetEnumerator(), which returns an iterator (IEnumerator<T> / RangeEnumerator). The iterator has MoveNext() (advance and return whether there is a current element) and Current (the element at the current position). The client calls GetEnumerator(), then repeatedly MoveNext() and Current (or uses foreach), and never sees the aggregate’s internal storage. Different aggregates can use different internal structures (array, tree, lazy generation) as long as they return an iterator that obeys the same contract.
Full working example: Custom range collection
1. Aggregate: RangeEnumerable
namespace IteratorExample;
public class RangeEnumerable : IEnumerable<int>
{
private readonly int _start, _count;
public RangeEnumerable(int start, int count) { _start = start; _count = count; }
public IEnumerator<int> GetEnumerator() => new RangeEnumerator(_start, _count);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
2. Iterator: RangeEnumerator
namespace IteratorExample;
public class RangeEnumerator : IEnumerator<int>
{
private readonly int _start, _count;
private int _current, _index;
public RangeEnumerator(int start, int count) { _start = start; _count = count; _current = start - 1; _index = -1; }
public bool MoveNext() { _index++; if (_index >= _count) return false; _current = _start + _index; return true; }
public int Current => _current;
object System.Collections.IEnumerator.Current => Current;
public void Reset() { _index = -1; _current = _start - 1; }
public void Dispose() { }
}
3. Usage
var range = new RangeEnumerable(10, 3); // 10, 11, 12
foreach (var n in range)
Console.WriteLine(n); // 10, 11, 12
How this code fits together: (1) The aggregate (RangeEnumerable) holds the logical range (_start, _count) and does not expose that as a list or array. (2) GetEnumerator() creates a new iterator (RangeEnumerator) that holds the same range and maintains a current position (_index). (3) The client (or foreach) calls MoveNext() to advance; the iterator computes the next value (_start + _index) and returns it via Current. The client never sees _start or _count; it only sees a sequence of integers. In .NET you can implement the same idea more concisely with yield return inside GetEnumerator().
When to use Iterator: Use when you have an aggregate (custom collection, tree, stream, lazy sequence) and you want callers to traverse it without exposing its internal structure. Use IEnumerable<T> and IEnumerator<T> (or yield return) so your type works with foreach and LINQ. Avoid when a simple array or List<T> is enough and you don’t need to hide representation.
Class structure explained: Each expression type implements IExpression with an Eval() (or Interpret()) method. Terminal expressions (e.g. NumberExpression) return a value directly. Non-terminal (composite) expressions (e.g. AddExpression, SubtractExpression) hold child expressions and combine their Eval() results. A sentence in the language is represented as a tree of these objects; evaluating the root Eval() traverses the tree and computes the result. A separate parser (not shown) would turn a string like "2 + 3" into this tree.
Full working example: Simple expression evaluator
1. Expression interface and terminal expression
namespace InterpreterExample;
public interface IExpression { decimal Eval(); }
public class NumberExpression : IExpression
{
private readonly decimal _value;
public NumberExpression(decimal value) => _value = value;
public decimal Eval() => _value;
}
2. Binary (composite) expressions
namespace InterpreterExample;
public class AddExpression : IExpression
{
private readonly IExpression _left, _right;
public AddExpression(IExpression left, IExpression right) { _left = left; _right = right; }
public decimal Eval() => _left.Eval() + _right.Eval();
}
public class SubtractExpression : IExpression
{
private readonly IExpression _left, _right;
public SubtractExpression(IExpression left, IExpression right) { _left = left; _right = right; }
public decimal Eval() => _left.Eval() - _right.Eval();
}
3. Building a tree and evaluating (usage)
// Represents: (10 + 3) - 2 => 13 - 2 => 11
IExpression expr = new SubtractExpression(
new AddExpression(new NumberExpression(10), new NumberExpression(3)),
new NumberExpression(2));
decimal result = expr.Eval(); // 11
How this code fits together: (1) Each grammar rule is a class: numbers are NumberExpression, addition is AddExpression, subtraction is SubtractExpression. (2) A “sentence” is a tree: the root might be SubtractExpression(left, right), and left might be AddExpression(10, 3), etc. (3) Calling Eval() on the root triggers a recursive traversal: composite nodes call Eval() on their children and combine results; terminal nodes return their value. No parser is shown here—in practice a parser would turn a string like "10 + 3 - 2" into this tree. The pattern is: grammar as classes, sentence as tree, interpretation by traversing the tree.
When to use Interpreter: Use when you have a small, well-defined grammar (expressions, simple DSL, rules) and you want to represent it as a tree of objects and interpret by traversing that tree. Avoid for large or ambiguous grammars; use a parser generator and possibly a separate AST plus Visitor for complex languages.
Class structure explained: Each element type (e.g. NumberExpr, AddExpr) implements a common interface with Accept(visitor). When Accept(visitor) is called, the element calls the visitor’s method for its type—e.g. visitor.VisitNumber(this) or visitor.VisitAdd(this)—so the visitor sees the concrete type without the client doing a cast. The visitor interface has one VisitX method per element type. A concrete visitor (e.g. EvalVisitor, PrintVisitor) implements the same operation (evaluate or print) for each type. To add a new operation, add a new visitor class; element classes stay unchanged. The trade-off: adding a new element type requires adding a method to the visitor interface and all existing visitors.
Full working example: AST with Eval and Print visitors
1. Element interface and node types
namespace VisitorExample;
public interface IExpr { T Accept<T>(IExprVisitor<T> v); }
public interface IExprVisitor<T> { T VisitNumber(NumberExpr e); T VisitAdd(AddExpr e); }
public class NumberExpr : IExpr
{
public int Value { get; set; }
public T Accept<T>(IExprVisitor<T> v) => v.VisitNumber(this);
}
public class AddExpr : IExpr
{
public IExpr Left { get; set; }
public IExpr Right { get; set; }
public T Accept<T>(IExprVisitor<T> v) => v.VisitAdd(this);
}
2. Eval visitor
namespace VisitorExample;
public class EvalVisitor : IExprVisitor<int>
{
public int VisitNumber(NumberExpr e) => e.Value;
public int VisitAdd(AddExpr e) => e.Left.Accept(this) + e.Right.Accept(this);
}
3. Print visitor (second operation)
namespace VisitorExample;
public class PrintVisitor : IExprVisitor<string>
{
public string VisitNumber(NumberExpr e) => e.Value.ToString();
public string VisitAdd(AddExpr e) => $"({e.Left.Accept(this)} + {e.Right.Accept(this)})";
}
4. Usage
IExpr expr = new AddExpr { Left = new NumberExpr { Value = 2 }, Right = new NumberExpr { Value = 3 } };
var evalVisitor = new EvalVisitor();
var printVisitor = new PrintVisitor();
int result = expr.Accept(evalVisitor); // 5
string text = expr.Accept(printVisitor); // "(2 + 3)"
How this code fits together: (1) Each element (NumberExpr, AddExpr) has Accept(visitor) which calls the visitor’s VisitNumber(this) or VisitAdd(this)—so the visitor receives the concrete type (double dispatch). (2) EvalVisitor implements evaluation: VisitNumber returns the value; VisitAdd returns Left.Accept(this) + Right.Accept(this), so the tree is traversed and values are combined. (3) PrintVisitor implements formatting: each VisitX returns a string; VisitAdd builds "(left + right)" by calling Accept(this) on children. (4) To add another operation (e.g. serialize), add a new visitor class; no change to NumberExpr or AddExpr.
When to use Visitor: Use when you have a stable set of element types and a growing set of operations (evaluate, print, serialize, type-check, etc.) and you want to add operations without changing element classes. Avoid when you frequently add new element types—each new type forces a new method on the visitor interface and on all existing visitors.
