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); //