Software design patterns
Published onIntro
Software design patterns are like your go-to toolkit for tackling common challenges in software development. These aren’t just any tools; they’re the ones that have been tried and tested by developers across the globe, proving their worth time and again. The great thing about these patterns is their versatility. They’re like your Swiss Army knife in the coding world - useful in almost any programming language and framework, be it object-oriented or functional programming. Learn them once, and you’re set for a wide array of projects.
The beauty of design patterns is how they bring clarity and consistency to your coding adventures. Think of them as your trusty GPS guiding you through the maze of software development. They help you write code that not only works well now but is also easy to manage and scale in the future, which comes in super handy when you’re on a team. Using these patterns can make your codebase much more readable and understandable, almost like it’s speaking the same language. This really helps when multiple people are working on the same project.
In the next few sections, we’re going to take a closer look at some of the most popular design patterns out there, with code examples in the C# programming language. We’ll dig into how they’re structured, how they can be applied, and the specific problems they’re great at solving.
Categories
Software design patterns are categorized into three fundamental types: Creational, Structural, and Behavioral. Each category serves a distinct purpose in the software development process, offering systematic approaches to solving common design challenges.
Creational Patterns (crafting objects):
Creational patterns are all about how you create objects in your software. It’s not just about creating new instances; it’s about doing it in a way that suits your project’s needs and keeps your code flexible. For example, the Singleton pattern ensures that a class has only one instance and provides a single point of access to it. Then there’s the Factory Method, which is great for when you want to leave the exact type of object you create up to subclasses.
Common creational patterns:
- Singleton: Ensures only one instance of a class, providing global access
- Factory Method: Delegates object creation to subclasses, defining an interface
- Abstract Factory: Creates related objects without specifying their concrete classes
- Builder: Separates object construction from its representation, allowing variation
- Object Pool: Manages and reuses a set of initialized objects efficiently
- Prototype: Copies existing objects to create new ones, using a prototype
Structural Patterns (architecting your code):
Structural patterns are like the architects of your code. They help you put together different parts of your software in a smart way, making sure everything is well-organized and efficient. For instance, the Adapter pattern is like a bridge that makes two incompatible interfaces work together. It’s all about making sure different parts of your code can connect and work in harmony.
Common structural patterns:
- Adapter (Wrapper): Allows incompatible interfaces to work together
- Composite: Composes objects into tree structures to represent part-whole hierarchies
- Proxy: Provides a placeholder for another object to control access to it
- Decorator: Dynamically adds responsibilities to an object
- Flyweight: Efficiently shares objects to support large quantities
- Facade: Provides a simplified interface to a complex subsystem
- Bridge: Separates an object’s abstraction from its implementation
Behavioral Patterns (managing object interactions):
Lastly, behavioral patterns are about the interaction and responsibility distribution among objects. It’s like being a conductor in an orchestra, ensuring every section comes in at the right time and plays well with others. The Observer pattern is a classic example here – it lets objects subscribe to event notifications and react accordingly, keeping your system’s components in sync.
Common behavioral patterns:
- Observer: Allows objects to observe and react to events in other objects
- Strategy: Enables selecting an algorithm at runtime
- Command: Encapsulates a request as an object, enabling parameterization
- State: Allows an object to change behavior when its state changes
- Chain of Responsibility: Passes requests along a chain of handlers
- Null Object: Provides a no-operation behavior for a null reference
- Template Method: Defines the skeleton of an algorithm in a method
- Visitor: Defines operations to be performed on elements of an object structure
- Mediator: Encapsulates how a set of objects interact
- Iterator: Provides a way to access elements of an aggregate object sequentially
Creational Patterns
Singleton
The Singleton pattern is a design pattern that ensures a class has only one instance while providing a global access point to this instance. This is particularly useful when exactly one object is needed to coordinate actions across the system.
Please note: The Singleton pattern is controversial and can be considered an antipattern in certain cases due to its potential for creating tight coupling and global state. It can also make unit testing more difficult. However, when used judiciously, it can be a useful pattern for managing resources that need to be globally accessible.
A correct implementation of the Singleton pattern should have the following caracteristiques:
- Single Instance: the Singleton class holds a static reference to its only instance and prevents external instantiation
- Global Access: this instance is accessed through a static property (or method), often named Instance or similar
- Lazy Initialization: the instance is created only when it is first needed
- Thread Safety: ensures that the class is safe in a multithreaded environment, like in a multi-threaded application
The following code implements the Singleton pattern using the built-in .NET Lazy<T>
type:
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance => lazyInstance.Value;
private Singleton()
{
Console.WriteLine("Singleton created");
}
public void DoSomething()
{
Console.WriteLine("Singleton is doing something...");
}
}
// ====================== Usage ======================
// the instance will be created once, when first accessed
Singleton.Instance.DoSomething();
// Outputs: Singleton created
// Outputs: Singleton is doing something...
Singleton.Instance.DoSomething();
// Outputs: Singleton is doing something...
Factory Method
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. This pattern is especially useful when a class cannot anticipate the class of objects it needs to create or when a class wants its subclasses to specify the objects it creates.
The Factory Method pattern is particularly useful for managing and organizing object creation in a system that requires flexibility and scalability in creating various types of objects.
Consider a logging framework where the type of log (like file log, database log, or console log) can vary. Instead of hardcoding each log type, the Factory Method can be used to create different types of loggers dynamically.
Here’s an example in C#:
public abstract class Logger
{
public abstract void Log(string message);
}
public class FileLogger : Logger
{
public override void Log(string message)
{
Console.WriteLine("FileLogger: " + message);
}
}
public class ConsoleLogger : Logger
{
public override void Log(string message)
{
Console.WriteLine("ConsoleLogger: " + message);
}
}
public abstract class LoggerFactory
{
// Factory method
public abstract Logger CreateLogger();
}
public class FileLoggerFactory : LoggerFactory
{
public override Logger CreateLogger()
{
return new FileLogger();
}
}
public class ConsoleLoggerFactory : LoggerFactory
{
public override Logger CreateLogger()
{
return new ConsoleLogger();
}
}
// ====================== Usage ======================
LoggerFactory factory = new FileLoggerFactory();
Logger logger = factory.CreateLogger();
// Use the logger
logger.Log("This is a log message.");
In this example, LoggerFactory
is an abstract class that declares the factory method CreateLogger
. FileLoggerFactory
and ConsoleLoggerFactory
are concrete implementations of LoggerFactory
that create FileLogger
and ConsoleLogger
instances, respectively. This allows the calling code to use the factory to create various types of loggers without being coupled to the concrete classes.
Abstract Factory
The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is especially useful for systems that need to be independent of how their products are created, composed, and represented.
This pattern allows a system to be configured with one of multiple families of products, providing flexibility and scalability to the code.
Consider a user interface library where you need to create elements that should look consistent across operating systems (like buttons and checkboxes). Instead of hardcoding the style for each OS, you can use the Abstract Factory pattern to create a set of related products (buttons, checkboxes) for each specific OS.
The main difference between the Abstract Factory and Factory Method patterns is that the Abstract Factory pattern creates families of related objects, while the Factory Method pattern creates a single object.
Example code in C#:
// Abstract Products
public interface IButton
{
void Render();
}
public interface ICheckbox
{
void Render();
}
// Concrete Products
public class WindowsButton : IButton
{
public void Render()
{
Console.WriteLine("Rendering a button in Windows style");
}
}
public class MacOSButton : IButton
{
public void Render()
{
Console.WriteLine("Rendering a button in MacOS style");
}
}
public class WindowsCheckbox : ICheckbox
{
public void Render()
{
Console.WriteLine("Rendering a checkbox in Windows style");
}
}
public class MacOSCheckbox : ICheckbox
{
public void Render()
{
Console.WriteLine("Rendering a checkbox in MacOS style");
}
}
// Abstract Factory
public interface IGUIFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
// Concrete Factories
public class WindowsFactory : IGUIFactory
{
public IButton CreateButton()
{
return new WindowsButton();
}
public ICheckbox CreateCheckbox()
{
return new WindowsCheckbox();
}
}
public class MacOSFactory : IGUIFactory
{
public IButton CreateButton()
{
return new MacOSButton();
}
public ICheckbox CreateCheckbox()
{
return new MacOSCheckbox();
}
}
// ====================== Usage ======================
IGUIFactory factory;
IButton button;
ICheckbox checkbox;
// Create GUI elements for Windows
factory = new WindowsFactory();
button = factory.CreateButton();
checkbox = factory.CreateCheckbox();
button.Render();
checkbox.Render();
// Create GUI elements for MacOS
factory = new MacOSFactory();
button = factory.CreateButton();
checkbox = factory.CreateCheckbox();
button.Render();
checkbox.Render();
In this example, IGUIFactory
is the abstract factory with methods CreateButton
and CreateCheckbox
. WindowsFactory
and MacOSFactory
are concrete factories that implement these methods to return Windows and MacOS styled buttons and checkboxes, respectively. This allows for the creation of a consistent set of UI elements that are specific to each operating system.
Builder
The Builder pattern is a creational design pattern that provides a flexible solution to various object creation problems in object-oriented programming. The key idea is to separate the construction of a complex object from its representation, allowing the same construction process to create different representations.
The Builder pattern is particularly useful when an object needs to be created with many optional components or configurations. It allows a client to construct complex objects step by step.
Imagine you’re building a text document converter that can convert a document into various formats like Text, PDF, and HTML. Each format requires different components and structures. The Builder pattern can be used to create different types of document converters.
Here’s an example in C#:
public class Document
{
private List<string> _content = new List<string>();
public void AddContent(string text)
{
_content.Add(text);
}
public IEnumerable<string> GetContent()
{
return _content;
}
}
public interface IDocumentBuilder
{
void AddTitle(string title);
void AddParagraph(string text);
Document GetDocument();
}
public class TextDocumentBuilder : IDocumentBuilder
{
private Document _document = new Document();
public void AddTitle(string title)
{
_document.AddContent($"Title: {title}");
}
public void AddParagraph(string text)
{
_document.AddContent($"Paragraph: {text}");
}
public Document GetDocument()
{
return _document;
}
}
public class Director
{
public Document Construct(IDocumentBuilder builder)
{
builder.AddTitle("My Document");
builder.AddParagraph("This is the first paragraph.");
builder.AddParagraph("This is the second paragraph.");
return builder.GetDocument();
}
}
// ====================== Usage ======================
var director = new Director();
var builder = new TextDocumentBuilder();
Document document = director.Construct(builder);
// The document is built and can be used
foreach (var line in document.GetContent())
{
Console.WriteLine(line);
}
In this example, the ConcreteBuilder
class is responsible for constructing the parts of the Product
step by step. The Director
class orchestrates the construction process by using the builder to build the product. This allows different representations of the product to be created without changing the client code.
Object Pool
The Object Pool design pattern is a creational design pattern that aims to reuse objects that are expensive to create, rather than creating and destroying them repeatedly. It’s particularly useful in scenarios where the cost of initializing a class instance is high, the rate of instantiation of a class is high, and the number of instances in use at any one time is low.
The key idea is to temporarily hold a set of initialized objects that are not in use; hence, when a new object is required, the pool first tries to provide one that has already been created and is idle.
Consider a scenario with database connections. Creating a new connection every time one is needed and then destroying it after use is resource-intensive. An object pool of database connections allows for the reuse of connections that are not currently in use.
Here’s an example in C#:
public class DatabaseConnection
{
// Simulate database connection
}
public class DatabaseConnectionPool
{
private readonly Queue<DatabaseConnection> _availableConnections = new Queue<DatabaseConnection>();
private readonly int _maxConnections;
private int _currentCount = 0;
public DatabaseConnectionPool(int maxConnections)
{
_maxConnections = maxConnections;
}
public DatabaseConnection GetConnection()
{
lock (_availableConnections)
{
if (_availableConnections.Count > 0)
{
return _availableConnections.Dequeue();
}
if (_currentCount < _maxConnections)
{
_currentCount++;
return new DatabaseConnection();
}
}
throw new InvalidOperationException("No available connections");
}
public void ReleaseConnection(DatabaseConnection connection)
{
lock (_availableConnections)
{
_availableConnections.Enqueue(connection);
}
}
}
// ====================== Usage ======================
var pool = new DatabaseConnectionPool(10);
var connection = pool.GetConnection();
// Use the connection...
pool.ReleaseConnection(connection);
In this example, DatabaseConnection
represents a database connection. The DatabaseConnectionPool
manages a pool of such connections. When a connection is requested, it either provides an existing idle connection or creates a new one if the maximum number hasn’t been reached. After use, connections are returned to the pool, making them available for reuse.
Prototype
The Prototype pattern is a creational design pattern that allows an object to create customized copies of itself. It’s particularly useful when the construction of a new object is more efficient by copying an existing instance rather than building a new one from scratch.
Imagine an application where you need to create multiple instances of a complex tree structure. Each tree might be slightly different, but they share many commonalities in structure and data. The Prototype pattern allows you to create a basic tree structure and clone it for each new instance, modifying only what’s necessary.
Example Code in C#:
public abstract class Tree
{
public int Height { get; set; }
// The method to clone itself
public abstract Tree Clone();
}
public class AppleTree : Tree
{
public string AppleVariety { get; set; }
public AppleTree(int height, string variety)
{
Height = height;
AppleVariety = variety;
}
public override Tree Clone()
{
// Clone this instance
return MemberwiseClone() as Tree;
}
}
// ====================== Usage ======================
// Original tree
AppleTree originalTree = new AppleTree(10, "Honeycrisp");
// Clone the tree
AppleTree clonedTree = originalTree.Clone() as AppleTree;
clonedTree.Height = 12; // Modifying the property of the cloned tree
Console.WriteLine("Original Tree Height: " + originalTree.Height); // 10
Console.WriteLine("Cloned Tree Height: " + clonedTree.Height); // 12
In this example, Tree
is an abstract base class with a Clone
method. AppleTree
is a concrete subclass implementing the Clone
method. The Clone
method uses MemberwiseClone
, which is a shallow copy method provided by the .NET framework. This allows the calling code to create a new AppleTree
that starts with the same height and apple variety as the original, but can then be modified independently.
Structural Patterns
Adapter (Wrapper)
The Adapter (Wrapper) pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces. This pattern is particularly useful in scenarios where you want to use an existing class but its interface does not match the one you need.
The Adapter pattern can be implemented in two ways:
- Class Adapter: Uses multiple inheritance to adapt one interface to another.
- Object Adapter: Relies on object composition.
Consider a scenario where you have a new system that needs to use an existing library or a legacy system. However, the interface of the new system does not match the interface of the existing library. The Adapter pattern can be used to create a compatibility layer between the new system and the existing library.
Example Code in C# (Object Adapter approach):
// the target interface used by our new system
public interface INewTextFormatter
{
string FormatText(string text);
}
// legacy code we need to integrate via an adapter
public class LegacyTextFormatter
{
public string FormatAsBold(string text)
{
return $"<b>{text}</b>";
}
}
// the adapter
public class TextFormatterAdapter : INewTextFormatter
{
private readonly LegacyTextFormatter _legacyFormatter;
public TextFormatterAdapter(LegacyTextFormatter legacyFormatter)
{
_legacyFormatter = legacyFormatter;
}
public string FormatText(string text)
{
return _legacyFormatter.FormatAsBold(text);
}
}
// ====================== Usage ======================
LegacyTextFormatter legacyFormatter = new LegacyTextFormatter();
INewTextFormatter formatter = new TextFormatterAdapter(legacyFormatter);
string formattedText = formatter.FormatText("Hello World");
Console.WriteLine(formattedText); // Outputs: <b>Hello World</b>
In this example, LegacyTextFormatter
is an existing class with a method FormatAsBold
that needs to be used by a new system expecting an INewTextFormatter
interface. TextFormatterAdapter
adapts LegacyTextFormatter
to the INewTextFormatter
interface, allowing the calling code to use the legacy functionality in the new system.
Composite
The Composite pattern is a structural design pattern that allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. This pattern is especially useful for representing hierarchies where you want to treat both individual objects and groups of objects in a similar manner.
The Composite pattern makes it easier to add new kinds of components. It provides flexibility to create complex compositions as it allows for components to be composed into tree structures to represent part-whole hierarchies.
Consider a graphic design application. It needs to treat both simple elements (like circles, squares) and complex compositions of elements (like a picture made of shapes) in the same way. The Composite pattern can be used to treat both single shapes and compositions of shapes uniformly.
Example Code in C#:
public abstract class Graphic
{
public abstract void Draw();
}
public class Circle : Graphic
{
public override void Draw()
{
Console.WriteLine("Drawing a Circle");
}
}
public class Square : Graphic
{
public override void Draw()
{
Console.WriteLine("Drawing a Square");
}
}
// Composite
public class CompositeGraphic : Graphic
{
private List<Graphic> _children = new List<Graphic>();
public void Add(Graphic graphic)
{
_children.Add(graphic);
}
public void Remove(Graphic graphic)
{
_children.Remove(graphic);
}
public override void Draw()
{
foreach (var child in _children)
{
child.Draw();
}
}
}
// ====================== Usage ======================
var circle = new Circle();
var square = new Square();
var composite = new CompositeGraphic();
composite.Add(circle);
composite.Add(square);
composite.Draw(); // Outputs: Drawing a Circle, Drawing a Square
In this example, Graphic
is the component interface with a Draw
method. Circle
and Square
are leaf objects representing simple graphic elements. CompositeGraphic
is a composite object that can contain leaf objects or other composites, and it implements the Draw
method, which calls Draw
on all its children. This setup allows CompositeGraphic
to combine multiple Graphic
objects (circles, squares, or other composites) and treat them uniformly.
Proxy
The Proxy design pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. This pattern is particularly useful for managing the cost of object creation, controlling the access to the object, or adding a layer of security between the client and the actual object.
There are several types of proxies:
- Remote Proxy: Represents an object in a different address space (e.g., a network)
- Virtual Proxy: Delays the creation and initialization of expensive objects
- Protection Proxy: Controls access to the original object based on access rights
- Smart Reference Proxy: Performs additional actions when an object is accessed, like reference counting
Consider an image viewer application that loads and displays high-resolution images. Loading high-resolution images can be memory and time-intensive. The Proxy pattern can be used to delay the loading of the image until it is absolutely necessary (like when the image needs to be rendered on the screen).
Example Code in C#:
public interface IImage
{
void Display();
}
public class RealImage : IImage
{
private string _fileName;
public RealImage(string fileName)
{
_fileName = fileName;
LoadFromDisk(fileName);
}
private void LoadFromDisk(string fileName)
{
Console.WriteLine("Loading from disk: " + fileName);
}
public void Display()
{
Console.WriteLine("Displaying: " + _fileName);
}
}
public class ProxyImage : IImage
{
private RealImage _realImage;
private string _fileName;
public ProxyImage(string fileName)
{
_fileName = fileName;
}
public void Display()
{
_realImage = _realImage ?? new RealImage(_fileName);
_realImage.Display();
}
}
// ====================== Usage ======================
IImage image = new ProxyImage("my_image.png");
// Image will be loaded only when it is required to be displayed
image.Display();
In this example, ProxyImage
acts as a proxy to RealImage
. The real image loading process is delayed until the Display
method of ProxyImage
is called. This is an instance of a virtual proxy, where the creation of an object is postponed until it’s actually needed.
Decorator
The Decorator pattern is a structural design pattern that allows for dynamically adding behavior to individual objects without affecting the behavior of other objects from the same class. This pattern is particularly useful for extending the capabilities of objects at runtime, in a flexible and reusable way.
The Decorator pattern provides an alternative to subclassing for extending functionality. Rather than statically inheriting behavior from a superclass, you can compose behaviors dynamically.
Imagine a text rendering system where you want to provide options to format text, like adding bold or italic styles. Instead of creating subclasses for each type of formatting, you can use the Decorator pattern to attach these formatting behaviors to a text object dynamically.
Example Code in C#:
public abstract class TextComponent
{
public abstract string Render();
}
public class PlainText : TextComponent
{
private string _text;
public PlainText(string text)
{
_text = text;
}
public override string Render()
{
return _text;
}
}
// Decorator
public abstract class TextDecorator : TextComponent
{
protected TextComponent _textComponent;
public TextDecorator(TextComponent textComponent)
{
_textComponent = textComponent;
}
public override string Render()
{
return _textComponent.Render();
}
}
// Concrete Decorators
public class BoldDecorator : TextDecorator
{
public BoldDecorator(TextComponent textComponent) : base(textComponent)
{
}
public override string Render()
{
return "<b>" + base.Render() + "</b>";
}
}
public class ItalicDecorator : TextDecorator
{
public ItalicDecorator(TextComponent textComponent) : base(textComponent)
{
}
public override string Render()
{
return "<i>" + base.Render() + "</i>";
}
}
// ====================== Usage ======================
TextComponent myText = new PlainText("Hello World");
myText = new BoldDecorator(myText);
myText = new ItalicDecorator(myText);
Console.WriteLine(myText.Render()); // Outputs: <i><b>Hello World</b></i>
In this example, PlainText
is a concrete component representing a basic text. TextDecorator
is an abstract decorator class, and BoldDecorator
and ItalicDecorator
are concrete decorators adding specific behaviors (bold and italic formatting). The calling code dynamically attaches these decorators to the PlainText
object, enhancing its behavior without changing its core functionality.
Flyweight
The Flyweight pattern is a structural design pattern that enables the efficient handling of large numbers of objects by sharing common parts of the object state among multiple objects. This pattern is particularly useful when you need to create a large number of similar objects, where storing each object independently could consume an excessive amount of memory.
The key idea of this pattern is to separate the object state into intrinsic and extrinsic states. The intrinsic state is shared and immutable, stored in the flyweight, while the extrinsic state depends on and varies with the flyweight’s context, and is passed in by the client.
Imagine you’re building a simple text editor where users can write and format text. Users can apply formatting like font size, font color, and bold/italic style to individual characters. In such an editor, many characters may share the same formatting properties, and creating a separate object for each character with the same formatting could be memory-intensive.
Example C# code:
public class CharacterFormatting
{
public string Font { get; }
public int FontSize { get; }
public ConsoleColor Color { get; }
public bool IsBold { get; }
public bool IsItalic { get; }
public CharacterFormatting(string font, int fontSize, ConsoleColor color, bool isBold, bool isItalic)
{
Font = font;
FontSize = fontSize;
Color = color;
IsBold = isBold;
IsItalic = isItalic;
}
}
public class Character
{
public char Symbol { get; }
public CharacterFormatting Formatting { get; }
public Character(char symbol, CharacterFormatting formatting)
{
Symbol = symbol;
Formatting = formatting;
}
public void Display()
{
Console.ForegroundColor = Formatting.Color;
Console.Write($"({Formatting.Font}, {Formatting.FontSize})");
if (Formatting.IsBold) Console.Write(" Bold");
if (Formatting.IsItalic) Console.Write(" Italic");
Console.Write(": ");
Console.ResetColor();
Console.WriteLine(Symbol);
}
}
// ====================== Usage ======================
// Create character formatting objects (flyweights)
var formatting1 = new CharacterFormatting("Arial", 12, ConsoleColor.Blue, false, true);
var formatting2 = new CharacterFormatting("Times New Roman", 14, ConsoleColor.Red, true, false);
// Create characters with shared formatting
var char1 = new Character('A', formatting1);
var char2 = new Character('B', formatting1);
var char3 = new Character('C', formatting2);
// Display characters
char1.Display();
char2.Display();
char3.Display();
In this example, CharacterFormatting
represents the flyweight object that contains shared formatting information. Character
represents individual characters, each having a reference to a CharacterFormatting
object. By sharing formatting information between characters with the same formatting, we reduce memory usage. This is a practical use of the flyweight pattern in a text editor where many characters may have the same formatting attributes.
Facade
The Facade design pattern is a structural design pattern that provides a simplified interface to a complex subsystem or set of interfaces. It acts as a single entry point to a group of interfaces in a subsystem, making it easier for clients to interact with the subsystem without needing to understand its internal complexity. It promotes loose coupling between clients and the subsystem, as clients are shielded from the details of how the subsystem works.
Consider a multimedia framework that provides various functionalities like audio and video processing. Each functionality involves multiple classes and complex interactions. Without a facade, clients would need to understand and manage these interactions. With a facade, clients can perform common tasks (e.g., play audio, play video) without dealing with the intricacies of multimedia processing.
Example C# code:
public class AudioPlayer
{
public void PlayAudio(string audioFile)
{
Console.WriteLine($"Playing audio: {audioFile}");
}
}
public class VideoPlayer
{
public void PlayVideo(string videoFile)
{
Console.WriteLine($"Playing video: {videoFile}");
}
}
public class MultimediaFacade
{
private readonly AudioPlayer _audioPlayer;
private readonly VideoPlayer _videoPlayer;
public MultimediaFacade()
{
_audioPlayer = new AudioPlayer();
_videoPlayer = new VideoPlayer();
}
public void PlayMedia(string fileName)
{
if (fileName.EndsWith(".mp3"))
{
_audioPlayer.PlayAudio(fileName);
}
else if (fileName.EndsWith(".mp4"))
{
_videoPlayer.PlayVideo(fileName);
}
else
{
Console.WriteLine("Unsupported media format.");
}
}
}
// ====================== Usage ======================
var multimediaFacade = new MultimediaFacade();
// Clients use the facade to play media without worrying about subsystem details.
multimediaFacade.PlayMedia("song.mp3");
multimediaFacade.PlayMedia("video.mp4");
In this example, the MultimediaFacade
provides a simplified interface to play media files. It internally uses AudioPlayer
and VideoPlayer
from the subsystem but shields clients from the complexities of selecting the right player based on the media format. Clients simply call PlayMedia
, and the facade handles the rest. This makes it easy for clients to interact with the multimedia framework without needing to understand the intricacies of audio and video playback.
Bridge
The Bridge design pattern is a structural design pattern that separates an object’s abstraction from its implementation, allowing them to vary independently. It’s particularly useful when you want to avoid a permanent binding between an abstraction (interface or abstract class) and its implementation, or when you want to extend both independently.
Consider a drawing application that can draw shapes. You want to support different rendering technologies, such as rendering on a screen or printing on paper. Instead of tightly coupling the shapes with the rendering technology, you can use the Bridge pattern to separate them.
Example C# code:
public interface IRenderer
{
void RenderCircle(int radius);
void RenderSquare(int side);
}
// Concrete rendering technologies
public class ScreenRenderer : IRenderer
{
public void RenderCircle(int radius)
{
Console.WriteLine($"Drawing a circle on the screen with radius {radius}");
}
public void RenderSquare(int side)
{
Console.WriteLine($"Drawing a square on the screen with side {side}");
}
}
public class PrinterRenderer : IRenderer
{
public void RenderCircle(int radius)
{
Console.WriteLine($"Printing a circle with radius {radius}");
}
public void RenderSquare(int side)
{
Console.WriteLine($"Printing a square with side {side}");
}
}
public abstract class Shape
{
protected IRenderer renderer;
protected Shape(IRenderer renderer)
{
this.renderer = renderer;
}
public abstract void Draw();
}
// Refined Abstractions: Circle and Square
public class Circle : Shape
{
private int _radius;
public Circle(IRenderer renderer, int radius) : base(renderer)
{
_radius = radius;
}
public override void Draw()
{
renderer.RenderCircle(_radius);
}
}
public class Square : Shape
{
private int _side;
public Square(IRenderer renderer, int side) : base(renderer)
{
_side = side;
}
public override void Draw()
{
renderer.RenderSquare(_side);
}
}
// ====================== Usage ======================
// Using the Bridge pattern to draw shapes with different renderers
IRenderer screenRenderer = new ScreenRenderer();
IRenderer printerRenderer = new PrinterRenderer();
Shape circle = new Circle(screenRenderer, 5);
Shape square = new Square(printerRenderer, 4);
circle.Draw(); // Drawing a circle on the screen with radius 5
square.Draw(); // Printing a square with side 4
In this example, the Bridge pattern separates shapes (abstraction) from rendering technologies (implementors). You can easily extend the system by adding new shapes or rendering technologies without modifying existing code. Clients interact with the high-level Shape
abstraction, and the specific rendering technology is provided through the IRenderer
interface.
Behavioral Patterns
Observer
The Observer design pattern is a behavioral design pattern that defines a one-to-many dependency between objects, where one object (the subject or publisher) maintains a list of its dependents (observers or subscribers) and notifies them of any state changes, usually by calling one of their methods. It is used to establish a communication mechanism between objects in a loosely coupled manner.
The Observer pattern enables decoupling between subjects and observers. Subjects do not need to know the specifics of their observers, and observers are not tightly coupled to specific subjects. This promotes flexibility and extensibility in the system.
Consider a stock market monitoring system. Multiple traders are interested in tracking the price changes of various stocks. The stock market (subject) notifies traders (observers) whenever a stock’s price changes. Each trader can then take actions based on the price updates.
Example Code in C#:
using System;
using System.Collections.Generic;
// subject (publisher)
public class StockMarket
{
private readonly List<IObserver> _observers = new List<IObserver>();
private double _stockPrice;
public double StockPrice
{
get { return _stockPrice; }
set
{
if (_stockPrice != value)
{
_stockPrice = value;
NotifyObservers();
}
}
}
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
private void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(this);
}
}
}
public interface IObserver
{
void Update(StockMarket stockMarket);
}
public class IndividualTrader : IObserver
{
private string _name;
public IndividualTrader(string name)
{
_name = name;
}
public void Update(StockMarket stockMarket)
{
Console.WriteLine($"{_name} received price update: ${stockMarket.StockPrice}");
}
}
// ====================== Usage ======================
StockMarket stockMarket = new StockMarket();
// Traders (observers)
IObserver trader1 = new IndividualTrader("Trader 1");
IObserver trader2 = new IndividualTrader("Trader 2");
stockMarket.Attach(trader1);
stockMarket.Attach(trader2);
// Simulate stock price changes
stockMarket.StockPrice = 100.0;
// Outputs: Trader 1 received price update: $100
// Outputs: Trader 2 received price update: $100
stockMarket.StockPrice = 105.0;
// Outputs: Trader 1 received price update: $105
// Outputs: Trader 2 received price update: $105
In this example, StockMarket
is the subject that maintains the stock price and notifies registered traders (observers) whenever the price changes. Traders implement the IObserver
interface, which includes the Update
method that gets called when the stock price changes. The Observer pattern allows traders to react to stock price updates without being tightly coupled to the stock market.
Strategy
The Strategy design pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows clients to choose an appropriate algorithm from a family of algorithms at runtime without altering the code that uses the algorithm. The Strategy pattern promotes flexibility by separating the behavior from the context.
The key idea of the Strategy pattern is to encapsulate the behavior (algorithm) into separate strategy classes, making it easy to add new strategies or change existing ones without modifying the context class.
Consider a sorting application where users can choose different sorting algorithms (e.g., bubble sort, merge sort, quicksort) for sorting a list of data. The Strategy pattern can be applied to allow users to switch between different sorting strategies without altering the sorting code.
Example Code in C#:
using System;
using System.Collections.Generic;
public interface ISortStrategy
{
void Sort(List<int> list);
}
public class BubbleSort : ISortStrategy
{
public void Sort(List<int> list)
{
Console.WriteLine("Sorting using bubble sort");
// implementation of bubble sort
}
}
public class MergeSort : ISortStrategy
{
public void Sort(List<int> list)
{
Console.WriteLine("Sorting using merge sort");
// implementation of merge sort
}
}
public class QuickSort : ISortStrategy
{
public void Sort(List<int> list)
{
Console.WriteLine("Sorting using quick sort");
// Implementation of quick sort
}
}
public class Sorter
{
private ISortStrategy _sortStrategy;
public void SetSortStrategy(ISortStrategy strategy)
{
_sortStrategy = strategy;
}
public void SortList(List<int> list)
{
_sortStrategy.Sort(list);
}
}
// ====================== Usage ======================
// Create a list of data to sort
List<int> data = new List<int> { 5, 2, 9, 1, 5, 6 };
// Context: Sorter
Sorter sorter = new Sorter();
// Use different sorting strategies at runtime
sorter.SetSortStrategy(new BubbleSort());
sorter.SortList(data);
sorter.SetSortStrategy(new MergeSort());
sorter.SortList(data);
sorter.SetSortStrategy(new QuickSort());
sorter.SortList(data);
In this example, the SortStrategy
interface defines a common method for sorting. Concrete strategy classes (BubbleSort
, MergeSort
, QuickSort
) implement the sorting algorithms. The Sorter
class represents the context, and it can switch between different sorting strategies at runtime by setting the appropriate strategy. This allows the sorting behavior to be easily changed or extended without modifying the Sorter class or the sorting code.
Command
The Command design pattern is a behavioral design pattern that turns a request into a standalone object. It allows you to encapsulate a request as an object, parameterize clients with requests, queue or log requests, and support undoable operations. The Command pattern separates the sender of a request (client) from the object that performs the action (receiver), and it provides a way to parameterize objects with operations.
The Command pattern allows you to parameterize objects with operations, delay the execution of a command, queue commands, or support undo and redo functionality.
Consider a remote control for a smart home system. The remote control should be able to send different commands to various devices (e.g., turn on lights, adjust thermostat). The Command pattern can be applied to encapsulate these commands as objects and execute them when needed.
Example Code in C#:
using System;
public interface ICommand
{
void Execute();
}
public class Light
{
public void TurnOn()
{
Console.WriteLine("Light is on");
}
public void TurnOff()
{
Console.WriteLine("Light is off");
}
}
public class TurnOnLightCommand : ICommand
{
private readonly Light _light;
public TurnOnLightCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOn();
}
}
public class TurnOffLightCommand : ICommand
{
private readonly Light _light;
public TurnOffLightCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOff();
}
}
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
}
// ====================== Usage ======================
Light livingRoomLight = new Light();
ICommand turnOnCommand = new TurnOnLightCommand(livingRoomLight);
ICommand turnOffCommand = new TurnOffLightCommand(livingRoomLight);
RemoteControl remote = new RemoteControl();
remote.SetCommand(turnOnCommand);
remote.PressButton(); // Light is on
remote.SetCommand(turnOffCommand);
remote.PressButton(); // Light is off
In this example, the Command pattern is used to encapsulate the actions of turning on and off a light as command objects (TurnOnLightCommand
and TurnOffLightCommand
). The RemoteControl
acts as the invoker, and it can associate different commands with its buttons and execute them. This allows you to control various devices with different commands using a remote control.
State
The State design pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The pattern involves defining a set of states, each representing a different behavior, and allowing the object to switch between these states when necessary. It encapsulates the behavior of an object into separate state objects, making it appear as if the object is changing its class.
The State pattern is used when an object’s behavior depends on its internal state, and this behavior needs to change dynamically as the state changes. It promotes loose coupling between the context and state objects.
Consider a vending machine where the behavior of the machine (e.g., dispensing items, returning change) depends on its current state (e.g., HasMoneyState, SoldOutState). The State pattern can be applied to model the different states and their associated behaviors.
Example Code in C#:
using System;
public interface IVendingMachineState
{
void InsertMoney(int amount);
void EjectMoney();
void SelectItem(string item);
void DispenseItem();
}
// States: HasMoneyState, NoMoneyState, SoldOutState
public class HasMoneyState : IVendingMachineState
{
public void InsertMoney(int amount)
{
Console.WriteLine("Money already inserted.");
}
public void EjectMoney()
{
Console.WriteLine("Money ejected.");
}
public void SelectItem(string item)
{
Console.WriteLine($"Item '{item}' selected.");
}
public void DispenseItem()
{
Console.WriteLine("Item dispensed.");
}
}
public class NoMoneyState : IVendingMachineState
{
public void InsertMoney(int amount)
{
Console.WriteLine($"Inserted ${amount}");
}
public void EjectMoney()
{
Console.WriteLine("No money to eject.");
}
public void SelectItem(string item)
{
Console.WriteLine("Insert money first.");
}
public void DispenseItem()
{
Console.WriteLine("Payment required.");
}
}
public class SoldOutState : IVendingMachineState
{
public void InsertMoney(int amount)
{
Console.WriteLine("Machine sold out.");
}
public void EjectMoney()
{
Console.WriteLine("No money to eject.");
}
public void SelectItem(string item)
{
Console.WriteLine("Machine sold out.");
}
public void DispenseItem()
{
Console.WriteLine("Machine sold out.");
}
}
public class VendingMachine
{
private IVendingMachineState _currentState;
public VendingMachine()
{
_currentState = new NoMoneyState(); // initial state
}
public void SetState(IVendingMachineState state)
{
_currentState = state;
}
public void InsertMoney(int amount)
{
_currentState.InsertMoney(amount);
}
public void EjectMoney()
{
_currentState.EjectMoney();
}
public void SelectItem(string item)
{
_currentState.SelectItem(item);
_currentState.DispenseItem();
}
}
// ====================== Usage ======================
VendingMachine vendingMachine = new VendingMachine();
vendingMachine.InsertMoney(5);
// Outputs: Inserted $5
vendingMachine.SelectItem("Soda");
// Outputs: Insert money first.
// Outputs: Payment required.
vendingMachine.SetState(new HasMoneyState());
vendingMachine.InsertMoney(2);
// Outputs: Money already inserted.
vendingMachine.SelectItem("Chips");
// Outputs: Item 'Chips' selected.
// Outputs: Item dispensed.
In this example, the State pattern is used to represent the different states of the vending machine (HasMoneyState
, NoMoneyState
, SoldOutState
). The VendingMachine
can switch between these states as the user interacts with the machine, and each state defines the appropriate behavior for that state. This allows the vending machine to change its behavior dynamically based on its internal state.
Chain of Responsibility
The Chain of Responsibility design pattern is a behavioral design pattern that allows you to pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain. This pattern is particularly useful when you want to avoid coupling between senders and receivers of a request and when you don’t know which object should handle a request at compile time.
Consider a customer support ticketing system where different support agents handle tickets based on their expertise. The Chain of Responsibility pattern can be applied to create a chain of support agents, where each agent can decide whether to handle a ticket or pass it to the next agent in line.
using System;
// Abstract Handler: SupportAgent
public abstract class SupportAgent
{
protected SupportAgent nextHandler;
public void SetNextHandler(SupportAgent handler)
{
nextHandler = handler;
}
public abstract void HandleTicket(Ticket ticket);
}
// Concrete Handlers: FirstLineSupport, SecondLineSupport, SpecialistSupport
public class FirstLineSupport : SupportAgent
{
public override void HandleTicket(Ticket ticket)
{
if (ticket.Category == TicketCategory.General)
{
Console.WriteLine($"First Line Support handled ticket: {ticket.Description}");
}
else if (nextHandler != null)
{
nextHandler.HandleTicket(ticket);
}
else
{
Console.WriteLine("No one can handle this ticket.");
}
}
}
public class SecondLineSupport : SupportAgent
{
public override void HandleTicket(Ticket ticket)
{
if (ticket.Category == TicketCategory.Technical)
{
Console.WriteLine($"Second Line Support handled technical ticket: {ticket.Description}");
}
else if (nextHandler != null)
{
nextHandler.HandleTicket(ticket);
}
else
{
Console.WriteLine("No one can handle this ticket.");
}
}
}
public class SpecialistSupport : SupportAgent
{
public override void HandleTicket(Ticket ticket)
{
Console.WriteLine($"Specialist Support handled special ticket: {ticket.Description}");
}
}
// Ticket class to represent customer support tickets
public class Ticket
{
public string Description { get; }
public TicketCategory Category { get; }
public Ticket(string description, TicketCategory category)
{
Description = description;
Category = category;
}
}
public enum TicketCategory
{
General,
Technical,
Special
}
// ====================== Usage ======================
// Create the chain of support agents
SupportAgent firstLineSupport = new FirstLineSupport();
SupportAgent secondLineSupport = new SecondLineSupport();
SupportAgent specialistSupport = new SpecialistSupport();
firstLineSupport.SetNextHandler(secondLineSupport);
secondLineSupport.SetNextHandler(specialistSupport);
// Create a support ticket
Ticket ticket = new Ticket("Issue with server", TicketCategory.Technical);
// Customer submits the ticket to the first support agent
firstLineSupport.HandleTicket(ticket);
// Outputs: Second Line Support handled technical ticket: Issue with server
In this updated example, SupportAgent
is an abstract class that encapsulates the common handling logic. Concrete support agents (FirstLineSupport
, SecondLineSupport
, SpecialistSupport
) inherit from the abstract class and implement their specific handling logic. The SetNextHandler
method is part of the abstract class, allowing you to set the next handler in the chain.
Null Object
The Null Object design pattern is a behavioral design pattern that addresses the problem of handling null references or null values in a way that avoids excessive null checks and exceptions. It provides an alternative object that implements the same interface or inherits from the same base class as the original object but represents a “do-nothing” or “no-op” behavior. This way, you can safely call methods and access properties on the null object without the risk of null pointer exceptions.
The Null Object pattern is useful in situations where you want to avoid null checks and provide a default behavior when an object is missing or null. It promotes better code readability and maintainability by eliminating the need for conditional checks for null values.
Consider a logging system where you have different loggers for different log levels (e.g., info, warning, error). Sometimes, you may not want to log anything, and using a null object for the logger can help avoid null checks and provide a no-op logging behavior.
Example C# code:
using System;
// Logger: ILogger interface
public interface ILogger
{
void Log(string message);
}
// Concrete Logger Implementations
public class InfoLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"INFO: {message}");
}
}
public class WarningLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"WARNING: {message}");
}
}
public class ErrorLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"ERROR: {message}");
}
}
// Null Logger: NullLogger class
public class NullLogger : ILogger
{
public void Log(string message)
{
// Do nothing (no-op behavior)
}
}
// Client Code
public class Application
{
private readonly ILogger _logger;
public Application(ILogger logger)
{
_logger = logger;
}
public void Run()
{
// Some application logic...
// Logging without null checks
_logger.Log("Application started.");
_logger.Log("Some operation completed.");
// More application logic...
_logger.Log("Application finished.");
}
}
// ====================== Usage ======================
// Use real logger for production
var appWithLogger = new Application(new InfoLogger());
appWithLogger.Run();
// Use null logger for testing or when no logging is needed
var appWithoutLogger = new Application(new NullLogger());
appWithoutLogger.Run();
In this example, the Null Object pattern is applied to the logger. The NullLogger
class implements the ILogger
interface but provides a no-op behavior for the Log
method. The client code (Application
) can use different loggers without worrying about null checks, and it can also use the NullLogger
when no logging is required. This pattern ensures that the application can continue to run smoothly without unexpected null reference exceptions.
Template Method
The Template Method design pattern is a behavioral design pattern that defines the skeleton or outline of an algorithm in a method but allows concrete subclasses to provide the specific implementation details of some steps of the algorithm. It is used when you have an algorithm with multiple steps, and the steps share a common structure but have different implementations.
The Template Method pattern is useful when you want to define a high-level algorithm but allow flexibility in the details of how specific steps are executed. It promotes code reusability and ensures that the overall algorithm structure remains consistent across different implementations.
Consider a document processing application that exports documents to various formats (e.g., PDF, HTML, Plain Text). The export process has a common structure, including opening the document, converting content, and saving the file. However, the details of each step vary depending on the output format. The Template Method pattern can be applied to define the export process while allowing different formats to customize the steps.
Example Code in C#:
using System;
public abstract class DocumentExporter
{
// Template Method
public void ExportDocument()
{
OpenDocument();
ConvertContent();
SaveFile();
}
protected abstract void OpenDocument();
protected abstract void ConvertContent();
protected abstract void SaveFile();
}
public class PDFExporter : DocumentExporter
{
protected override void OpenDocument()
{
Console.WriteLine("Opening PDF document...");
}
protected override void ConvertContent()
{
Console.WriteLine("Converting content to PDF format...");
}
protected override void SaveFile()
{
Console.WriteLine("Saving PDF file...");
}
}
public class HTMLExporter : DocumentExporter
{
protected override void OpenDocument()
{
Console.WriteLine("Opening HTML document...");
}
protected override void ConvertContent()
{
Console.WriteLine("Converting content to HTML format...");
}
protected override void SaveFile()
{
Console.WriteLine("Saving HTML file...");
}
}
public class TextExporter : DocumentExporter
{
protected override void OpenDocument()
{
Console.WriteLine("Opening Plain Text document...");
}
protected override void ConvertContent()
{
Console.WriteLine("Converting content to Plain Text format...");
}
protected override void SaveFile()
{
Console.WriteLine("Saving Plain Text file...");
}
}
// ====================== Usage ======================
// Export a document to different formats
DocumentExporter pdfExporter = new PDFExporter();
pdfExporter.ExportDocument();
DocumentExporter htmlExporter = new HTMLExporter();
htmlExporter.ExportDocument();
DocumentExporter textExporter = new TextExporter();
textExporter.ExportDocument();
In this example, the DocumentExporter
abstract class defines the template method ExportDocument
, which outlines the common structure of the document export process. Concrete exporters (PDFExporter
, HTMLExporter
, TextExporter
) inherit from the abstract class and provide specific implementations for opening the document, converting content, and saving the file. The Template Method pattern ensures that the export process remains consistent while allowing customization for different formats.
Visitor
The Visitor design pattern is a behavioral design pattern that allows you to separate the algorithm or operations from the objects on which it operates. It enables you to add new operations or behaviors to a group of related classes without modifying those classes. The pattern achieves this by defining a separate visitor object that encapsulates the operations and can traverse the elements of an object structure.
The Visitor pattern is particularly useful when you have a complex class hierarchy with various types of elements and need to perform different operations on those elements without modifying their code. It promotes extensibility and allows you to add new operations (visitors) without changing the existing classes.
Consider a code analysis tool that needs to traverse and analyze the abstract syntax tree (AST) of a programming language. The AST consists of various node types (e.g., expressions, statements) that need to be processed differently (e.g., printing, type checking, optimization). The Visitor pattern can be used to define different visitors for each type of node, keeping the analysis code separate from the node classes.
Example C# code:
using System;
using System.Collections.Generic;
public interface ASTNode
{
void Accept(Visitor visitor);
}
public class ExpressionNode : ASTNode
{
public void Accept(Visitor visitor)
{
visitor.Visit(this);
}
}
public class StatementNode : ASTNode
{
public void Accept(Visitor visitor)
{
visitor.Visit(this);
}
}
public interface Visitor
{
void Visit(ExpressionNode node);
void Visit(StatementNode node);
}
public class PrintVisitor : Visitor
{
public void Visit(ExpressionNode node)
{
Console.WriteLine("Visiting and printing an ExpressionNode.");
}
public void Visit(StatementNode node)
{
Console.WriteLine("Visiting and printing a StatementNode.");
}
}
public class TypeCheckVisitor : Visitor
{
public void Visit(ExpressionNode node)
{
Console.WriteLine("Visiting and type-checking an ExpressionNode.");
}
public void Visit(StatementNode node)
{
Console.WriteLine("Visiting and type-checking a StatementNode.");
}
}
public class AbstractSyntaxTree
{
private readonly List<ASTNode> nodes = new List<ASTNode>();
public void AddNode(ASTNode node)
{
nodes.Add(node);
}
public void Accept(Visitor visitor)
{
foreach (var node in nodes)
{
node.Accept(visitor);
}
}
}
// ====================== Usage ======================
AbstractSyntaxTree ast = new AbstractSyntaxTree();
ast.AddNode(new ExpressionNode());
ast.AddNode(new StatementNode());
Visitor printVisitor = new PrintVisitor();
Visitor typeCheckVisitor = new TypeCheckVisitor();
// Perform different operations on the AST
ast.Accept(printVisitor);
// Outputs: Visiting and printing an ExpressionNode.
// Outputs: Visiting and printing a StatementNode.
ast.Accept(typeCheckVisitor);
// Outputs: Visiting and type-checking an ExpressionNode.
// Outputs: Visiting and type-checking a StatementNode.
In this example, the Visitor
interface defines visit methods for different types of AST nodes (expression and statement nodes). Concrete visitors (PrintVisitor
and TypeCheckVisitor
) provide specific implementations for these visit methods. The AbstractSyntaxTree
represents the object structure containing AST nodes, and it accepts visitors to perform various operations without modifying the node classes.
Mediator
The Mediator design pattern is a behavioral design pattern that promotes loose coupling among objects by centralizing their interactions through a mediator object. It defines an object (the mediator) that encapsulates how a set of objects interact with each other. Instead of objects communicating directly with each other, they communicate through the mediator. This pattern helps reduce the dependencies between objects and simplifies the maintenance of complex systems.
The Mediator pattern is beneficial when you have a complex system where many objects need to interact with each other. By centralizing the communication through a mediator, you can avoid the need for each object to be aware of the others, reducing coupling and making the system more maintainable and extensible.
Consider a chat application where users can send messages to each other. In this scenario, users are the colleagues, and the chat server acts as the mediator, managing message delivery between users. The Mediator pattern helps ensure that users don’t need to know the details of each other’s messaging logic.
Example C# code:
using System;
using System.Collections.Generic;
public interface IChatRoom
{
void SendMessage(User sender, string message);
void RegisterUser(User user);
}
public class ChatRoom : IChatRoom
{
private readonly List<User> users = new List<User>();
public void RegisterUser(User user)
{
users.Add(user);
}
public void SendMessage(User sender, string message)
{
foreach (var user in users)
{
// Send the message to all users except the sender
if (user != sender)
{
user.ReceiveMessage(sender, message);
}
}
}
}
public class User
{
public string Name { get; private set; }
private readonly IChatRoom chatRoom;
public User(string name, IChatRoom chatRoom)
{
Name = name;
this.chatRoom = chatRoom;
chatRoom.RegisterUser(this);
}
public void SendMessage(string message)
{
chatRoom.SendMessage(this, message);
}
public void ReceiveMessage(User sender, string message)
{
Console.WriteLine($"{Name} received a message from {sender.Name}: {message}");
}
}
// ====================== Usage ======================
IChatRoom chatRoom = new ChatRoom();
User user1 = new User("Alice", chatRoom);
User user2 = new User("Bob", chatRoom);
User user3 = new User("Charlie", chatRoom);
user1.SendMessage("Hello, everyone!");
user2.SendMessage("Hi, Alice!");
user3.SendMessage("Hey there!");
In this example, the ChatRoom
acts as the mediator, and individual users (User
) communicate through the chat room. Users send messages to the chat room, which then forwards the messages to all other users. The Mediator pattern allows users to communicate without needing to know the details of each other’s messaging logic, promoting loose coupling and maintainability.
Iterator
The Iterator design pattern is a behavioral design pattern that provides a way to access elements of a collection (such as a list or an array) sequentially without exposing the underlying representation of the collection. It defines an object called an iterator, which encapsulates the traversal logic, and allows clients to iterate over elements of a collection without needing to know how the iteration is implemented.
Consider a scenario where you have a list of items, and you want to iterate over these items to perform some operations without exposing the list’s internal structure. The Iterator pattern allows you to achieve this separation.
Example C# code:
using System;
using System.Collections;
using System.Collections.Generic;
public interface IIterator<T>
{
T Next();
bool HasNext();
}
public class ListIterator<T> : IIterator<T>
{
private readonly List<T> list;
private int currentIndex = 0;
public ListIterator(List<T> list)
{
this.list = list;
}
public T Next()
{
if (HasNext())
{
T currentItem = list[currentIndex];
currentIndex++;
return currentItem;
}
throw new InvalidOperationException("No more elements to iterate.");
}
public bool HasNext()
{
return currentIndex < list.Count;
}
}
public interface ICollection<T>
{
IIterator<T> CreateIterator();
void Add(T item);
}
public class ListCollection<T> : ICollection<T>
{
private readonly List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public IIterator<T> CreateIterator()
{
return new ListIterator<T>(items);
}
}
// ====================== Usage ======================
ICollection<string> collection = new ListCollection<string>();
collection.Add("Item 1");
collection.Add("Item 2");
collection.Add("Item 3");
IIterator<string> iterator = collection.CreateIterator();
while (iterator.HasNext())
{
string item = iterator.Next();
Console.WriteLine("Item: " + item);
}
In this example, the ListIterator
encapsulates the logic for iterating over a list of items (ListCollection
). The client code can iterate over the items without needing to know the internal structure of the list, demonstrating the separation of concerns provided by the Iterator pattern.