Design Patterns in C#

Design patterns are proven solutions to recurring problems in software design. Here, we’ll explore Adapter, Facade, Factory, Observer, Singleton, and Builder patterns, with explanations and examples in C#.


1. Adapter Pattern

Purpose:

Allows incompatible interfaces to work together by creating a bridge between them.

Example:

You want to use a LightningCable with a device that supports only USB.

Implementation:

// Target Interface
interface IUSB
{
    void ConnectWithUSB();
}

// Adaptee
class LightningCable
{
    public void ConnectWithLightning()
    {
        Console.WriteLine("Connected with Lightning");
    }
}

// Adapter
class LightningToUSBAdapter : IUSB
{
    private LightningCable _lightningCable;

    public LightningToUSBAdapter(LightningCable cable)
    {
        _lightningCable = cable;
    }

    public void ConnectWithUSB()
    {
        Console.WriteLine("Adapter in use");
        _lightningCable.ConnectWithLightning();
    }
}

// Usage
class Program
{
    static void Main()
    {
        IUSB usbDevice = new LightningToUSBAdapter(new LightningCable());
        usbDevice.ConnectWithUSB();
    }
}

2. Facade Pattern

Purpose:

Simplifies interaction with a complex subsystem by providing a unified interface.

Example:

A home theater system has multiple components like TV, speakers, and lights. A single method to start the movie simplifies user interaction.

Implementation:

class TV
{
    public void TurnOn()
    {
        Console.WriteLine("TV turned on");
    }
}

class Speakers
{
    public void SetVolume(int level)
    {
        Console.WriteLine($"Volume set to {level}");
    }
}

class Lights
{
    public void Dim()
    {
        Console.WriteLine("Lights dimmed");
    }
}

// Facade
class HomeTheaterFacade
{
    private TV _tv;
    private Speakers _speakers;
    private Lights _lights;

    public HomeTheaterFacade(TV tv, Speakers speakers, Lights lights)
    {
        _tv = tv;
        _speakers = speakers;
        _lights = lights;
    }

    public void WatchMovie()
    {
        _tv.TurnOn();
        _speakers.SetVolume(20);
        _lights.Dim();
        Console.WriteLine("Movie started!");
    }
}

// Usage
class Program
{
    static void Main()
    {
        var theater = new HomeTheaterFacade(new TV(), new Speakers(), new Lights());
        theater.WatchMovie();
    }
}

3. Factory Pattern

Purpose:

Provides an interface for creating objects without specifying their exact class.

Example:

A shape factory produces different shapes like Circle or Square based on input.

Implementation:

interface IShape
{
    void Draw();
}

class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing Circle");
    }
}

class Square : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing Square");
    }
}

class ShapeFactory
{
    public IShape GetShape(string shapeType)
    {
        if (shapeType.Equals("Circle", StringComparison.OrdinalIgnoreCase))
            return new Circle();
        else if (shapeType.Equals("Square", StringComparison.OrdinalIgnoreCase))
            return new Square();

        return null;
    }
}

// Usage
class Program
{
    static void Main()
    {
        var factory = new ShapeFactory();

        IShape shape1 = factory.GetShape("Circle");
        shape1.Draw();

        IShape shape2 = factory.GetShape("Square");
        shape2.Draw();
    }
}

4. Observer Pattern

Purpose:

Defines a one-to-many dependency between objects. When one object changes state, all dependents are notified.

Example:

A Channel notifies its subscribers when a new video is uploaded.

Implementation:

using System;
using System.Collections.Generic;

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

// Subject
class Channel
{
    private List<IObserver> subscribers = new List<IObserver>();

    public void Subscribe(IObserver observer)
    {
        subscribers.Add(observer);
    }

    public void UploadVideo(string title)
    {
        Console.WriteLine($"Channel uploaded: {title}");
        NotifySubscribers($"New video: {title}");
    }

    private void NotifySubscribers(string message)
    {
        foreach (var observer in subscribers)
        {
            observer.Update(message);
        }
    }
}

// Concrete Observer
class Subscriber : IObserver
{
    private string _name;

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

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

// Usage
class Program
{
    static void Main()
    {
        var channel = new Channel();

        var sub1 = new Subscriber("Alice");
        var sub2 = new Subscriber("Bob");

        channel.Subscribe(sub1);
        channel.Subscribe(sub2);

        channel.UploadVideo("Observer Pattern Tutorial");
    }
}

5. Singleton Pattern

Purpose:

Ensures a class has only one instance and provides a global point of access to it.

Implementation:

class Singleton
{
    private static Singleton _instance;

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

    public static Singleton GetInstance()
    {
        if (_instance == null)
        {
            _instance = new Singleton();
        }
        return _instance;
    }

    public void ShowMessage()
    {
        Console.WriteLine("Single instance used");
    }
}

// Usage
class Program
{
    static void Main()
    {
        var singleton1 = Singleton.GetInstance();
        singleton1.ShowMessage();

        var singleton2 = Singleton.GetInstance();
        Console.WriteLine(ReferenceEquals(singleton1, singleton2)); // Output: True
    }
}

6. Builder Pattern

Purpose:

Constructs complex objects step by step, allowing greater control over the creation process.

Example:

Building a Car with properties like engine, wheels, and color.

Implementation:

class Car
{
    public string Engine { get; private set; }
    public int Wheels { get; private set; }
    public string Color { get; private set; }

    private Car(Builder builder)
    {
        Engine = builder.Engine;
        Wheels = builder.Wheels;
        Color = builder.Color;
    }

    public class Builder
    {
        public string Engine { get; private set; }
        public int Wheels { get; private set; }
        public string Color { get; private set; }

        public Builder SetEngine(string engine)
        {
            Engine = engine;
            return this;
        }

        public Builder SetWheels(int wheels)
        {
            Wheels = wheels;
            return this;
        }

        public Builder SetColor(string color)
        {
            Color = color;
            return this;
        }

        public Car Build()
        {
            return new Car(this);
        }
    }

    public override string ToString()
    {
        return $"Car [Engine={Engine}, Wheels={Wheels}, Color={Color}]";
    }
}

// Usage
class Program
{
    static void Main()
    {
        var car = new Car.Builder()
            .SetEngine("V8")
            .SetWheels(4)
            .SetColor("Red")
            .Build();

        Console.WriteLine(car); //