Design Patterns in C#

Design patterns are tools that we, as programmers, invented to solve common problems when developing software. They are literally like recipes to follow that help us create a more robust and easy-to-maintain system.

Why are design patterns important?

How many times have you faced the following situation: “Oh man, this design sucks. Surely, someone had to think of a way to resolve it before?” Well, there you go, design patterns are these ways other programmers did. They make our code more understandable, reusable, and mistake-proof. Just like a skillful craftsman uses the right tool on the right job, a good programmer applies the right design pattern.

Creational Design Patterns

1. Singleton Pattern

Singleton pattern, also can be described in terms of a golden ticket. It means that only one instance of the class will occur in the entire system . In this case, when exactly one instance is needed, Singleton is appropriate. Builder pattern, in turn, can be compared with the assembly of a jigsaw puzzle.

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

    Singleton()
    {
    }

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

2. Factory Pattern

The Factory pattern is for when you need a factory that produces different types. It is your method of object creation without the details of it. Factory is like a helping handle for the creation.

public abstract class Animal
{
    public abstract string Speak();
}

public class Dog : Animal
{
    public override string Speak()
    {
        return "Woof!";
    }
}

public class Cat : Animal
{
    public override string Speak()
    {
        return "Meow!";
    }
}

public class AnimalFactory
{
    public Animal GetAnimal(string AnimalType)
    {
        switch (AnimalType)
        {
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
            default:
                throw new Exception("Invalid animal type");
        }
    }
}

3. Builder Pattern

Builder is a pattern that abandons abstract constructors and constructors, allowing complex objects to be created step by step. In this case, when object creativity has a lot of parts, it just works.

public class Car
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
}

public class CarBuilder
{
    private Car _car;

    public CarBuilder()
    {
        _car = new Car();
    }

    public CarBuilder SetMake(string make)
    {
        _car.Make = make;
        return this;
    }

    public CarBuilder SetModel(string model)
    {
        _car.Model = model;
        return this;
    }

    public CarBuilder SetYear(int year)
    {
        _car.Year = year;
        return this;
    }

    public Car Build()
    {
        return _car;
    }
}

4. Dependency Injection Pattern

The Dependency Injection pattern when you want to change things but not the result. Picture bringing your hot sauce to a pizza spot. You want to eat a pizza, but you want to add hot sauce. A dose of hot sauce is dependency injection.

public interface IMessage
{
    void SendMessage(string message);
}

public class Email : IMessage
{
    public void SendMessage(string message)
    {
        Console.WriteLine("Email message: " + message);
    }
}

public class SMS : IMessage
{
    public void SendMessage(string message)
    {
        Console.WriteLine("SMS message: " + message);
    }
}

public class Notification
{
    private IMessage _message;

    public Notification(IMessage message)
    {
        this._message = message;
    }

    public void Notify(string message)
    {
        _message.SendMessage(message);
    }
}

Structural Design Patterns

1. Adapter Pattern

An Adapter pattern which is like the one when you need to plug the device in the old socket. Just as the Socket and Device weren’t designed at one point for the other earlier, you wish now to do them one.

// Existing way requests are implemented
public class Adaptee
{
    public double SpecificRequest(double a, double b)
    {
        return a / b;
    }
}

// New standard for requests
public interface ITarget
{
    string Request(int i);
}

// Implementing the new standard in terms of the old
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;

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

    public string Request(int i)
    {
        return "Rough estimate is " + (int)Math.Round(_adaptee.SpecificRequest(i, 3));
    }
}

2. Decorator Pattern

The Decorator pattern is like adding toppings to ice cream. It allows you to add functionality to an object without modifying its structure. It's useful when you want to add features to an object flexibly.

public abstract class Coffee
{
    public abstract double GetCost(); // Returns the cost of the coffee
    public abstract string GetIngredients(); // Returns the ingredients of the coffee
}

public class SimpleCoffee : Coffee
{
    public override double GetCost()
    {
        return 1;
    }

    public override string GetIngredients()
    {
        return "Coffee";
    }
}

public class MilkCoffee : Coffee
{
    protected Coffee coffee;

    public MilkCoffee(Coffee coffee)
    {
        this.coffee = coffee;
    }

    public override double GetCost()
    {
        return coffee.GetCost() + 0.5;
    }

    public override string GetIngredients()
    {
        return coffee.GetIngredients() + ", Milk";
    }
}

3. Facade Pattern

The Facade pattern is like using an app instead of directly interacting with the operating system. It provides a simpler interface for working with complex systems.

public class SubSystemOne
{
    public void MethodOne()
    {
        Console.WriteLine(" SubSystemOne Method");
    }
}

public class SubSystemTwo
{
    public void MethodTwo()
    {
        Console.WriteLine(" SubSystemTwo Method");
    }
}

public class SubSystemThree
{
    public void MethodThree()
    {
        Console.WriteLine(" SubSystemThree Method");
    }
}

public class Facade
{
    private SubSystemOne one;
    private SubSystemTwo two;
    private SubSystemThree three;

    public Facade()
    {
        one = new SubSystemOne();
        two = new SubSystemTwo();
        three = new SubSystemThree();
    }

    public void MethodA()
    {
        Console.WriteLine("\nMethodA() ---- ");
        one.MethodOne();
        two.MethodTwo();
    }

    public void MethodB()
    {
        Console.WriteLine("\nMethodB() ---- ");
        two.MethodTwo();
        three.MethodThree();
    }
}

4. Bridge Pattern

The Bridge pattern is like using a remote control to control your TV. It allows you to separate the abstraction from its implementation. It's useful when you want to change the implementation without affecting the abstraction.

public interface IImplementation
{
    string OperationImplementation();
}

public class Abstraction
{
    protected

 IImplementation implementation;

    public Abstraction(IImplementation implementation)
    {
        this.implementation = implementation;
    }

    public virtual string Operation()
    {
        return "Abstract: Base operation with:\n" +
            implementation.OperationImplementation();
    }
}

public class ConcreteImplementationA : IImplementation
{
    public string OperationImplementation()
    {
        return "ConcreteImplementationA: Here's the result on the platform A.";
    }
}

public class ConcreteImplementationB : IImplementation
{
    public string OperationImplementation()
    {
        return "ConcreteImplementationB: Here's the result on the platform B.";
    }
}

Behavioral Design Patterns

1. Observer Pattern

The Observer pattern is like subscribing to updates of your favorite TV show. It automatically notifies you when there are changes. It's useful when you need to keep different parts of your system synchronized.

public interface IObserver
{
    void Update(int i);
}

public interface ISubject
{
    void Register(IObserver o);
    void Unregister(IObserver o);
    void NotifyRegisteredUsers(int i);
}

public class Subject : ISubject
{
    List<IObserver> UserList = new List<IObserver>();

    public void NotifyRegisteredUsers(int i)
    {
        foreach (var observer in UserList)
        {
            observer.Update(i);
        }
    }

    public void Register(IObserver o)
    {
        UserList.Add(o);
    }

    public void Unregister(IObserver o)
    {
        UserList.Remove(o);
    }
}

public class Observer : IObserver
{
    public void Update(int i)
    {
        Console.WriteLine("Flag value changed to :" + i);
    }
}

2. Command Pattern

The Command pattern is like ordering food at a restaurant. You give the waiter an order, and he takes care of the rest. It's useful when you want to decouple the requester of an action from the one who performs it.

public interface ICommand
{
    void Execute();
}

public class Receiver
{
    public void Action()
    {
        Console.WriteLine("Receiver Action");
    }
}

public class ConcreteCommand : ICommand
{
    private Receiver receiver;

    public ConcreteCommand(Receiver receiver)
    {
        this.receiver = receiver;
    }

    public void Execute()
    {
        receiver.Action();
    }
}

public class Invoker
{
    private ICommand command;

    public void SetCommand(ICommand command)
    {
        this.command = command;
    }

    public void ExecuteCommand()
    {
        command.Execute();
    }
}

3. Strategy Pattern

The Strategy pattern is like having different routes to reach the same destination. It allows you to choose the best option for each situation. It's useful when you have multiple ways to perform a task and want to choose the most suitable one at runtime.

public interface IStrategy
{
    int DoOperation(int num1, int num2);
}

public class OperationAdd : IStrategy
{
    public int DoOperation(int num1, int num2)
    {
        return num1 + num2;
    }
}

public class OperationSubstract : IStrategy
{
    public int DoOperation(int num1, int num2)
    {
        return num1 - num2;
    }
}

public class OperationMultiply : IStrategy
{
    public int DoOperation(int num1, int num2)
    {
        return num1 * num2;
    }
}

public class Context
{
    private IStrategy strategy;

    public Context(IStrategy strategy)
    {
        this.strategy = strategy;
    }

    public int ExecuteStrategy(int num1, int num2)
    {
        return strategy.DoOperation(num1, num2);
    }
}

Conclusion

Design patterns are tools in developer’s toolbox: they make it easier for you to build better software faster. They allow you to write more readable, understandable, and efficient code. On the other hand, design patterns are not silver bullet: some of them can look quite complex. You should learn patterns to better use them – which ones suit for you and when to use. Don’t get bored continuously experimenting and learning, in the end you’ll see how your skills improve throughout the years! If you are interested in this topic and want to study more deeply, try to read a book of Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides “Design Patterns: Elements of Reusable Object-Oriented Software”. Also, you can find valuable information on C# Design Patterns at C# Corner.

I also leave you my github repository, with more detailed examples where you can find each pattern. https://github.com/AdrianBailador/DesignPattners

DevelopmentDesign PatternsDevelopersCsharpDotnet
Avatar for Adrián Bailador

Written by Adrián Bailador

🚀 Full-Stack Dev 👨🏻‍💻 .NET Engineer 👾 Geek & Friki 💡 Talks about #dotnet, #csharp, #azure, #visualstudio and a little bit of #nextjs.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.