Mastering Behavioral Design Patterns in .NET: Streamlining Object Communication

In software development, objects frequently need to communicate while maintaining flexibility and minimizing dependenciesBehavioral design patterns in .NET provide structured solutions to streamline these interactions, ensuring that responsibilities are well-defined and that code remains maintainable and scalable. By leveraging these patterns, developers can enhance code readability, improve reusability, and facilitate easier modifications without disrupting existing functionality. This guide explores essential behavioral patterns, detailing their purpose, practical use cases, alignment with SOLID principles, and step-by-step code implementations to demonstrate their real-world impact.

  • Chain of Responsibility: Passes a request along a chain of handlers until one of them handles it. Example: Middleware in ASP.NET Core processes HTTP requests sequentially.
  • Command: Encapsulates a request as an object, allowing parameterization, queuing, and logging. Example: Implementing Undo/Redo operations in a text editor.
  • Interpreter: Defines a grammar and an interpreter for a language. Example: Parsing mathematical expressions or SQL-like query languages.
  • Iterator: Provides a way to access elements of a collection without exposing its underlying structure. Example.NET IEnumerable<T> and IEnumerator<T>.
  • Mediator: Centralizes communication between objects to reduce dependencies. Example: Implementing a message broker in a CQRS pattern.
  • Memento: Captures an object’s state so it can be restored later. Example: Saving game progress or undo functionality.
  • Observer: Defines a dependency between objects so that when one changes, all dependents are notified. Example.NET Events & Delegates, Reactive Extensions (Rx).
  • State: Allows an object to alter its behavior when its internal state changes. Example: Workflow management systems, where a document transitions through different approval states.
  • Strategy: Defines a family of algorithms and allows them to be swapped at runtime. Example: Sorting strategies (List<T>.Sort(IComparer<T>)).
  • Template Method: Defines the skeleton of an algorithm and allows subclasses to override specific steps. Example: Base class in an ASP.NET MVC Controller with predefined request lifecycle methods.
  • Visitor: Adds new operations to existing object structures without modifying them. Example: Applying operations to elements in a syntax tree (e.g., Roslyn compiler in .NET).

Chain of Responsibility Pattern

Passes a request sequentially through a chain of handlers until one processes it, promoting flexibility and decoupling between sender and receiver. This pattern is useful when multiple handlers could process a request, but the exact handler isn’t known in advance, making it ideal for loggingauthenticationvalidation, and event processing. It enhances maintainability by allowing new handlers to be added or modified without altering existing code.

Common Use Cases

  • Logging Systems: Different loggers (e.g., console, file, database) process log messages in sequence until one handles it.
  • Event Handling: GUI frameworks process user events through a chain of handlers to determine the appropriate response.

SOLID Principle

  • Single Responsibility Principle: Each handler in the chain has a single responsibility, processing specific requests and passing others along.

Implementing Chain of Responsibility: Support Ticket Processing System

The Chain of Responsibility pattern is ideal for implementing a support ticket processing system where tickets are escalated through different support levels until resolved.

1️⃣Define an abstract handler with a method to process requests and a reference to the next

This code defines an abstract class SupportHandler, which is a part of the Chain of Responsibility pattern.

public abstract class SupportHandler
{
protected SupportHandler _nextHandler;

public void SetNext(SupportHandler nextHandler)
{
_nextHandler = nextHandler;
}

public abstract void HandleRequest(SupportTicket ticket);
}

The SupportHandler class is setting up a chain of handlers for processing support tickets. Each handler in the chain can either handle the ticket or delegate it to the next handler, enabling flexible handling based on the type of ticket or the capabilities of each handler.

2️⃣Create concrete handlers representing different support levels

This code implements specific handlers for the Chain of Responsibility pattern, where each level of support (Level One, Level Two, and Level Three) processes a support ticket based on the complexity of the issue.

public class LevelOneSupport : SupportHandler
{
public override void HandleRequest(SupportTicket ticket)
{
if (ticket.IssueComplexity <= 1)
{
Console.WriteLine("Level One resolved the issue.");
}
else
{
_nextHandler?.HandleRequest(ticket);
}
}
}

public class LevelTwoSupport : SupportHandler
{
public override void HandleRequest(SupportTicket ticket)
{
if (ticket.IssueComplexity <= 2)
{
Console.WriteLine("Level Two resolved the issue.");
}
else
{
_nextHandler?.HandleRequest(ticket);
}
}
}

public class LevelThreeSupport : SupportHandler
{
public override void HandleRequest(SupportTicket ticket)
{
Console.WriteLine("Level Three resolved the issue.");
}
}
  • These classes represent different support levels in a help desk system.
  • Level One handles simple issues (complexity <= 1), Level Two handles moderately complex issues (complexity <= 2), and Level Three handles the most complex issues (everything else).
  • If a ticket isn’t within the scope of a level, it is passed to the next handler in the chain.
  • This structure allows for easily adding or modifying support levels without changing the existing logic, thanks to the Chain of Responsibility pattern.

3️⃣Create a SupportTicket class to represent support requests

This code defines a class SupportTicket, which represents a support ticket in the system.

public class SupportTicket
{
public int IssueComplexity { get; set; }

public SupportTicket(int complexity)
{
IssueComplexity = complexity;
}
}

The SupportTicket class encapsulates a support ticket, storing its complexity level (IssueComplexity) to determine which support handler should process the ticket. It provides a constructor to initialize the ticket with a specific complexity value. This class serves as the data object passed through the chain of handlers in the Chain of Responsibility pattern.

4️⃣Configure the chain and process a ticket

This code demonstrates how the Chain of Responsibility pattern is used to process a support ticket through multiple levels of support.

var levelOne = new LevelOneSupport();
var levelTwo = new LevelTwoSupport();
var levelThree = new LevelThreeSupport();

levelOne.SetNext(levelTwo);
levelTwo.SetNext(levelThree);

var ticket = new SupportTicket(complexity: 2);
levelOne.HandleRequest(ticket);

This code demonstrates how a support ticket with complexity 2 is processed through a chain of handlers (LevelOneSupportLevelTwoSupport, and LevelThreeSupport). Each handler checks if it can handle the ticket based on its complexity, and if not, it forwards the ticket to the next handler in the chain.

5️⃣Output

In this example, LevelTwoSupport resolves the ticket because its complexity matches the ticket’s issue.

Level Two resolved the issue.

Command Pattern

The Command pattern encapsulates request as an object, allowing for greater flexibility in how requests are handled. By converting a request into an object, this pattern separates the sender of the request from the receiver, enabling features like queuingundo/redo operations, logging, and transactional behavior. The pattern also supports the decoupling of objects, making it easier to add, modify, or extend the operations without affecting existing code. As a developer, this pattern is particularly useful in scenarios where requests need to be parameterizedstored, or executed at different times or in a specific order, such as in task scheduling systemsmenu systems, or event-driven architectures.

Common Use Cases

  • Undo/Redo Functionality: Encapsulating actions as command objects enables easy implementation of undo and redo operations.
  • Task Scheduling: Commands can be queued and executed at a later time, facilitating task scheduling.

SOLID Principle

  • Open/Closed Principle: New commands can be added without modifying existing code, promoting extensibility.

Implementing the Command Pattern: Text Editor with Undo and Redo

The Command Pattern is particularly useful for implementing undo and redo functionality in a text editor because it encapsulates requests as objects. This allows for operations to be treated as discretestandalone commands that can be storedexecutedundone, or redone.

1️⃣Define an ICommand interface with an Execute and Unexecute method

This code defines a basic Command Interface for the Command Pattern.

public interface ICommand
{
void Execute();
void Unexecute();
}

This interface declares two methods, Execute and Unexecute, which are meant to be implemented by concrete command classes.

2️⃣Create a TextEditor class to represent the receiver of the commands

This code defines a TextEditor class with functionality to manipulate its internal text.

public class TextEditor
{
public string Text { get; private set; } = string.Empty;

public void AppendText(string newText)
{
Text += newText;
}

public void RemoveText(int length)
{
if (length <= Text.Length)
{
Text = Text.Substring(0, Text.Length - length);
}
}
}

This TextEditor class will serve as the receiver in a command object. The command objects (implementing the ICommand interface) will invoke actions like append or remove on the TextEditor.

3️⃣Implement concrete command classes for appending and removing text

This code defines a AppendTextCommand class that implements the ICommand interface. It encapsulates the logic for appending text to a TextEditor and undoing that action.

public class AppendTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _textToAppend;

public AppendTextCommand(TextEditor editor, string text)
{
_editor = editor;
_textToAppend = text;
}

public void Execute()
{
_editor.AppendText(_textToAppend);
}

public void Unexecute()
{
_editor.RemoveText(_textToAppend.Length);
}
}

This class is a concrete command that encapsulates the action of appending text and its undo counterpart (removing the text).

4️⃣Create an Invoker class to manage command execution and history

This code defines a CommandManager class that manages the undo and redo functionality using the Command Pattern. It keeps track of the executed commands and allows users to undo and redo their actions.

public class CommandManager
{
private readonly Stack<ICommand> _undoStack = new();
private readonly Stack<ICommand> _redoStack = new();

public void ExecuteCommand(ICommand command)
{
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
}

public void Undo()
{
if (_undoStack.Any())
{
var command = _undoStack.Pop();
command.Unexecute();
_redoStack.Push(command);
}
}

public void Redo()
{
if (_redoStack.Any())
{

::contentReference[oaicite:0]{index=0}
  • CommandManager manages a stack-based undo/redo system by keeping track of executed commands (_undoStack) and undone commands (_redoStack).
  • ExecuteCommand performs actions and updates the stacks.
  • Undo undoes the most recent action and moves it to the redo stack.
  • Redo re-applies an undone action and moves it back to the undo stack.

This pattern allows for a very flexible and powerful way of managing user actions in applications like text editors or drawing apps, where undo/redo functionality is required.

Interpreter Pattern

The Interpreter Pattern is a design pattern used to help a program understand and process language or expressions. It works by defining rules for a language and then creating an interpreter to read and understand sentences written in that language. This is especially useful when working with custom languages, like query languages or domain-specific languages (DSLs), and is often used in systems like rule engines or parsers.

The main advantage of this pattern is that it makes it easy to add new rules or change the language without breaking the rest of your system. It separates the logic for interpreting these expressions from the main application, which keeps the code clean and easy to update over time. Whether you’re building a custom parser or a system that processes user-defined rules, the Interpreter Pattern helps you manage and extend your code with ease.

Common Use Cases

  • Parsing Mathematical Expressions: Evaluating arithmetic expressions dynamically.
  • SQL-like Query Languages: Interpreting custom query languages within applications.

SOLID Principle

  • Single Responsibility Principle: Each class in the grammar has a single responsibility related to interpreting a specific part of the language.

Implementing the Interpreter Pattern: Interpreter for Arithmetic Expressions

Let’s say you are tasked with evaluating arithmetic expressions provided by users. Instead of hardcoding the rules for each type of operation in one place, the Interpreter pattern allows you to define each operation as its own object. When the user enters an expression like 3 + (2 * 4), your interpreter can break it down, evaluate the multiplication first, and then add the result to 3.

This approach makes it easier to handle more complex expressions, and as new operations are added (for example, adding the ability to handle powers or modulus), you can extend the interpreter without altering the entire logic.

1️⃣Define an abstract expression interface

This interface is the foundation of the Interpreter Pattern. It defines a common methodInterpret(), which all concrete classes (representing different types of expressions) must implement. The Interpret() method returns an integer value, which is the result of interpreting the expression.

public interface IExpression
{
int Interpret();
}

In the Interpreter Pattern, you often build a tree structure of expressions. Each node in the tree is an instance of IExpression, and the tree itself represents the abstract syntax tree (AST) of the expression you’re interpreting. By using the Interpret() method, you recursively evaluate the expression, processing each part (such as the left and right operands) to compute the final result.

2️⃣Create terminal expressions for numbers

The class NumberExpression is a concrete implementation of the IExpression interface, representing a literal number (i.e., a constant value in an arithmetic expression).

public class NumberExpression : IExpression
{
private readonly int _number;

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

public int Interpret()
{
return _number;
}
}

The NumberExpression class encapsulates literal number as an expression. In arithmetic expressions, numbers are fundamental building blocks, and they need to be represented as objects that can be interpreted within the context of an expression tree. The NumberExpression class provides that representation.

3️⃣Create non-terminal expressions for addition and subtraction

These two classes, AdditionExpression and SubtractionExpression, represent operation expressions (specifically addition and subtraction) in the context of the Interpreter Pattern. They allow you to perform arithmetic operations between two sub-expressions.

public class AdditionExpression : IExpression
{
private readonly IExpression _leftExpression;
private readonly IExpression _rightExpression;

public AdditionExpression(IExpression left, IExpression right)
{
_leftExpression = left;
_rightExpression = right;
}

public int Interpret()
{
return _leftExpression.Interpret() + _rightExpression.Interpret();
}
}

public class SubtractionExpression : IExpression
{
private readonly IExpression _leftExpression;
private readonly IExpression _rightExpression;

public SubtractionExpression(IExpression left, IExpression right)
{
_leftExpression = left;
_rightExpression = right;
}

public int Interpret()
{
return _leftExpression.Interpret() - _rightExpression.Interpret();
}
}

These classes encapsulate specific operations (addition and subtraction) as expression objects. They follow the Interpreter Pattern and provide the mechanism to interpret and evaluate arithmetic expressions by breaking them down into sub-expressions (such as addition and subtraction).

4️⃣Build and interpret an expression

The code represents the arithmetic expression (5 + 10) — 3, which involves an addition and a subtraction operation. The goal of the code is to evaluate the result of this expression using the Interpreter Pattern.

// Represents the expression (5 + 10) - 3
IExpression expression = new SubtractionExpression(
new AdditionExpression(
new NumberExpression(5),
new NumberExpression(10)
),
new NumberExpression(3)
);

int result = expression.Interpret();
Console.WriteLine(result); // Output: 12
  • The expression (5 + 10) - 3 is being represented as an expression tree, where each node is an operation (AdditionExpressionSubtractionExpression) or a value (NumberExpression).
  • The root of the tree is a SubtractionExpression, which represents the subtraction operation: (5 + 10) - 3.
  • The left operand of the subtraction operation is another AdditionExpression, which represents the addition operation: 5 + 10.
  • The right operand of the subtraction operation is a NumberExpression with the value 3

5️⃣Output

(5 + 10) — 3 => Addition: 5 + 10 = 15 => Subtraction: 15–3 = 12

**Interpreting the Expression**:
- The interpreter evaluates this expression recursively by invoking `Interpret()` on the root expression (`SubtractionExpression`).
- The `Interpret()` method of `SubtractionExpression` calls the `Interpret()` method of its left operand (`AdditionExpression`) and its right operand (`NumberExpression(3)`).
- The left operand (`AdditionExpression`) itself calls `Interpret()` on its operands, which are `NumberExpression(5)` and `NumberExpression(10)`.
- Each `NumberExpression` simply returns the number it holds when `Interpret()` is called.

**Steps of Evaluation**:
- `NumberExpression(5)` returns `5`.
- `NumberExpression(10)` returns `10`.
- `AdditionExpression` then adds `5` and `10`, returning `15`.
- `NumberExpression(3)` returns `3`.
- Finally, `SubtractionExpression` subtracts `3` from `15`, returning `12`.

**Output**:
- The result of the expression `(5 + 10) - 3` is `12`.
- The `Console.WriteLine(result)` prints `12` to the console.

This example demonstrates how the Interpreter Pattern can be used to represent arithmetic expressions and evaluate them using an expression tree. The recursive and modular structure of the pattern provides a flexible and extensible way to interpret and compute complex expressions. This design makes it easy to add new operations, handle nested expressions, and evaluate them efficiently.

Iterator Pattern

The Iterator Pattern provides a way to access elements of a collection sequentially without exposing its internal structure. This pattern is useful when you need to traverse a collection but want to keep its implementation details hidden. As a developer, it allows you to iterate through different types of collections (e.g., arrays, lists, trees) in a consistent way, without worrying about how they are structured internally.

Common Use Cases

  • .NET IEnumerable<T> and IEnumerator<T>: Standard interfaces for collection iteration.
  • Tree Traversal: Iterating through nodes in a tree structure.

SOLID Principle

  • Single Responsibility Principle: Separates the responsibility of traversing a collection from the collection itself.

Implementing Iterator Pattern: Custom Collection

In this scenario, you might implement a custom collection (e.g., a BookCollection) and a corresponding iterator (e.g., BookIterator) to allow you to iterate through the books without directly exposing the internal storage or needing to expose the details of how the books are organized. This pattern allows you to maintain encapsulation and decouple the collection’s structure from its usage.

1️⃣Define a custom collection

This code demonstrates an implementation of the Iterator Pattern in the context of a Library class that holds a collection of Book objects. It implements the IEnumerable<Book> interface, which allows the Library to be used in a foreach loop or any other code that expects an IEnumerable collection.

public class Book
{
public string Title { get; set; }
}

public class Library : IEnumerable<Book>
{
private readonly List<Book> _books = new List<Book>();

public void AddBook(Book book)
{
_books.Add(book);
}

public IEnumerator<Book> GetEnumerator()
{
return _books.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
  • The Library class acts as a collection of Book objects. By implementing IEnumerable<Book>, it exposes an iterator, which allows the collection to be traversed using a foreach loop or any other iteration mechanism that works with IEnumerable.
  • The AddBook method allows you to add Book objects to the Library.
  • The GetEnumerator method provides the actual iteration mechanism, enabling you to iterate over the books in the library using the built-in List<Book>‘s enumerator.
  • The explicit implementation of IEnumerable.GetEnumerator ensures compatibility with non-generic collection APIs.

2️⃣Use the custom collection with a foreach loop

This code demonstrates how to use the Iterator Pattern with the Library class.

var library = new Library();
library.AddBook(new Book { Title = "1984" });
library.AddBook(new Book { Title = "Brave New World" });

foreach (var book in library)
{
Console.WriteLine(book.Title);
}
// Output:
// 1984
// Brave New World
  • The Library class implements the Iterator Pattern through the IEnumerable<Book> interface, which allows it to be used in a foreach loop.
  • When the foreach loop is executed on the Library instance, it calls the GetEnumerator method, which returns an IEnumerator<Book> for iterating through the books in the _books list.
  • The foreach loop automatically calls the HasNext and Next methods on the enumerator behind the scenes, allowing for seamless iteration over the collection.

Mediator Pattern

The Mediator Pattern helps reduce this complexity by introducing a central point of communication (the Mediator) that facilitates communication between different components (often called Colleagues). Instead of objects directly interacting with each other, they send their requests to the mediator, which then handles the interactions. This allows objects to remain unaware of each other and only depend on the mediator, promoting loose coupling.

Common Use Cases

  • Message Brokers in CQRS: Managing commands and queries between components.
  • Chat Applications: Handling communication between users through a central mediator.

SOLID Principle

  • Single Responsibility Principle: Encapsulates interaction logic in a mediator, reducing the responsibilities of individual components.

Implementing the Mediator Pattern: Chat Room

The Mediator Pattern in a chat room centralizes communication through a mediator, so users don’t need to interact with each other directly. Instead, all messages are routed through the mediator, which simplifies the system by reducing dependencies between users and making it easier to add new features or changes without affecting the users. This approach makes the chat room more scalablemaintainable, and adaptable as it grows.

1️⃣Define a mediator interface

This code defines an interface IChatRoomMediator that outlines the contract for the mediator in the chat room. The ShowMessage method allows a user to send a message through the mediator, which will then handle the message delivery to the appropriate recipients.

public interface IChatRoomMediator
{
void ShowMessage(User user, string message);
}

The mediator ensures that users communicate indirectly through it, rather than directly with each other.

2️⃣Implement the mediator

This ChatRoom class implements the IChatRoomMediator interface and defines the ShowMessage method. When a user sends a message, the ShowMessage method is called, and it outputs the message to the console, prefixed by the user’s name.

public class ChatRoom : IChatRoomMediator
{
public void ShowMessage(User user, string message)
{
Console.WriteLine($"{user.Name}: {message}");
}
}

The ChatRoom class acts as the mediator, controlling how messages are displayed, ensuring that users only interact with the mediator rather than directly with each other. This simplifies communication and keeps the system decoupled.

3️⃣Create a User class that interacts with the mediator

This User class represents a user in the chat room. It has a Name property and a private reference to the IChatRoomMediator (the mediator). In the constructor, the user’s name and the mediator are initialized.

public class User
{
public string Name { get; private set; }
private readonly IChatRoomMediator _mediator;

public User(string name, IChatRoomMediator mediator)
{
Name = name;
_mediator = mediator;
}

public void SendMessage(string message)
{
_mediator.ShowMessage(this, message);
}
}

The SendMessage method allows the user to send a message by passing the message and the user instance to the ShowMessage method of the mediator.

4️⃣Demonstrate the implementation

This code creates a ChatRoom as the mediator and then two User objects, Alice and Bob, each of whom is linked to the mediator. When Alice sends a message, the SendMessage method calls the ShowMessage method on the mediator, which displays her message. Similarly, when Bob sends a message, the mediator handles displaying his message as well.

// Create a mediator (chat room)
IChatRoomMediator chatRoom = new ChatRoom();

// Create users
var user1 = new User("Alice", chatRoom);
var user2 = new User("Bob", chatRoom);

// Users send messages through the mediator
user1.SendMessage("Hello, Bob!");
user2.SendMessage("Hi, Alice! How are you?");

5️⃣Output

In this example, the User objects send messages through the ChatRoom mediator, which handles communication between users, thereby decoupling them from direct interaction with each other.

Alice: Hello, Bob!
Bob: Hi, Alice! How are you?

Memento Pattern

The Memento Pattern is a behavioral design pattern that allows you to capture and externalize an object’s state so that it can be restored later without exposing the details of its implementation. This pattern is particularly useful when you need to save and restore the state of an object, such as implementing undo/redo functionality. It helps in maintaining a clean separation between the object’s state and its behavior. You would use this pattern to ensure that state changes can be tracked or rolled back easily without having to expose the internals of the object.

Common Use Cases

  • Undo/Redo functionality in Text Editors: The Memento pattern is ideal for implementing undo and redo functionality because it allows you to save the state of the document at each modification and revert to it when needed.
  • Saving and Restoring Game States: In video games, the Memento pattern is useful for saving checkpoints and restoring the game state to those points as players progress.
  • Transaction Management: The Memento pattern helps in maintaining the consistency of a process by saving and restoring the transaction states at various points in time.
  • Snapshot of Complex Objects: When dealing with complex objects, you can use the Memento pattern to store their state at certain intervals, which can be helpful for comparison or reverting to earlier versions.

SOLID Principles

  • Single Responsibility Principle (SRP): The Memento pattern adheres to SRP because the Memento object is responsible solely for storing the state of an object, and not for the behavior of the object itself.
  • Open/Closed Principle (OCP): The Memento pattern supports OCP because you can extend the functionality of an object (e.g., saving and restoring state) without modifying its internal implementation.
  • Liskov Substitution Principle (LSP): By separating state management (Memento) from object behavior, the Memento pattern allows you to substitute any object that can manage its state with another, provided they adhere to the same interface.
  • Interface Segregation Principle (ISP): The Memento pattern doesn’t enforce a large interface; rather, it uses specific methods for saving and restoring state, ensuring that objects are not forced to implement unnecessary functionality.
  • Dependency Inversion Principle (DIP): Memento objects are independent of the classes that utilize them, meaning the object that holds the state is decoupled from the class itself.

Implementing the Memento Pattern: Undo Functionality in a Text Editor

In the context of a text editor, the Memento Pattern can be used to implement undo functionality. Every time a user makes a change (e.g., typing a letter), the editor saves the state of the text at that moment. If the user wants to undo the change, the editor can restore the state from the saved memento.

1️⃣Originator: The Text Editor

The TextEditor class represents the object whose state we want to store and potentially restore. It contains the text that the user is editing.

public class TextEditor
{
private string _text;

public void SetText(string text)
{
_text = text;
}

public string GetText()
{
return _text;
}

// Create a memento (capturing the state)
public Memento SaveStateToMemento()
{
return new Memento(_text);
}

// Restore the state from a memento
public void RestoreStateFromMemento(Memento memento)
{
_text = memento.GetSavedState();
}
}
  • The TextEditor can save its state using the SaveStateToMemento() method. This method creates a Memento object containing the current state (_text).
  • The RestoreStateFromMemento() method allows the editor to revert to a previous state by restoring the _text from a Memento.

2️⃣Memento: Capturing the State

The Memento class encapsulates the state of the TextEditor. The important part is that the Memento only exposes the saved state and does not allow modification of it, ensuring that the internal state of the TextEditor is protected.

public class Memento
{
private readonly string _state;

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

public string GetSavedState()
{
return _state;
}
}

The Memento stores the state (_state), which is the text at a specific point in time. It provides a method to retrieve the state but does not allow direct modification of the state, ensuring the integrity of the stored information.

3️⃣Caretaker: Managing States

The Caretaker class keeps track of the mementos. This allows you to save multiple states (for undo functionality) and restore the most recent state when necessary.

public class Caretaker
{
private readonly Stack<Memento> _mementoList = new Stack<Memento>();

public void AddMemento(Memento memento)
{
_mementoList.Push(memento);
}

public Memento GetMemento()
{
return _mementoList.Pop();
}
}

The Caretaker class uses a Stack<Memento> to store the mementos. It allows the saving of states and retrieval of the most recent state (i.e., performing the undo action).

4️⃣ Implementation

This code demonstrates the Memento design pattern, which is used to capture and restore an object’s internal state without exposing its details. The pattern is particularly useful when implementing undo functionality in applications.

public class Program
{
public static void Main(string[] args)
{
var textEditor = new TextEditor();
var caretaker = new Caretaker();

// User types "Hello"
textEditor.SetText("Hello");
caretaker.AddMemento(textEditor.SaveStateToMemento());

// User types "World"
textEditor.SetText("Hello World");
caretaker.AddMemento(textEditor.SaveStateToMemento());

// Undo the last change ("World")
textEditor.RestoreStateFromMemento(caretaker.GetMemento());
Console.WriteLine(textEditor.GetText()); // Outputs: "Hello"

// Undo the previous change ("Hello")
textEditor.RestoreStateFromMemento(caretaker.GetMemento());
Console.WriteLine(textEditor.GetText()); // Outputs: ""
}
}
  • The Main method simulates a text editor where a user types in text, and the state is saved after each change.
  • When the user types “Hello”, a memento is created to save this state.
  • The user then types “World”, and another memento is created for that new state.
  • The caretaker stores these mementos, which capture the state of the TextEditor at each point.
  • The undo functionality is implemented by calling RestoreStateFromMemento, which reverts the TextEditor to its previous state by restoring the most recent memento.

Observer Pattern

The Observer Pattern is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (subject) changes state, all its dependents (observers) are automatically notified and updated. This pattern is especially useful when you want to minimize the coupling between components of your system. You would use it when you need to manage the flow of information between an object and its dependents without making them tightly dependent on each other. By doing so, the Observer Pattern allows you to addremove, or modify the observers dynamically, without needing to modify the subject.

Common Use Cases

  • UI Event Handling: Observing changes in the data model and automatically updating the user interface. It keeps the user interface in sync with the underlying model, simplifying event-driven programming.
  • Message Systems: In systems like messaging or email services, where the message broadcast needs to notify multiple recipients. Ensures all subscribers to the message get notified without direct dependencies between the message publisher and receivers.
  • Stock Price Monitoring: A stock ticker updating subscribers with live stock price updates. Easily notify multiple clients (subscribers) about stock price changes without direct communication between the clients and stock server.
  • Logging and Monitoring: Observers that track changes in the state of an application for logging or performance monitoring purposes. This decouples the monitoring system from the core business logic.

SOLID Principles

  • Single Responsibility Principle (SRP): The observer and subject classes have a clear responsibility. The observer reacts to changes, while the subject handles the state changes.
  • Open/Closed Principle (OCP): New observers can be added without modifying the subject, allowing the system to be open for extension but closed for modification.
  • Liskov Substitution Principle (LSP): The pattern ensures that observers can be substituted and still behave as expected by notifying all subscribers without altering the subject’s behavior.
  • Interface Segregation Principle (ISP): Observers can implement only the specific methods they need, thus adhering to interface segregation.
  • Dependency Inversion Principle (DIP): High-level modules (subjects) do not depend on low-level modules (observers), but rather on abstractions, which keeps components loosely coupled.

Implementing the Observer Pattern: Chat Application Notifications

In a chat application, when one user sends a message, all other users who are currently connected to the chat should receive a notification about the new message. However, instead of each user being directly aware of every other user (which would create a tangled web of dependencies), we use the Observer Pattern.

1️⃣Define the Observer Interface

The Observer Interface defines the Update method that will be called to notify observers of a new message.

public interface IObserver
{
void Update(string message);
}

The observer interface ensures that any class can act as an observer, providing a standard way to handle updates when the subject (chat room) changes.

2️⃣Create the Subject Class (ChatRoom)

The ChatRoom class is the subject that holds the list of observers (users) and notifies them when a new message is sent.

public class ChatRoom
{
private List<IObserver> _observers = new List<IObserver>();

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

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

public void NotifyObservers(string message)
{
foreach (var observer in _observers)
{
observer.Update(message);
}
}

public void SendMessage(string message)
{
Console.WriteLine($"New message sent: {message}");
NotifyObservers(message); // Notify all observers (users)
}
}

The ChatRoom class acts as the subject. It maintains a list of all users (observers) who are currently registered and sends them notifications (via the Update method) whenever a new message is sent.

3️⃣Create Concrete Observers (Users)

Each User class implements the IObserver interface. When a message is sent, the Update method is called, and the user is notified.

The User class represents an observer (a connected user in the chat). When a new message is sent in the chat room, the observer (user) gets notified and prints the message.

public class User : IObserver
{
private string _name;

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

public void Update(string message)
{
Console.WriteLine($"{_name} received a new message: {message}");
}
}

Each user acts as an observer, reacting to changes (new messages) in the chat room. The observer pattern ensures that all users are notified of new messages without them having direct dependencies on the sender.

4️⃣Set Up and Test the Observer Pattern (Chat Simulation)

We now create a chat room and register several users to observe it. When a message is sent, the system notifies all registered users.

public class Program
{
public static void Main()
{
// Create a chat room
var chatRoom = new ChatRoom();

// Create users (observers)
var user1 = new User("Alice");
var user2 = new User("Bob");
var user3 = new User("Charlie");

// Register users to the chat room (subject)
chatRoom.RegisterObserver(user1);
chatRoom.RegisterObserver(user2);
chatRoom.RegisterObserver(user3);

// Send a message in the chat room
chatRoom.SendMessage("Hello, everyone!");

// Unregister user2
chatRoom.RemoveObserver(user2);

// Send another message
chatRoom.SendMessage("How's everyone doing?");
}
}

The chat room (subject) does not need to know about each specific user. It simply sends the message and lets the observers handle the display. This decouples the message sender (subject) from the receivers (observers).

5️⃣Expected Output

The output demonstrates how all users (observers) get notified when a new message is sent, and how user2 (who is removed) no longer receives updates after being unregistered.

New message sent: Hello, everyone!
Alice received a new message: Hello, everyone!
Bob received a new message: Hello, everyone!
Charlie received a new message: Hello, everyone!

New message sent: How's everyone doing?
Alice received a new message: How's everyone doing?
Charlie received a new message: How's everyone doing?

The Observer Pattern is ideal for this scenario because it allows us to:

  • Decouple the chat room (subject) from the users (observers). The chat room doesn’t need to know the specifics about how each user processes the messages.
  • Easily manage adding and removing observers (users) at runtime. New users can join the chat and start receiving messages without affecting the existing code.
  • Handle dynamic updates: When a new message is sent, all observers are automatically notified, and we don’t have to manually update each user.

State Pattern

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It essentially enables an object to appear as if it has changed its class. This pattern is useful when an object’s behavior is dependent on its state, and you want to avoid using a large number of conditionals or complex switch statements. By encapsulating state-specific behavior in separate classes, the State pattern promotes flexibility and makes the code easier to manage and extend.

Common Use Cases

  • Game Development (Player States):
    The State pattern is used to manage different player states, such as idle, running, or attacking. Instead of using complex conditional statements, each state is encapsulated in a class with specific behaviors.
  • Order Processing Systems:
    The State pattern can be applied in order management systems to handle different stages of an order such as “Order Placed”, “Processing”, “Shipped”, and “Delivered”, where each stage has its own set of actions.
  • UI Components:
    In UI design, the State pattern helps manage complex component behavior depending on its state (e.g., enabled, disabled, loading). Each state can trigger specific UI updates without complex conditions in the code.

SOLID Principles

  • Single Responsibility Principle (SRP):
    Each state class has one responsibility: managing its own behavior. This keeps the code modular and focused on a single task.
  • Open/Closed Principle (OCP):
    The State pattern allows for the addition of new states without changing the existing code. This makes the system easily extensible.
  • Liskov Substitution Principle (LSP):
    Each state class can be replaced by its subtype without altering the expected behavior of the context object. Subclasses of a state can be used interchangeably.
  • Interface Segregation Principle (ISP):
    The State pattern often uses fine-grained interfaces, ensuring that objects only implement what is necessary for their specific behavior in that state.
  • Dependency Inversion Principle (DIP):
    The state context can depend on abstractions (interfaces) rather than concrete state classes, promoting flexibility and decoupling.

Implementing the State Pattern: Order Processing System

Let’s imagine we are developing a simple order processing system where an order can be in different states: “Placed”, “Processing”, “Shipped”, and “Delivered”. Each state has specific behavior, such as performing actions on payment, updating inventory, or notifying the customer.

1️⃣Define the IOrderState Interface
The IOrderState interface will define the actions that can be executed in any state.

public interface IOrderState
{
void HandleOrder(Order order);
}

This interface provides a contract for the different states. All concrete states will implement this interface to provide state-specific behavior.

2️⃣Implement Concrete State Classes
Each state class will implement IOrderState and define behavior specific to that state.

public class OrderPlacedState : IOrderState
{
public void HandleOrder(Order order)
{
Console.WriteLine("Order has been placed. Preparing to process payment.");
// Perform specific actions like validate payment
order.SetState(new OrderProcessingState());
}
}

public class OrderProcessingState : IOrderState
{
public void HandleOrder(Order order)
{
Console.WriteLine("Order is being processed. Verifying inventory.");
// Perform specific actions like checking stock
order.SetState(new OrderShippedState());
}
}

public class OrderShippedState : IOrderState
{
public void HandleOrder(Order order)
{
Console.WriteLine("Order has been shipped. Notify the customer.");
// Perform specific actions like sending shipment notification
order.SetState(new OrderDeliveredState());
}
}

public class OrderDeliveredState : IOrderState
{
public void HandleOrder(Order order)
{
Console.WriteLine("Order has been delivered. Thank you for shopping!");
// Perform specific actions like updating order history
}
}

Here, we have concrete states for “Placed”, “Processing”, “Shipped”, and “Delivered”. Each state class will override the HandleOrder method to implement specific actions.

3️⃣Create the Context Class (Order)
The Order class will maintain the current state and delegate actions to the current state’s HandleOrder method.

public class Order
{
private IOrderState currentState;

public Order()
{
currentState = new OrderPlacedState(); // Initial state
}

public void SetState(IOrderState state)
{
currentState = state;
}

public void HandleOrder()
{
currentState.HandleOrder(this);
}
}

The Order class contains the currentState, which is an instance of a class implementing IOrderState. The HandleOrder method calls the current state’s HandleOrder method to trigger state-specific behavior. The SetState method is used to transition between states.

4️⃣Demonstrating the State Pattern in Action
Finally, we’ll simulate the order process, where each state transitions automatically based on actions defined within the states.

public class Program
{
public static void Main(string[] args)
{
var order = new Order();
order.HandleOrder(); // Order Placed -> Processing
order.HandleOrder(); // Processing -> Shipped
order.HandleOrder(); // Shipped -> Delivered
order.HandleOrder(); // Already Delivered, no further state changes
}
}

In this demonstration, we are creating an order, processing it through each state, and observing how the behavior changes at each state.

5️⃣Output

The State pattern is used to manage the different stages of an order’s lifecycle. Each state encapsulates the behavior for that specific stage (e.g., “processing payment” for the “Order Placed” state), making the code cleaner, easier to extend, and maintain.

Order has been placed. Preparing to process payment.
Order is being processed. Verifying inventory.
Order has been shipped. Notify the customer.
Order has been delivered. Thank you for shopping!

Without the State pattern, you would likely end up with a messy series of if-else or switch statements, which would become difficult to maintain as the number of states grows.

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithmsencapsulate each one, and make them interchangeable. This pattern lets you change the algorithm used by a class without altering its code. It’s especially useful when you have multiple ways to perform an operation, and you want to be able to select the appropriate algorithm at runtime based on the context. This helps avoid cluttering your code with complex conditionals and provides flexibility by allowing you to swap algorithms easily.

Common Use Cases

  • Payment Processing: When handling multiple payment methods (credit card, PayPal, bank transfer), the Strategy Pattern can be used to select the payment algorithm at runtime based on user choice.
  • Sorting Algorithms: In cases where you need to switch between different sorting algorithms (e.g., quicksort, mergesort, or bubblesort), the Strategy Pattern allows you to do so dynamically.
  • Compression Algorithms: If your application needs to support various compression techniques (ZIP, GZIP, BZIP2), the Strategy Pattern helps in selecting the appropriate one based on the file type or user preferences.

SOLID Principles

  • Single Responsibility Principle: Each strategy encapsulates a single algorithm, meaning that each class has only one responsibility.
  • Open/Closed Principle: The Strategy Pattern is open for extension (new strategies can be added), but closed for modification (existing classes don’t need to be modified).
  • Liskov Substitution Principle: Strategies can be substituted for each other without affecting the correctness of the program, as they implement the same interface.
  • Interface Segregation Principle: The pattern promotes the use of small, focused interfaces that provide a clean separation between the context and the strategy.
  • Dependency Inversion Principle: The context depends on abstractions (strategy interface), not concrete implementations, allowing for greater flexibility.

Implementing the Strategy Pattern: Payment Processing

Imagine you are building an e-commerce platform that needs to process payments through various methods (Credit Card, PayPal, and Bank Transfer). Instead of hardcoding the logic for each payment method into your payment processing code, you decide to use the Strategy Pattern to make the payment method flexible and easily extendable.

1️⃣Define the Strategy Interface

The first step is to define a IPaymentStrategy interface that all concrete strategies will implement.

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

The IPaymentStrategy interface defines a Pay method that all payment strategies must implement. This allows different payment methods to be used interchangeably.

2️⃣Create Concrete Strategy Classes

Each payment method will be a concrete class that implements the IPaymentStrategy interface.

public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Processing credit card payment of ${amount}");
}
}

public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of ${amount}");
}
}

public class BankTransferPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine($"Processing bank transfer payment of ${amount}");
}
}

Each concrete strategy implements the Pay method in its own way, reflecting the specific logic needed for each payment method. You could add more strategies in the future (e.g., mobile payments) without modifying existing classes.

3️⃣Create the Context Class

The context class (PaymentProcessor) will use an instance of IPaymentStrategy to delegate the payment processing. This allows the client to change the payment method at runtime.

public class PaymentProcessor
{
private readonly IPaymentStrategy _paymentStrategy;

public PaymentProcessor(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}

public void ProcessPayment(decimal amount)
{
_paymentStrategy.Pay(amount);
}
}

The PaymentProcessor class holds a reference to the IPaymentStrategy interface, allowing it to call the Pay method of the selected strategy. This class can easily change payment methods based on runtime conditions (e.g., user selection).

4️⃣Use the Strategy Pattern in the Application

In the main application, the client selects the payment strategy and passes it to the PaymentProcessor.

public class Program
{
public static void Main(string[] args)
{
// User selects PayPal as the payment method
IPaymentStrategy paymentMethod = new PayPalPayment();
PaymentProcessor paymentProcessor = new PaymentProcessor(paymentMethod);

paymentProcessor.ProcessPayment(100m);

// Later in the program, the user selects Credit Card
paymentMethod = new CreditCardPayment();
paymentProcessor = new PaymentProcessor(paymentMethod);

paymentProcessor.ProcessPayment(200m);
}
}

The Program class demonstrates how the PaymentProcessor uses the Strategy Pattern. The payment method is selected at runtime (in this case, PayPal and Credit Card), and the appropriate strategy is passed to the PaymentProcessor. This allows the payment logic to be switched without modifying the PaymentProcessor class.

By using the Strategy Pattern, you ensure that the payment processing logic remains clean, maintainable, and easy to modify as new payment methods are added over time.

Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm in a base class, allowing subclasses to implement specific steps of the algorithm without changing its structure. It’s particularly useful when you have an algorithm that needs to follow a certain order but allows variation in specific steps. This pattern lets you enforce a specific flow while giving flexibility to the subclasses to handle the details. The main benefit is reducing code duplication by providing a common structure while allowing customization of certain parts of the process.

Common Use Cases

  • Document Processing: The template method defines the overall structure for processing any document, while subclasses can define how specific document types are processed.
  • Order Processing in E-commerce: Every order has a similar processing structure (payment, shipping, etc.), but each type of order might have unique steps. The template method ensures a consistent process while allowing variations for different types of orders.
  • File Importers: Different file formats need different parsing logic but follow the same core processing steps. The template method allows you to reuse the same import flow while enabling specific parsing for each file type.

SOLID Principles

  • Single Responsibility Principle (SRP): The Template Method pattern separates the algorithm structure (in the base class) from the specific behavior (in subclasses), ensuring that each class has only one responsibility.
  • Open/Closed Principle (OCP): You can extend the behavior of the algorithm by creating new subclasses without modifying the existing code.
  • Liskov Substitution Principle (LSP): Any subclass can be substituted for the base class in the template method without altering the correctness of the algorithm.
  • Interface Segregation Principle (ISP): The Template Method pattern encourages small, focused interfaces, where each subclass only implements what is needed for its part of the algorithm.
  • Dependency Inversion Principle (DIP): The template method algorithm depends on abstractions (e.g., abstract methods) rather than concrete implementations.

Code Walkthrough: Implementing the Template Method Pattern for Order Processing

We are implementing an order processing system where the steps for processing orders are common, but each type of order (e.g., standard order, expedited order) has its own specific logic for handling payment and shipping.

1️⃣Define the Base Class with the Template Method

The base class OrderProcessor defines the template method ProcessOrder(), which contains the sequence of steps. Some of these steps are implemented by the base class, while others are abstract and must be implemented by subclasses.

public abstract class OrderProcessor
{
// Template Method
public void ProcessOrder()
{
ValidateOrder();
ProcessPayment();
ShipOrder();
NotifyCustomer();
}

protected abstract void ProcessPayment(); // Step to be implemented by subclasses
protected abstract void ShipOrder(); // Step to be implemented by subclasses

private void ValidateOrder()
{
Console.WriteLine("Validating order...");
}

private void NotifyCustomer()
{
Console.WriteLine("Notifying customer...");
}
}

2️⃣Implement Subclasses for Different Order Types

Each subclass defines the specific steps for handling payment and shipping, but the common structure (validate, notify) remains the same.

public class StandardOrderProcessor : OrderProcessor
{
protected override void ProcessPayment()
{
Console.WriteLine("Processing standard payment...");
}

protected override void ShipOrder()
{
Console.WriteLine("Shipping standard order...");
}
}

public class ExpeditedOrderProcessor : OrderProcessor
{
protected override void ProcessPayment()
{
Console.WriteLine("Processing expedited payment with priority...");
}

protected override void ShipOrder()
{
Console.WriteLine("Shipping expedited order with priority...");
}
}

3️⃣Demonstrating the Template Method in Action

Now, let’s run both types of order processors. We can see that the common steps (validate and notify) are handled by the base class, while payment and shipping are handled differently by each subclass.

public class Program
{
public static void Main()
{
OrderProcessor standardOrder = new StandardOrderProcessor();
Console.WriteLine("Processing Standard Order:");
standardOrder.ProcessOrder(); // Will use the template method

Console.WriteLine();

OrderProcessor expeditedOrder = new ExpeditedOrderProcessor();
Console.WriteLine("Processing Expedited Order:");
expeditedOrder.ProcessOrder(); // Will use the template method
}
}

4️⃣Output

When you run the code, the output will show the steps of the algorithm in the correct order, with variations based on the order type:

Processing Standard Order:
Validating order...
Processing standard payment...
Shipping standard order...
Notifying customer...

Processing Expedited Order:
Validating order...
Processing expedited payment with priority...
Shipping expedited order with priority...
Notifying customer...
  • The ProcessOrder method in the base class OrderProcessor is the template method that defines the sequence of steps.
  • Specific behaviors (payment processing and shipping) are abstracted to be implemented by subclasses, allowing us to define the common steps once while customizing specific actions based on order type.
  • We avoid code duplication by reusing the common structure and allowing subclass-specific behavior.

Visitor Pattern

The Visitor pattern is a behavioral design pattern that allows you to add further operations to objects without having to modify them. It lets you define a new operation for a class hierarchy without changing the classes themselves. This pattern helps you separate concerns by moving operations outside of the classes on which they operate, improving maintainability and scalability.

Common Use Cases

  • Processing Complex Data Structures: The Visitor pattern is ideal when working with complex data structures, such as trees or graphs, where different operations need to be applied to various elements without modifying the data structure itself.
  • Object Serialization: It is frequently used when needing to serialize a set of objects into different formats (e.g., XML, JSON, etc.) without altering the structure of the objects themselves.
  • Compilers or Interpreters: In the context of building a compiler or an interpreter, the Visitor pattern can traverse the abstract syntax tree and apply operations such as optimizations or transformations.

SOLID Principles

  • Single Responsibility Principle (SRP):
    The Visitor pattern adheres to SRP by allowing operations to be handled in a separate class, leaving the object structure free of concerns about these operations.
  • Open/Closed Principle (OCP):
    The Visitor pattern supports OCP by allowing new operations to be added to existing classes without modifying the classes themselves, thus extending their behavior in a modular way.
  • Liskov Substitution Principle (LSP):
    The pattern ensures that subclasses of a particular object can be replaced with their parent class without breaking the expected behavior of the visitor.
  • Interface Segregation Principle (ISP):
    The pattern helps avoid overly complex interfaces by breaking down operations into smaller, more specific ones.
  • Dependency Inversion Principle (DIP):
    The Visitor pattern can work with abstractions rather than concrete implementations, ensuring that high-level modules depend on abstractions.

Implementing the Visitor Pattern: Shopping Cart

Let’s imagine you’re building a simple shopping cart application where different types of items (products, discounts, shipping) need various operations, such as calculating prices, applying discounts, or checking availability. By using the Visitor pattern, you can apply these operations without modifying the item classes themselves.

1️⃣Define the IVisitable Interface

We start by defining the IVisitable interface, which each item type (product, discount, shipping) will implement. This will allow each item to accept a visitor that can perform an operation on it.

public interface IVisitable
{
void Accept(IVisitor visitor);
}

The IVisitable interface is the key point for allowing an object to accept a visitor. Each item class will implement the Accept method to allow the visitor to act on it.

2️⃣Define the IVisitor Interface

The IVisitor interface is defined to represent all the operations that can be performed on the items in the shopping cart. In this example, we’ll define operations for calculating the price and applying discounts.

public interface IVisitor
{
void Visit(Product product);
void Visit(Discount discount);
void Visit(Shipping shipping);
}

The IVisitor interface provides the abstraction for new operations. Concrete visitor classes will implement specific behaviors like price calculation or applying discounts.

3️⃣Implement Concrete Item Classes (ProductDiscountShipping)

Here we define the concrete classes (ProductDiscount, and Shipping) which will implement the IVisitable interface. Each class will have its own implementation of the Accept method, which calls the appropriate visitor method.

public class Product : IVisitable
{
public decimal Price { get; set; }

public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}

public class Discount : IVisitable
{
public decimal Amount { get; set; }

public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}

public class Shipping : IVisitable
{
public decimal Cost { get; set; }

public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}

Each class exposes its structure and behavior via the Accept method, allowing a visitor to interact with the object.

4️⃣Implement the Concrete Visitor Class

The concrete PriceCalculator visitor will be used to calculate the total price of the items in the shopping cart. It visits each item (product, discount, and shipping) and performs its operation (like price calculation or discount application).

public class PriceCalculator : IVisitor
{
public decimal Total { get; private set; }

public void Visit(Product product)
{
Total += product.Price;
}

public void Visit(Discount discount)
{
Total -= discount.Amount;
}

public void Visit(Shipping shipping)
{
Total += shipping.Cost;
}
}

This is where the actual operation (calculating the price) is implemented, and it is applied to each of the IVisitable objects through the visitor pattern.

5️⃣Use the Visitor to Perform Operations

Finally, we demonstrate how the visitor pattern is used to process the items in a shopping cart. We create instances of the items and apply the PriceCalculator visitor to calculate the total.

public class ShoppingCart
{
private List<IVisitable> _items = new List<IVisitable>();

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

public void ApplyVisitor(IVisitor visitor)
{
foreach (var item in _items)
{
item.Accept(visitor);
}
}
}

public class Program
{
public static void Main()
{
var cart = new ShoppingCart();
cart.AddItem(new Product { Price = 100 });
cart.AddItem(new Discount { Amount = 10 });
cart.AddItem(new Shipping { Cost = 15 });

var priceCalculator = new PriceCalculator();
cart.ApplyVisitor(priceCalculator);

Console.WriteLine($"Total Price: {priceCalculator.Total}");
}
}

The Accept method is called on each item to invoke the appropriate operation (price calculation, discount application, etc.). This is a real-world application of the Visitor pattern, where new operations are added (like total price calculation) without modifying the item classes.

Behavioral design patterns in .NET are invaluable tools that help manage complex object interactions while maintaining loose coupling and high cohesion. By implementing these patterns, developers can foster more maintainable and adaptable codebases, ensuring long-term scalability and reducing the risk of introducing bugs. Whether you are building large-scale enterprise applications or maintaining smaller projects, understanding and applying these patterns can significantly improve the readabilityflexibility, and evolution of your software.