C# Programming Design Patterns Simplified
-
C# programming language has become one of the most popular languages for building powerful and robust applications. One of the reasons behind its success is the ability to implement design patterns effectively. In this post, we will delve into the world of C# design patterns, simplifying them and providing you with practical examples. By the end of this post, you will have a solid understanding of these design patterns and how they can be applied in your C# programming projects.
Objectives of this post
-
Understand the basics of C# programming
-
Learn about various design patterns used in software development
-
Implement these design patterns in C# code
Basics of C# Programming
Before diving into the design patterns, let's review some essential C# programming concepts.
Variables and data types
In C#, you declare variables using the following syntax:
type variableName = initialValue;
Some common data types in C# are:
-
int
for integers -
float
for floating-point numbers -
double
for double-precision floating-point numbers -
char
for characters -
string
for text
Control structures
Control structures are used to control the flow of your program. Some common control structures in C# are:
if
statement:
if (condition) { // Execute this block if the condition is true }
switch
statement:
switch (variable) { case value1: // Execute this block if the variable equals value1 break; case value2: // Execute this block if the variable equals value2 break; default: // Execute this block if the variable doesn't match any of the cases break; }
Functions and methods
Functions are blocks of code that perform a specific task and can be called from other parts of your code. In C#, functions are called methods and can be defined inside classes. Here's an example of a simple method:
public int Add(int a, int b) { return a + b; }
Classes and objects
Classes are blueprints for creating objects, which are instances of a class. A class can have properties and methods. Here's an example of a class with a property and a method:
public class Dog { public string Name { get; set; } public void Bark() { Console.WriteLine("Woof!"); } }
Inheritance and polymorphism
Inheritance allows you to create a new class that inherits the properties and methods of an existing class. Polymorphism enables you to use a derived class as if it were the base class, allowing for code reusability and flexibility. Here's an example:
public class Animal { public virtual void MakeSound() { Console.WriteLine("The animal makes a sound"); } } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("The dog barks"); } }
Interfaces and abstract classes
Interfaces and abstract classes are used to define a contract that classes must implement. Interfaces can't contain any implementation, while abstract classes can have both abstract and non-abstract members. Here's an example:
public interface IFlyable { void Fly(); } public abstract class Bird : IFlyable { public abstract void MakeSound(); public void Fly() { Console.WriteLine("The bird flies"); } }
Creational Design Patterns
Creational design patterns deal with object creation mechanisms, trying to create objects in a way that is suitable for the situation.
Singleton
-
Definition and use cases: Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when you need to control access to shared resources, like a database connection or a configuration object.
-
Implementation in C#:
public class Singleton { private static Singleton _instance; private Singleton() { } public static Singleton Instance { get { if (_instance == null) { _instance = new Singleton(); } return _instance; } } }
Factory Method
-
Definition and use cases: Factory Method pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. This pattern is useful when you need to create objects of different types based on some input parameters.
-
Implementation in C#:
public abstract class Animal { public abstract void Speak(); } public class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } } public class Cat : Animal { public override void Speak() { Console.WriteLine("Meow!"); } } public static class AnimalFactory { public static Animal CreateAnimal(string type) { switch (type) { case "Dog": return new Dog(); case "Cat": return new Cat(); default: throw new ArgumentException("Invalid animal type"); } } }
Abstract Factory
-
Definition and use cases: Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is useful when you need to create objects that work together, like UI components for different platforms or database connections for different providers.
-
Implementation in C#:
public interface IAnimalFactory { IDog CreateDog(); ICat CreateCat(); } public class PetAnimalFactory : IAnimalFactory { public IDog CreateDog() { return new PetDog(); } public ICat CreateCat() { return new PetCat(); } } public class WildAnimalFactory : IAnimalFactory { public IDog CreateDog() { return new WildDog(); } public ICat CreateCat() { return new WildCat(); } }
Builder
-
Definition and use cases: Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is useful when you need to create complex objects with many optional or varying parts, like a car or a pizza.
-
Implementation in C#:
public class Pizza { public string Dough { get; set; } public string Sauce { get; set; } public string Toppings { get; set; } } public interface IPizzaBuilder { void BuildDough(); void BuildSauce(); void BuildToppings(); Pizza GetPizza(); } public class MargheritaPizzaBuilder : IPizzaBuilder { private Pizza _pizza = new Pizza(); public void BuildDough() { _pizza.Dough = "Thin crust"; } public void BuildSauce() { _pizza.Sauce = "Tomato"; } public void BuildToppings() { _pizza.Toppings = "Mozzarella"; } public Pizza GetPizza() { return _pizza; } } public class PizzaDirector { public void ConstructPizza(IPizzaBuilder pizzaBuilder) { pizzaBuilder.BuildDough(); pizzaBuilder.BuildSauce(); pizzaBuilder.BuildToppings(); } }
Prototype
-
Definition and use cases: Prototype pattern specifies the kind of objects to create using a prototypical instance and creates new objects by copying this prototype. This pattern is useful when you need to create many instances of a class that are slightly different from each other, like game objects or document templates.
-
Implementation in C#:
public abstract class Animal : ICloneable { public string Name { get; set; } public abstract object Clone(); } public class Dog : Animal { public override object Clone() { return this.MemberwiseClone(); } } public class Cat : Animal { public override object Clone() { return this.MemberwiseClone(); } }
Structural Design Patterns
Structural design patterns are concerned with class and object composition, defining ways to compose objects to obtain new functionality.
Adapter
-
Definition and use cases: Adapter pattern converts the interface of a class into another interface that clients expect. This pattern is useful when you need to make two incompatible interfaces work together, like integrating third-party libraries or legacy systems.
-
Implementation in C#:
public interface ITarget { void Request(); } public class Adaptee { public void SpecificRequest() { Console.WriteLine("Called SpecificRequest()"); } } public class Adapter : ITarget { private readonly Adaptee _adaptee; public Adapter(Adaptee adaptee) { _adaptee = adaptee; } public void Request() { _adaptee.SpecificRequest(); } }
Bridge
-
Definition and use cases: Bridge pattern decouples an abstraction from its implementation, allowing the two to vary independently. This pattern is useful when you need to support multiple implementations of an interface or when you want to separate the platform-specific code from the platform-independent code.
-
Implementation in C#:
public interface IRenderer { void Render(string text); } public class ConsoleRenderer : IRenderer { public void Render(string text) { Console.WriteLine(text); } } public class HtmlRenderer : IRenderer { public void Render(string text) { Console.WriteLine($"<p>{text}</p>"); } } public abstract class Text { protected IRenderer Renderer; public Text(IRenderer renderer) { Renderer = renderer; } public abstract void Display(); } public class PlainText : Text { public PlainText(IRenderer renderer) : base(renderer) { } public override void Display() { Renderer.Render("Plain text"); } } public class BoldText : Text { public BoldText(IRenderer renderer) : base(renderer) { } public override void Display() { Renderer.Render("**Bold text**"); } }
Composite
-
Definition and use cases: Composite pattern composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions of objects uniformly. This pattern is useful when you need to build complex structures from simpler components, like a file system or a GUI.
-
Implementation in C#:
public abstract class Component { public abstract void Operation(); } public class Leaf : Component { public override void Operation() { Console.WriteLine("Leaf operation"); } } public class Composite : Component { private readonly List<Component> _children = new List<Component>(); public void Add(Component component) { _children.Add(component); } public void Remove(Component component) { _children.Remove(component); } public override void Operation() { Console.WriteLine("Composite operation"); foreach (var child in _children) { child.Operation(); } } }
Decorator
-
Definition and use cases: Decorator pattern dynamically adds new responsibilities to an object without changing its interface, providing a flexible alternative to subclassing. This pattern is useful when you need to extend an object's behavior at runtime, like adding logging or caching to a class.
-
Implementation in C#:
public interface IComponent { void Operation(); } public class ConcreteComponent : IComponent { public void Operation() { Console.WriteLine("ConcreteComponent operation"); } } public abstract class Decorator : IComponent { protected readonly IComponent Component; protected Decorator(IComponent component) { Component = component; } public abstract void Operation(); } public class ConcreteDecoratorA : Decorator { public ConcreteDecoratorA(IComponent component) : base(component) { } public override void Operation() { Console.WriteLine("Decorator A"); Component.Operation(); } } public class ConcreteDecoratorB : Decorator { public ConcreteDecoratorB(IComponent component) : base(component) { } public override void Operation() { Console.WriteLine("Decorator B"); Component.Operation(); } }
Facade
-
Definition and use cases: Facade pattern provides a unified interface to a set of interfaces in a subsystem, making the subsystem easier to use. This pattern is useful when you need to simplify the interaction with a complex system, like a compiler or an e-commerce platform.
-
Implementation in C#:
public class SubsystemA { public void OperationA() { Console.WriteLine("Subsystem A operation"); } } public class SubsystemB { public void OperationB() { Console.WriteLine("Subsystem B operation"); } } public class Facade { private readonly SubsystemA _subsystemA; private readonly SubsystemB _subsystemB; public Facade(SubsystemA subsystemA, SubsystemB subsystemB) { _subsystemA = subsystemA; _subsystemB = subsystemB; } public void UnifiedOperation() { _subsystemA.OperationA(); _subsystemB.OperationB(); } }
Flyweight
-
Definition and use cases: Flyweight pattern uses sharing to support a large number of fine-grained objects efficiently, reducing the memory footprint of your application. This pattern is useful when you need to create many instances of a class with a lot of shared state, like text characters or game objects.
-
Implementation in C#:
public class Flyweight { private readonly string _sharedState; public Flyweight(string sharedState) { _sharedState = sharedState; } public void Operation(string uniqueState) { Console.WriteLine($"Shared state: {_sharedState}, unique state: {uniqueState}"); } } public class FlyweightFactory { private readonly Dictionary<string, Flyweight> _flyweights = new Dictionary<string, Flyweight>(); public Flyweight GetFlyweight(string key) { if (!_flyweights.ContainsKey(key)) { _flyweights[key] = new Flyweight(key); } return _flyweights[key]; } }
Proxy
-
Definition and use cases: Proxy pattern provides a surrogate or placeholder for another object to control access to it. This pattern is useful when you need to add some functionality to an object without changing its interface, like access control or lazy loading.
-
Implementation in C#:
public interface ISubject { void Request(); } public class RealSubject : ISubject { public void Request() { Console.WriteLine("RealSubject request"); } } public class Proxy : ISubject { private readonly RealSubject _realSubject; public Proxy(RealSubject realSubject) { _realSubject = realSubject; } public void Request() { Console.WriteLine("Proxy request"); _realSubject.Request(); } }
Behavioral Design Patterns
Behavioral design patterns define the ways in which objects communicate with one another and how they cooperate.
Chain of Responsibility
-
Definition and use cases: Chain of Responsibility pattern avoids coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. This pattern is useful when you need to process a request through a series of handlers, like error handling or input validation.
-
Implementation in C#:
public abstract class Handler { protected Handler Successor; public void SetSuccessor(Handler successor) { Successor = successor; } public abstract void HandleRequest(int request); } public class ConcreteHandler1 : Handler { public override void HandleRequest(int request) { if (request >= 0 && request < 10) { Console.WriteLine($"{this.GetType().Name} handled request {request}"); } else if (Successor != null) { Successor.HandleRequest(request); } } } public class ConcreteHandler2 : Handler { public override void HandleRequest(int request) { if (request >= 10 && request < 20) { Console.WriteLine($"{this.GetType().Name} handled request {request}"); } else if (Successor != null) { Successor.HandleRequest(request); } } }
Command
-
Definition and use cases: Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. This pattern is useful when you need to decouple the sender of a request from the receiver, like menu items or button actions.
-
Implementation in C#:
public interface ICommand { void Execute(); } public class ConcreteCommand : ICommand { private readonly Receiver _receiver; public ConcreteCommand(Receiver receiver) { _receiver = receiver; } public void Execute() { _receiver.Action(); } } public class Receiver { public void Action() { Console.WriteLine("Receiver action"); } } public class Invoker { private ICommand _command; public void SetCommand(ICommand command) { _command = command; } public void ExecuteCommand() { _command.Execute(); } }
Interpreter
-
Definition and use cases: Interpreter pattern defines a representation for a language's grammar and provides an interpreter to evaluate sentences in the language. This pattern is useful when you need to implement a domain-specific language or parse complex expressions, like arithmetic formulas or SQL queries.
-
Implementation in C#:
public abstract class Expression { public abstract int Interpret(Dictionary<string, int> context); } public class TerminalExpression : Expression { private readonly string _variableName; public TerminalExpression(string variableName) { _variableName = variableName; } public override int Interpret(Dictionary<string, int> context) { return context[_variableName]; } } public class AddExpression : Expression { private readonly Expression _leftExpression; private readonly Expression _rightExpression; public AddExpression(Expression left, Expression right) { _leftExpression = left; _rightExpression = right; } public override int Interpret(Dictionary<string, int> context) { return _leftExpression.Interpret(context) + _rightExpression.Interpret(context); } }
Iterator
-
Definition and use cases: Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. This pattern is useful when you need to traverse a collection of objects without knowing their specific implementation, like a list or a tree.
-
Implementation in C#:
public interface IIterator { object First(); object Next(); bool IsDone(); object CurrentItem(); } public interface IAggregate { IIterator CreateIterator(); } public class ConcreteAggregate : IAggregate { privatepublic class ConcreteIterator : IIterator { private readonly ConcreteAggregate _aggregate; private int _current; public ConcreteIterator(ConcreteAggregate aggregate) { _aggregate = aggregate; } public object First() { return _aggregate[0]; } public object Next() { _current++; return IsDone() ? null : _aggregate[_current]; } public bool IsDone() { return _current >= _aggregate.Count; } public object CurrentItem() { return _aggregate[_current]; } }
Mediator
-
Definition and use cases: Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly. This pattern is useful when you need to coordinate the behavior of a group of objects, like user interface components or networked peers.
-
Implementation in C#:
public interface IMediator { void Send(string message, Colleague colleague); } public abstract class Colleague { protected readonly IMediator Mediator; protected Colleague(IMediator mediator) { Mediator = mediator; } public abstract void Send(string message); public abstract void Notify(string message); } public class ConcreteMediator : IMediator { private Colleague _colleague1; private Colleague _colleague2; public void SetColleague1(Colleague colleague) { _colleague1 = colleague; } public void SetColleague2(Colleague colleague) { _colleague2 = colleague; } public void Send(string message, Colleague colleague) { if (colleague == _colleague1) { _colleague2.Notify(message); } else { _colleague1.Notify(message); } } } public class ConcreteColleague1 : Colleague { public ConcreteColleague1(IMediator mediator) : base(mediator) { } public override void Send(string message) { Mediator.Send(message, this); } public override void Notify(string message) { Console.WriteLine($"Colleague1 receives message: {message}"); } } public class ConcreteColleague2 : Colleague { public ConcreteColleague2(IMediator mediator) : base(mediator) { } public override void Send(string message) { Mediator.Send(message, this); } public override void Notify(string message) { Console.WriteLine($"Colleague2 receives message: {message}"); } }
Memento
-
Definition and use cases: Memento pattern captures and externalizes an object's internal state, allowing the object to be restored to this state later. This pattern is useful when you need to implement undoable operations, like text editing or game state management.
-
Implementation in C#:
public class Originator { public string State { get; set; } public Memento CreateMemento() { return new Memento(State); } public void SetMemento(Memento memento) { State = memento.State; } } public class Memento { public string State { get; } public Memento(string state) { State = state; } } public class Caretaker { private Memento _memento; public void SaveState(Originator originator) { _memento = originator.CreateMemento(); } public void RestoreState(Originator originator) { originator.SetMemento(_memento); } }
Observer
-
Definition and use cases: 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. This pattern is useful when you need to maintain consistency between related objects, like data bindings or event handling.
-
Implementation in C#:
public interface IObserver { void Update(); } public interface ISubject { void Attach(IObserver observer); void Detach(IObserver observer); void Notify(); } public class ConcreteSubject : ISubject { private readonly List<IObserver> _observers = new List<IObserver>(); public int State { get; set; } public void Attach(IObserver observer) { _observers.Add(observer); } public void Detach(IObserver observer) { _observers.Remove(observer); } public void Notify() { foreach (var observer in _observers) { observer.Update(); } } } public class ConcreteObserver : IObserver { private readonly ConcreteSubject _subject; public ConcreteObserver(ConcreteSubject subject) { _subject = subject; } public void Update() { Console.WriteLine($"Observer updated with new state: {_subject.State}"); } }
State
-
Definition and use cases: State pattern allows an object to alter its behavior when its internal state changes, appearing to change its class. This pattern is useful when you need to implement state-dependent behavior, like a state machine or a TCP connection.
-
Implementation in C#:
public interface IState { void Handle(Context context); } public class ConcreteStateA : IState { public void Handle(Context context) { context.State = new ConcreteStateB(); } } public class ConcreteStateB : IState { public void Handle(Context context) { context.State = new ConcreteStateA(); } } public class Context { public IState State { get; set; } public Context(IState state) { State = state; } public void Request() { State.Handle(this); } }
Strategy
-
Definition and use cases: Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from clients that use it. This pattern is useful when you need to implement different variations of an algorithm, like sorting or compression methods.
-
Implementation in C#:
public interface IStrategy { void Algorithm(); } public class ConcreteStrategyA : IStrategy { public void Algorithm() { Console.WriteLine("Strategy A"); } } public class ConcreteStrategyB : IStrategy { public void Algorithm() { Console.WriteLine("Strategy B"); } } public class Context { private IStrategy _strategy; public Context(IStrategy strategy) { _strategy = strategy; } public void SetStrategy(IStrategy strategy) { _strategy = strategy; } public void ExecuteStrategy() { _strategy.Algorithm(); } }
Template Method
-
Definition and use cases: Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. This pattern is useful when you need to implement the invariant parts of an algorithm once and leave the variant parts to be overridden by subclasses, like a parser or a framework.
-
Implementation in C#:
public abstract class AbstractClass { public void TemplateMethod() { PrimitiveOperation1(); PrimitiveOperation2(); } public abstract void PrimitiveOperation1(); public abstract void PrimitiveOperation2(); } public class ConcreteClassA : AbstractClass { public override void PrimitiveOperation1() { Console.WriteLine("ConcreteClassA operation 1"); } public override void PrimitiveOperation2() { Console.WriteLine("ConcreteClassA operation 2"); } } public class ConcreteClassB : AbstractClass { public override void PrimitiveOperation1() { Console.WriteLine("ConcreteClassB operation 1"); } public override void PrimitiveOperation2() { Console.WriteLine("ConcreteClassB operation 2"); } }
Visitor
-
Definition and use cases: Visitor pattern represents an operation to be performed on the elements of an object structure without changing the classes on which it operates. This pattern is useful when you need to define new operations on a set of objects without changing their implementation, like rendering or serialization.
-
Implementation in C#:
public interface IElement { void Accept(IVisitor visitor); } public class ConcreteElementA : IElement { public void Accept(IVisitor visitor) { visitor.VisitConcreteElementA(this); } public void OperationA() { Console.WriteLine("ConcreteElementA operation"); } } public class ConcreteElementB : IElement { public void Accept(IVisitor visitor) { visitor.VisitConcreteElementB(this); } public void OperationB() { Console.WriteLine("ConcreteElementB operation"); } } public interface IVisitor { void VisitConcreteElementA(ConcreteElementA concreteElementA); void VisitConcreteElementB(ConcreteElementB concreteElementB); } public class ConcreteVisitor1 : IVisitor { public void VisitConcreteElementA(ConcreteElementA concreteElementA) { concreteElementA.OperationA(); } public void VisitConcreteElementB(ConcreteElementB concreteElementB) { concreteElementB.OperationB(); } } public class ConcreteVisitor2 : IVisitor { public void VisitConcreteElementA(ConcreteElementA concreteElementA) { Console.WriteLine("ConcreteVisitor2 visited ConcreteElementA"); } public void VisitConcreteElementB(ConcreteElementB concreteElementB) { Console.WriteLine("ConcreteVisitor2 visited ConcreteElementB"); } }
Conclusion
In this post, we have covered a wide range of design patterns used in C# programming. Understanding and applying these design patterns can help you write more efficient, flexible, and maintainable code.
Benefits of using design patterns in C# programming
-
Improve code quality and maintainability
-
Promote code reusability and modularity
-
Enhance communication between team members by providing a common vocabulary
Further resources and learning materials
-
Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
-
Head First Design Patterns by Eric Freeman, Elisabeth Robson, Bert Bates, and Kathy Sierra
-
C# Design Patterns: A Tutorial by James W. Cooper
-