Design Patterns - Code snippets in C#

Photo by Lucas Kapla on Unsplash

Design Patterns - Code snippets in C#

Design patterns are reusable solutions to common software design problems. Here are 10 of the most commonly used design patterns:

  1. Singleton Pattern: Ensures that a class has only one instance and provides a global point of access to that instance.

  2. Factory Method Pattern: Provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.

  3. Abstract Factory Pattern: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

  4. Builder Pattern: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.

  5. Prototype Pattern: Creates new objects by copying an existing object (prototype) and modifying it as needed.

  6. Adapter Pattern: Allows objects with incompatible interfaces to work together by providing a common interface that both can use.

  7. Decorator Pattern: Dynamically adds or overrides behavior in an object's methods at runtime without altering its structure.

  8. Facade Pattern: Provides a simplified interface to a set of interfaces in a subsystem, making it easier to use.

  9. Proxy Pattern: Provides a surrogate or placeholder for another object to control its access, add functionality, or delay instantiation.

  10. Observer Pattern: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

These patterns are categorized into three main categories: creational, structural, and behavioral. Learning and understanding these patterns can significantly improve your software design skills and help you create more maintainable, modular, and scalable code.

Singleton Pattern

The Singleton pattern is a creational design pattern that ensures that a class has only one instance and provides a global point of access to that instance. It is often used to manage resources that should be shared among multiple parts of an application. Here's a complete implementation of the Singleton pattern in C#, covering different scenarios and variations:

public sealed class Singleton
{
    private static Singleton instance;
    private static readonly object lockObject = new object();

    private Singleton()
    {
        // Private constructor to prevent external instantiation.
    }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (lockObject)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    // Add instance methods and properties here.
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using the Singleton instance
        Singleton instance1 = Singleton.Instance;
        Singleton instance2 = Singleton.Instance;
        Console.WriteLine(instance1 == instance2); // Output: true (same instance)

        // Scenario 2: Multithreading
        Parallel.Invoke(
            () => SingletonThread1(),
            () => SingletonThread2()
        );

        // Scenario 3: Serialization and Deserialization
        Singleton instance3 = Singleton.Instance;

        // Serialize the instance
        using (FileStream fileStream = new FileStream("singleton.bin", FileMode.Create))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(fileStream, instance3);
        }

        // Deserialize the instance
        Singleton instance4;
        using (FileStream fileStream = new FileStream("singleton.bin", FileMode.Open))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            instance4 = (Singleton)formatter.Deserialize(fileStream);
        }

        Console.WriteLine(instance3 == instance4); // Output: false (different instances)
    }

    static void SingletonThread1()
    {
        Singleton instance = Singleton.Instance;
        // Use the instance safely in a multithreaded context.
    }

    static void SingletonThread2()
    {
        Singleton instance = Singleton.Instance;
        // Use the instance safely in a multithreaded context.
    }
}

In this example, the sealed class Singleton ensures that the class cannot be inherited, which helps maintain the single instance guarantee. The Singleton class ensures that only one instance is created and used across various scenarios, including basic usage, multithreading, and serialization/deserialization. The use of a double-checked locking mechanism (lockObject) ensures thread safety when creating the instance in a multithreaded environment.

Keep in mind that while Singleton is useful in certain situations, it's important to carefully consider whether it's the best design choice for your specific application needs.

New Approach

The latest .NET version supports a more concise way of creating a singleton class:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());

    // Private constructor to prevent instantiation from other classes.
    private Singleton()
    {
    }

    // The public method to access the single instance.
    public static Singleton Instance => lazyInstance.Value;

    // Add other methods and properties here as needed.
}

In this example:

  1. sealed class Singleton ensures that the class cannot be inherited, which helps maintain the single instance guarantee.

  2. private static readonly Lazy<Singleton> lazyInstance is a Lazy<T> instance that ensures lazy initialization of the singleton. It's thread-safe and initializes the instance only when it's first accessed.

  3. The constructor private Singleton() is made private to prevent external instantiation of the class.

  4. public static Singleton Instance is a public property that provides access to the single instance of the class. The Value property of Lazy<T> is used to access the instance, and it's created when first accessed.

Factory Method Pattern

The Factory Method pattern is a creational design pattern that provides an interface for creating objects, but allows subclasses to decide which class to instantiate. Here's a complete implementation of the Factory Method pattern in C# covering various scenarios:

using System;

// Product
interface IProduct
{
    void ShowInfo();
}

// Concrete Products
class ConcreteProductA : IProduct
{
    public void ShowInfo()
    {
        Console.WriteLine("Concrete Product A");
    }
}

class ConcreteProductB : IProduct
{
    public void ShowInfo()
    {
        Console.WriteLine("Concrete Product B");
    }
}

// Creator
abstract class Creator
{
    public abstract IProduct FactoryMethod();
}

// Concrete Creators
class ConcreteCreatorA : Creator
{
    public override IProduct FactoryMethod()
    {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB : Creator
{
    public override IProduct FactoryMethod()
    {
        return new ConcreteProductB();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using the Factory Method
        Creator creatorA = new ConcreteCreatorA();
        IProduct productA = creatorA.FactoryMethod();
        productA.ShowInfo();  // Output: Concrete Product A

        Creator creatorB = new ConcreteCreatorB();
        IProduct productB = creatorB.FactoryMethod();
        productB.ShowInfo();  // Output: Concrete Product B

        // Scenario 2: Decoupling client from product creation
        Client(new ConcreteCreatorA());  // Output: Concrete Product A
        Client(new ConcreteCreatorB());  // Output: Concrete Product B
    }

    static void Client(Creator creator)
    {
        IProduct product = creator.FactoryMethod();
        product.ShowInfo();
    }
}

In this example, the Factory Method pattern is implemented with a Creator class and its subclasses, along with their respective FactoryMethod implementations. The IProduct interface defines the contract for the products, and the ConcreteProductA and ConcreteProductB classes implement it. The Program class demonstrates two scenarios:

  1. Direct usage of the Factory Method pattern by creating Creator instances and using the FactoryMethod to create products.

  2. Decoupling the client code from the product creation process by using the Client method, which accepts a Creator instance as a parameter.

By using the Factory Method pattern, you can achieve flexible object creation and easily extend the product hierarchy without modifying the client code.

Abstract Factory Pattern

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. Here's a complete implementation of the Abstract Factory pattern in C# covering various scenarios:

using System;

// Abstract Product A
interface IProductA
{
    void DisplayInfo();
}

// Concrete Product A1
class ConcreteProductA1 : IProductA
{
    public void DisplayInfo()
    {
        Console.WriteLine("Concrete Product A1");
    }
}

// Concrete Product A2
class ConcreteProductA2 : IProductA
{
    public void DisplayInfo()
    {
        Console.WriteLine("Concrete Product A2");
    }
}

// Abstract Product B
interface IProductB
{
    void DisplayInfo();
}

// Concrete Product B1
class ConcreteProductB1 : IProductB
{
    public void DisplayInfo()
    {
        Console.WriteLine("Concrete Product B1");
    }
}

// Concrete Product B2
class ConcreteProductB2 : IProductB
{
    public void DisplayInfo()
    {
        Console.WriteLine("Concrete Product B2");
    }
}

// Abstract Factory
interface IAbstractFactory
{
    IProductA CreateProductA();
    IProductB CreateProductB();
}

// Concrete Factory 1
class ConcreteFactory1 : IAbstractFactory
{
    public IProductA CreateProductA()
    {
        return new ConcreteProductA1();
    }

    public IProductB CreateProductB()
    {
        return new ConcreteProductB1();
    }
}

// Concrete Factory 2
class ConcreteFactory2 : IAbstractFactory
{
    public IProductA CreateProductA()
    {
        return new ConcreteProductA2();
    }

    public IProductB CreateProductB()
    {
        return new ConcreteProductB2();
    }
}

class Client
{
    public void Run(IAbstractFactory factory)
    {
        IProductA productA = factory.CreateProductA();
        IProductB productB = factory.CreateProductB();

        productA.DisplayInfo();
        productB.DisplayInfo();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Concrete Factory 1
        IAbstractFactory factory1 = new ConcreteFactory1();
        Client client1 = new Client();
        client1.Run(factory1);

        // Scenario 2: Using Concrete Factory 2
        IAbstractFactory factory2 = new ConcreteFactory2();
        Client client2 = new Client();
        client2.Run(factory2);
    }
}

In this example, the Abstract Factory pattern is implemented with abstract product interfaces, concrete product classes, an abstract factory interface, and concrete factory classes. The Client class demonstrates two scenarios:

  1. Using ConcreteFactory1 to create ConcreteProductA1 and ConcreteProductB1.

  2. Using ConcreteFactory2 to create ConcreteProductA2 and ConcreteProductB2.

By using the Abstract Factory pattern, you can create families of related or dependent objects while providing a consistent way to create those objects regardless of their concrete classes. This helps ensure better modularity and flexibility in your codebase.

How is Abstract Factory Pattern different from Factory Method pattern

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It deals with creating multiple related objects that belong to a common theme. This pattern is suitable when you need to create families of objects that are related and designed to work together. It's particularly useful when you want to ensure that the products you create are compatible and consistent within a given family.

The Factory Method pattern defines an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It deals with creating single objects while allowing subclasses to determine the specific class to instantiate. This pattern is suitable when you have a family of related classes with a common base class, and you want to delegate the responsibility of creating instances of these classes to subclasses. It's helpful when you want to decouple the client code from the specific classes being instantiated.

In summary, the Abstract Factory pattern focuses on creating families of related objects with consistent interfaces, while the Factory Method pattern focuses on creating individual objects in a flexible manner by delegating the instantiation responsibility to subclasses. The choice between these patterns depends on the level of abstraction and the nature of the objects you're dealing with in your application.

Builder Pattern

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations. Here's a comprehensive implementation of the Builder pattern in C# covering various scenarios:

using System;

// Product
class Product
{
    public string PartA { get; set; }
    public string PartB { get; set; }
    public string PartC { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"Part A: {PartA}, Part B: {PartB}, Part C: {PartC}");
    }
}

// Builder
interface IBuilder
{
    void BuildPartA();
    void BuildPartB();
    void BuildPartC();
    Product GetProduct();
}

// Concrete Builders
class ConcreteBuilderA : IBuilder
{
    private Product product = new Product();

    public void BuildPartA()
    {
        product.PartA = "Part A for Builder A";
    }

    public void BuildPartB()
    {
        product.PartB = "Part B for Builder A";
    }

    public void BuildPartC()
    {
        product.PartC = "Part C for Builder A";
    }

    public Product GetProduct()
    {
        return product;
    }
}

class ConcreteBuilderB : IBuilder
{
    private Product product = new Product();

    public void BuildPartA()
    {
        product.PartA = "Part A for Builder B";
    }

    public void BuildPartB()
    {
        product.PartB = "Part B for Builder B";
    }

    public void BuildPartC()
    {
        product.PartC = "Part C for Builder B";
    }

    public Product GetProduct()
    {
        return product;
    }
}

// Director
class Director
{
    public void Construct(IBuilder builder)
    {
        builder.BuildPartA();
        builder.BuildPartB();
        builder.BuildPartC();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Concrete Builder A
        IBuilder builderA = new ConcreteBuilderA();
        Director director = new Director();
        director.Construct(builderA);
        Product productA = builderA.GetProduct();
        productA.ShowInfo();

        // Scenario 2: Using Concrete Builder B
        IBuilder builderB = new ConcreteBuilderB();
        director.Construct(builderB);
        Product productB = builderB.GetProduct();
        productB.ShowInfo();
    }
}

In this example, the Builder pattern is implemented with a Product class, an IBuilder interface, concrete builder classes (ConcreteBuilderA and ConcreteBuilderB), and a Director class. The Director class orchestrates the construction process using a builder instance.

The scenarios show how to use different concrete builders to create products with distinct parts. The Construct method in the Director class defines the construction process and ensures that the same process can create different representations of the Product.

By using the Builder pattern, you can separate the construction logic from the product's representation, allowing you to create complex objects step by step while maintaining flexibility and reusability.

Prototype Pattern

The Prototype pattern is a creational design pattern that involves creating a new object by copying an existing object, known as the prototype. This pattern helps in creating objects efficiently and is particularly useful when the instantiation process is expensive. Here's a comprehensive implementation of the Prototype pattern in C# covering various scenarios:

using System;

// Prototype
interface ICloneablePrototype
{
    ICloneablePrototype Clone();
}

// Concrete Prototype A
class ConcretePrototypeA : ICloneablePrototype
{
    public int Id { get; set; }

    public ICloneablePrototype Clone()
    {
        return new ConcretePrototypeA { Id = this.Id };
    }
}

// Concrete Prototype B
class ConcretePrototypeB : ICloneablePrototype
{
    public string Name { get; set; }

    public ICloneablePrototype Clone()
    {
        return new ConcretePrototypeB { Name = this.Name };
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Concrete Prototype A
        ConcretePrototypeA originalA = new ConcretePrototypeA { Id = 1 };
        ConcretePrototypeA clonedA = (ConcretePrototypeA)originalA.Clone();
        Console.WriteLine(originalA.Id); // Output: 1
        Console.WriteLine(clonedA.Id);   // Output: 1

        // Scenario 2: Using Concrete Prototype B
        ConcretePrototypeB originalB = new ConcretePrototypeB { Name = "Prototype B" };
        ConcretePrototypeB clonedB = (ConcretePrototypeB)originalB.Clone();
        Console.WriteLine(originalB.Name); // Output: Prototype B
        Console.WriteLine(clonedB.Name);   // Output: Prototype B
    }
}

In this example, the Prototype pattern is implemented using an ICloneablePrototype interface that defines a Clone method. Concrete prototypes (ConcretePrototypeA and ConcretePrototypeB) implement this interface and provide their own cloning logic.

The scenarios demonstrate how to create clones of objects by calling the Clone method. Cloning is an efficient way to create new objects that share the same structure as existing objects, especially when object creation is resource-intensive.

By using the Prototype pattern, you can avoid costly object creation and initialization processes, improve performance, and create copies of objects with ease. Keep in mind that some objects might require deep cloning if they contain complex nested structures or references.

Adapter Pattern

The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two interfaces, allowing them to collaborate without changing their code. Here's a comprehensive implementation of the Adapter pattern in C# covering various scenarios:

using System;

// Adaptee (Incompatible Class)
class Adaptee
{
    public void SpecificRequest()
    {
        Console.WriteLine("Adaptee's Specific Request");
    }
}

// Target (Interface Expected by Client)
interface ITarget
{
    void Request();
}

// Adapter
class Adapter : ITarget
{
    private readonly Adaptee adaptee;

    public Adapter(Adaptee adaptee)
    {
        this.adaptee = adaptee;
    }

    public void Request()
    {
        adaptee.SpecificRequest();
    }
}

// Client
class Client
{
    public void ExecuteOperation(ITarget target)
    {
        target.Request();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using the Adapter with Adaptee
        Adaptee adaptee = new Adaptee();
        ITarget adapter = new Adapter(adaptee);
        Client client = new Client();
        client.ExecuteOperation(adapter); // Output: Adaptee's Specific Request
    }
}

In this example, the Adapter pattern is implemented with an Adaptee class (representing an incompatible class), an ITarget interface (representing the expected interface), and an Adapter class that adapts the Adaptee to the ITarget interface.

The Client class demonstrates how the Adapter works by accepting an ITarget instance and executing its Request method. The adapter's Request method internally calls the SpecificRequest method of the Adaptee.

Scenarios like the one demonstrated can involve integrating legacy code or third-party libraries into a new system or adapting interfaces to meet specific requirements without modifying existing code.

By using the Adapter pattern, you can achieve seamless collaboration between objects with incompatible interfaces, ensuring that the client code works smoothly with diverse components.

Decorator Pattern

The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. Here's a comprehensive implementation of the Decorator pattern in C# covering various scenarios:

using System;

// Component
interface IComponent
{
    string Operation();
}

// Concrete Component
class ConcreteComponent : IComponent
{
    public string Operation()
    {
        return "Concrete Component";
    }
}

// Decorator
abstract class Decorator : IComponent
{
    protected IComponent component;

    public Decorator(IComponent component)
    {
        this.component = component;
    }

    public virtual string Operation()
    {
        return component.Operation();
    }
}

// Concrete Decorator A
class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent component) : base(component)
    {
    }

    public override string Operation()
    {
        return $"Concrete Decorator A, {base.Operation()}";
    }
}

// Concrete Decorator B
class ConcreteDecoratorB : Decorator
{
    public ConcreteDecoratorB(IComponent component) : base(component)
    {
    }

    public override string Operation()
    {
        return $"Concrete Decorator B, {base.Operation()}";
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Decorators
        IComponent component = new ConcreteComponent();
        Console.WriteLine(component.Operation()); // Output: Concrete Component

        IComponent decoratorA = new ConcreteDecoratorA(component);
        Console.WriteLine(decoratorA.Operation()); // Output: Concrete Decorator A, Concrete Component

        IComponent decoratorB = new ConcreteDecoratorB(decoratorA);
        Console.WriteLine(decoratorB.Operation()); // Output: Concrete Decorator B, Concrete Decorator A, Concrete Component
    }
}

In this example, the Decorator pattern is implemented with an IComponent interface (representing the base component), a ConcreteComponent class (implementing the base component), an abstract Decorator class, and two concrete decorator classes (ConcreteDecoratorA and ConcreteDecoratorB).

The scenarios demonstrate how decorators can wrap around components to add or modify behavior dynamically. Decorators can be stacked on top of each other, allowing multiple layers of behavior to be applied to the base component.

By using the Decorator pattern, you can achieve flexible composition of objects with different behaviors while keeping classes open for extension and closed for modification. This is especially useful when you need to add features to existing classes without changing their source code or when you have a need for a variety of customizable combinations.

Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes it easier for clients to interact with complex systems by hiding the underlying complexity. Here's a comprehensive implementation of the Facade pattern in C# covering various scenarios:

using System;

// Subsystem A
class SubsystemA
{
    public void MethodA()
    {
        Console.WriteLine("Subsystem A - Method A");
    }
}

// Subsystem B
class SubsystemB
{
    public void MethodB()
    {
        Console.WriteLine("Subsystem B - Method B");
    }
}

// Subsystem C
class SubsystemC
{
    public void MethodC()
    {
        Console.WriteLine("Subsystem C - Method C");
    }
}

// Facade
class Facade
{
    private SubsystemA subsystemA;
    private SubsystemB subsystemB;
    private SubsystemC subsystemC;

    public Facade()
    {
        subsystemA = new SubsystemA();
        subsystemB = new SubsystemB();
        subsystemC = new SubsystemC();
    }

    public void Operate()
    {
        Console.WriteLine("Facade - Operating...");
        subsystemA.MethodA();
        subsystemB.MethodB();
        subsystemC.MethodC();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Facade
        Facade facade = new Facade();
        facade.Operate();
        // Output:
        // Facade - Operating...
        // Subsystem A - Method A
        // Subsystem B - Method B
        // Subsystem C - Method C
    }
}

In this example, the Facade pattern is implemented with three subsystem classes (SubsystemA, SubsystemB, and SubsystemC), each having their own methods. The Facade class provides a simplified interface that orchestrates the interactions with the subsystems.

The scenario demonstrates how the client code can use the Facade to interact with the complex subsystems without needing to understand their individual complexities. The Facade encapsulates the details of working with the subsystems and presents a unified interface to the client.

By using the Facade pattern, you can provide a clear and simplified interface to a complex system, making it easier for clients to use the system's functionality without being overwhelmed by its internal details. This is particularly useful when you want to shield clients from complex interactions with multiple components or subsystems.

Proxy Pattern

The Proxy pattern is a structural design pattern that provides a surrogate or placeholder for another object to control its access, add functionality, or delay instantiation. Here's a comprehensive implementation of the Proxy pattern in C# covering various scenarios:

using System;

// Subject
interface ISubject
{
    void Request();
}

// Real Subject
class RealSubject : ISubject
{
    public void Request()
    {
        Console.WriteLine("Real Subject - Request");
    }
}

// Proxy
class Proxy : ISubject
{
    private RealSubject realSubject;

    public void Request()
    {
        if (realSubject == null)
        {
            Console.WriteLine("Proxy - Creating Real Subject");
            realSubject = new RealSubject();
        }

        Console.WriteLine("Proxy - Request");
        realSubject.Request();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Proxy
        ISubject proxy = new Proxy();
        proxy.Request();
        // Output:
        // Proxy - Creating Real Subject
        // Proxy - Request
        // Real Subject - Request
    }
}

In this example, the Proxy pattern is implemented with an ISubject interface (representing the subject or target object), a RealSubject class (implementing the real subject), and a Proxy class (implementing the proxy). The Proxy class controls access to the RealSubject and adds functionality before or after invoking the RealSubject's methods.

The scenario demonstrates how the client code interacts with the Proxy object, which in turn manages the creation and delegation of requests to the RealSubject. The proxy can control access, provide caching, or perform additional tasks while maintaining a consistent interface with the real subject.

By using the Proxy pattern, you can achieve various goals such as lazy initialization, access control, caching, logging, or providing a simplified interface to a complex system. It allows you to control the interaction with the real object while hiding its details from the client code.

Observer Pattern

The Observer pattern is a behavioral design pattern that defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically. Here's a comprehensive implementation of the Observer pattern in C# covering various scenarios:

using System;
using System.Collections.Generic;

// Observer
interface IObserver
{
    void Update(string message);
}

// Subject
interface ISubject
{
    void Attach(IObserver observer);
    void Detach(IObserver observer);
    void NotifyObservers(string message);
}

// Concrete Observer
class ConcreteObserver : IObserver
{
    private string name;

    public ConcreteObserver(string name)
    {
        this.name = name;
    }

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

// Concrete Subject
class ConcreteSubject : ISubject
{
    private List<IObserver> observers = new List<IObserver>();
    private string state;

    public void Attach(IObserver observer)
    {
        observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void SetState(string state)
    {
        this.state = state;
        NotifyObservers($"State changed to: {state}");
    }

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

class Program
{
    static void Main(string[] args)
    {
        // Scenario 1: Using Observer
        ConcreteSubject subject = new ConcreteSubject();
        ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
        ConcreteObserver observer2 = new ConcreteObserver("Observer 2");

        subject.Attach(observer1);
        subject.Attach(observer2);

        subject.SetState("State 1");
        // Output:
        // Observer 1 received message: State changed to: State 1
        // Observer 2 received message: State changed to: State 1

        subject.Detach(observer1);

        subject.SetState("State 2");
        // Output:
        // Observer 2 received message: State changed to: State 2
    }
}

In this example, the Observer pattern is implemented with an IObserver interface, an ISubject interface, a ConcreteObserver class (implementing observers), and a ConcreteSubject class (implementing the subject). The scenario demonstrates how observers can subscribe to a subject and be notified of its state changes.

The ConcreteSubject maintains a list of observers and notifies them when its state changes. Observers (ConcreteObserver) respond to notifications by updating themselves.

By using the Observer pattern, you can establish loose coupling between subjects and observers, allowing multiple objects to react to changes in the state of a subject. This pattern is particularly useful for implementing event handling, real-time data propagation, and scenarios where multiple parts of a system need to stay synchronized.