C# Strategy Pattern Tutorial: 3 Reasons to Put It In Action


There are thousands of ways to implement a feature in your application.

However, during implementation, you might run into a problem. A problem that occurs in many other applications.

Fortunately, there is a solution for recurring code problems. Design patterns. The design patterns serve as a blueprint for developing software more efficiently and faster.

So what do you do when you need to change the algorithm at runtime? Use the Strategy design pattern.

The Strategy design pattern is a behavioral pattern that allows an algorithm to be selected at runtime. This is useful when an application uses different strategies for different objects or when the strategy needs to be dynamically selected at runtime based on input data.

The Strategy design pattern is also known as the policy design pattern.

This article presents the Strategy pattern in C#. I will also describe how to use it to solve some real-life problems.

What is the Strategy design pattern in C#?

Efficient design is the core of object-oriented programming (OOP). Poor strategic planning creates messy applications that are difficult to improve. By planning how your programs will work, it becomes easier to maintain your code.

Naturally, this need for adequately designed programs is one of the core reasons the patterns laid out by the Gang of Four (GoF) have quickly become the beating heart of OOP. With these patterns, you can write sleek, easily maintainable, highly reusable code.

Discovering various ways to solve a single problem when writing applications and programs is normal. However, while you may have a preferred solution, you may also want to make other options available to the client.

The Strategy design pattern allows you to define and encapsulate a family of algorithms that you can use interchangeably.

A real-life example of a problem that the Strategy pattern can solve

gym weightlifting

As an illustration, picture yourself building an application for a subscription-based business, such as a local gym.

So, your gym has kickstarts with only one tier for which all gym members pay a single price. But, you may quickly find that as your business grows, you can no longer afford to accommodate all members pouring through your gates simultaneously.

To naturally keep up with the increased demand, you create a new off-peak tier allowing other subscribing members to come only between 2 PM and 4 PM. But, unfortunately, particular gym members approach you after some time, demanding a personal trainer. So, you make plans to add this to your application.

However, while you are implementing this, a member approaches your front-desk enquiring if they can make their payments each year rather than on a month-by-month basis.

Although you realize that these new features will help you increase your revenue and make your application a fantastic experience, it does not remove the headache of adding these new plans. This pain is because each newly added subscription causes your program length to double.

Eventually, neither you nor other technical team members can understand your code.

Solving your problem with the Strategy design pattern

Although the subscription illustration may seem dramatic, developers often face similar issues because of poor design. Fortunately, using the Strategy pattern is one of the most effective ways of tackling such problems.

In such cases, once you discover that you may need to separate the peak and off-peak members, you take both algorithms for each membership and place them in a separate strategies class. Then, you store a reference to one of these memberships in the original subscription class, which you often refer to as the context class.

You can outsource the subscription selection process to various clients through strategy separation. So, if a customer wants a personal trainer membership, they can select the reference for that strategy, and the context will now perform all necessary actions.

What type of design pattern is the Strategy?

Design patterns are important for developers because they can help solve common programming problems.

There are three main types of design patterns:

  • Creational – Creational design patterns help with the creation of objects.
  • Structural – Structural design patterns help with the organization of objects.
  • Behavioral – behavioral design patterns help with the communication between objects.

Design patterns are not specific to any one programming language, but they are often implemented differently depending on the language. However, understanding the concepts behind design patterns is more important than knowing the specifics of how to implement them.

The Strategy design pattern is a behavior pattern that allows an algorithm or strategy to be selected at runtime. This pattern is useful when changing an object’s behavior dynamically.

Other behavioral patterns are:

  • Chain of Responsibility – The Chain of Responsibility design pattern is a software design pattern in which an object is passed along a chain of objects until it reaches the object that can handle the request.
  • Command – The Command design pattern is a behavioral software design pattern that encapsulates a request as an object, allowing for the parameterization and separation of the request from the object that executes it. Commands can be stored in a history list, allowing for undo/redo functionality.
  • Interpreter – The Interpreter design pattern is a programming technique used to evaluate sentences in a language.
  • Iterator – The Iterator design pattern is a design pattern that allows for the encapsulation of the logic for traversing a data structure into a separate class, called an iterator. It allows the data structure to be traversed in a variety of ways without the need to modify the underlying data structure.
  • Mediator – The Mediator design pattern provides a centralized communication channel between components in a system. It allows for loosely coupled communication between components, making the system more scalable and easier to maintain.
  • Memento – The Memento design pattern is a software design pattern that provides the ability to save and restore the state of an object. The Memento pattern stores the state of an object in a memento object. You can use that object to restore the state of the object at a later time.
  • Observer – The Observer design pattern is used to maintain a record of changes to an object’s state and notify registered observers when that state changes.
  • State – State design pattern is a software design pattern that enables an object to alter its behavior when its internal state changes. This pattern is used in software development to encapsulate the behavior of an object so that you can change it easily.
  • Template Method – The Template Method design pattern is a way to define the skeleton of an algorithm in a base class but to let subclasses override specific steps of the algorithm.
  • Visitor – The Visitor design pattern is an object-oriented design pattern in which an object, called a visitor, is used to modify the internal state of an object, called an element, without changing the element’s interface.

Why is it called a Strategy pattern?

A strategy is a template or general plan. For example, to win in chess, you must have a detailed strategy to defeat the opponent.

A strategy is often used to determine the best course of action when faced with uncertainty, mainly when the outcome depends on multiple factors, such as deciding which route to take to school or how to solve a mathematical equation.

The Gang of Four introduced the name “Strategy” in their book Design Patterns. In this context, a strategy is a plan, a set of rules, or an algorithm used to achieve the desired outcome. The Strategy refers to choosing the proper behavior to solve a problem at runtime.

When to use the Strategy design pattern?

Use the Strategy pattern when:

  • Many related classes differentiate only in behavior. You have one common interface but different classes with different behavior.
  • You need different variations of an algorithm.
  • An algorithm uses data that should be hidden from clients.

Finally, use this pattern when a class defines many behaviors, representing them as conditional statements.

Instead of having many conditionals in your code, you can move conditional branches to separate strategy classes. You will see how to eliminate conditional statements using the Strategy pattern later in the article.

How to implement the Strategy design pattern in C# – structural code

The strategy design pattern generally pivots around three core parts:

  1. Context – maintains the reference to a Strategy object.
  2. Strategy – defines an interface common to all strategy implementations.
  3. ConcreteStrategy – implements the Strategy interface.
Strategy pattern – UML diagram

Using these, we can dive into how to implement this design pattern structurally in C#.

To implement the Strategy design pattern, follow the next steps.

Start by defining the Strategy interface:

public interface InterfaceStrategy
{
    IEnumerable<string> PerformAlgorithm(List<string> list);
}

Next, implement the Strategy interface:

class DefaultConcreteStrategy : InterfaceStrategy
{
    public IEnumerable<string> PerformAlgorithm(List<string> list)
    {
        list.Sort();
        return list;
    }
}

class AlternativeConcreteStrategy : InterfaceStrategy
{
    public IEnumerable<string> PerformAlgorithm(List<string> list)
    {
        list.Sort();
        list.Reverse();
        return list;
    }
}

After that, define the Context class. It contains the code to run strategy work:

class Context
{
    private InterfaceStrategy _strategy;

    public Context()
    {
    }

    public Context(InterfaceStrategy strategy)
    {
        _strategy = strategy;
    }

    // here we can replace the current or default strategy if we choose
    public void SetStrategy(InterfaceStrategy strategy)
    {
        _strategy = strategy;
    }
    public void CarryOutWork()
    {
        Console.WriteLine("Context: Carrying out Sorting Work");
        var myResult = _strategy
            .PerformAlgorithm(new List<string>
            {
                "the",
                "boy",
                "is",
                "leaving"
            });

        Console.WriteLine(String.Join(",", myResult));
    }
}

Finally, use it in your program:

// client picks the default concrete strategy:
Console.WriteLine("Sorting strategy has been set to alphabetical:");
var context = new Context();

context.SetStrategy(new DefaultConcreteStrategy());
context.CarryOutWork();
Console.WriteLine();

// client picks the alternative concrete strategy
Console.WriteLine("Sorting strategy has been set to reverse:");
context.SetStrategy(new AlternativeConcreteStrategy());
context.CarryOutWork();

Code Returns:

Sorting strategy has been set to alphabetical:
Context: Carrying out Sorting Work
boy,is,leaving,the

Sorting strategy has been set to reverse:
Context: Carrying out Sorting Work
the,leaving,is,boy

This code follows a typical structural exploration of the strategy design pattern in C#, where you have two different options for sorting a list. You set the strategy reference to either the alphabetical or reverse option, and the program will sort the list according to the selected option.

Full code as a reference:

class Context
{
    private InterfaceStrategy _strategy;

    public Context()
    {
    }

    public Context(InterfaceStrategy strategy)
    {
        _strategy = strategy;
    }

    // here we can replace the current or default strategy if we choose
    public void SetStrategy(InterfaceStrategy strategy)
    {
        _strategy = strategy;
    }
    public void CarryOutWork()
    {
        Console.WriteLine("Context: Carrying out Sorting Work");
        var myResult = _strategy
            .PerformAlgorithm(new List<string>
            {
                "the",
                "boy",
                "is",
                "leaving"
            });

        Console.WriteLine(String.Join(",", myResult));
    }
}

// this interface holds the operations used by all strategies.
// The context can use this interface to call the operation defined
// by the strategy without directly accessing it.
public interface InterfaceStrategy
{
    IEnumerable<string> PerformAlgorithm(List<string> list);
}

// our default sorting strategy which focuses
// on sorting the list alphabetically
class DefaultConcreteStrategy : InterfaceStrategy
{
    public IEnumerable<string> PerformAlgorithm(List<string> list)
    {
        list.Sort();
        return list;
    }
}

// an alternative sorting strategy
// that reverses the list instead of sorting it alphabetically
class AlternativeConcreteStrategy : InterfaceStrategy
{
    public IEnumerable<string> PerformAlgorithm(List<string> list)
    {
        list.Sort();
        list.Reverse();
        return list;
    }
}

Does strategy pattern use polymorphism?

The Strategy pattern is a software design pattern that uses polymorphism to allow an algorithm to be selected at runtime. You can use it to select a different algorithm based on a different condition or to allow the algorithm to be selected by the user.

Strategy design pattern – Real-world example in C#

Now, we can lift ourselves back into the gym world.

class GymContext
{
    private IMembershipStrategy _strategy;

    public GymContext()
    {
    }

    public GymContext(IMembershipStrategy strategy)
    {
        _strategy = strategy;
    }

    // here we can replace the current or default strategy
    // if we choose
    public void SetStrategy(IMembershipStrategy strategy)
    {
        _strategy = strategy;
    }

    // method to display the time they are allowed
    // at the gym depending on the type of membership they choose
    public void MemberAcceptableTime()
    {
        Console.WriteLine("Context: Member is Selecting " +
            "Their Membership Type");
        var customerMembership = _strategy
            .SelectMembership(new List<string>
            {
                "Anytime",
                "2 PM to 4 PM."
            });
        Console.WriteLine("This membership will " +
            "allow you to access the gym at " + customerMembership);
    }
}

public interface IMembershipStrategy
{
    string SelectMembership(IList<string> timeData);
}

// strategy for selecting a peak membership
class PeakStrategy : IMembershipStrategy
{
    public string SelectMembership(IList<string> timeData)
    {
        // Peak Membership Implementation
        return timeData[0];
    }
}

// strategy for selecting an off-peak membership
class OffPeakStrategy : IMembershipStrategy
{
    public string SelectMembership(IList<string> timeData)
    {
        // Off Peak Membership Implementation
        return timeData[1];
    }
}

And the usage:

// client picks the default concrete strategy:
Console.WriteLine("Membership type has been set to peak:");

var context = new GymContext();
context.SetStrategy(new PeakStrategy());
context.MemberAcceptableTime();
Console.WriteLine();

// client picks the alternative concrete strategy
Console.WriteLine("Membership type has been set to off-peak:");

context.SetStrategy(new OffPeakStrategy());
context.MemberAcceptableTime();

Code Result:

Membership type has been set to peak:
Context: Member is Selecting Their Membership Type
This membership will allow you to access the gym at Anytime

Membership type has been set to off-peak:
Context: Member is Selecting Their Membership Type
This membership will allow you to access the gym at 2 PM to 4 PM.

In this case, we are exploring both our peak and off-peak strategies. However, although we do not examine other aspects of these subscriptions, such as payments, we can see how our context manages each option. When the user sets their membership to the peak type, our application lets them understand that they will be able to visit the gym at any time. Alternatively, when they select the off-peak membership, our application helps them know that their access times will be limited.

Using this as a starting point, we can start adding other subscriptions and display appropriate messages about them accordingly.

3 reasons to use the Strategy design pattern

The advantages of the Strategy pattern are:

  • An excellent alternative to subclassing.
  • Strategies eliminate conditional statements.
  • A choice of implementations.

The Strategy design pattern can be handy when developing an application. One of the advantages of the Strategy design pattern is that it provides a way for you to define the functionality that your program needs. It also allows you to abstract the details of how your program works so that you don’t have to worry about them.

In addition, it can be beneficial in writing unit tests because it will allow you to focus on testing specific pieces of functionality rather than the entire application.

Disadvantages of using the Strategy design pattern

The disadvantages of the Strategy design pattern are:

  • Clients must be aware of different Strategy implementations.
  • Communication overhead between Strategy and client code (Context).
  • Increased number of objects.

The main disadvantage of the Strategy design pattern is that clients must be aware of different Strategy implementations. It can be a challenge in some scenarios. It may also be difficult for clients to communicate with different Strategies.

Finally, the strategy pattern increases the number of objects in your application. This is because you must create a new object for each strategy you want to use.

What are related patterns to the Strategy design pattern?

A few related patterns to the Strategy pattern are worth mentioning. The first is the Flyweight pattern, which is concerned with minimizing the memory footprint of an object by sharing data between objects whenever possible. The State pattern is also related, as it deals with an object’s behavior changing based on its internal state.

Strategy and State patterns can be two different ways of implementing an algorithm. The Strategy pattern encapsulates an algorithm in a class, making it exchangeable with other algorithms. The State pattern represents an algorithm as a state machine. Each state is a different step in the algorithm.

What is the difference between Strategy pattern and Command pattern?

In the Strategy pattern, a class represents a single algorithm. Clients can use this algorithm by instantiating this class. In the Command pattern, a class represents a specific action or command. Clients can use this action by creating objects that encapsulate this command.

What is the difference between Strategy pattern and Dependency Injection?

There’s a lot of jargon in the software development field. A lot of people don’t know what to call the concepts. It can be challenging to know where to begin regarding software development. So, let me take the confusion out of it for you.

The Strategy pattern is a design pattern in which you can select an algorithm at runtime. The dependency injection pattern is a design pattern in which an object is given its dependencies rather than creating them.

For more information on dependency injection and why it’s one of the most valuable patterns you can use, check out the separate comprehensive article.

What is the difference between Factory and Strategy patterns?

The differences between the Factory and Strategy patterns are:

  • The Factory Method pattern focuses on creating objects, while the Strategy pattern focuses on defining algorithms.
  • The Factory Method pattern is a more specific solution to the problem of object creation, while the Strategy pattern is more general and can be applied to a broader range of problems.

Replace type code with the Strategy pattern – refactor to eliminate switch statements

The switch statement is a very powerful construct in programming. It allows the programmer to choose several code paths based on an input condition. However, sometimes the switch statements with the same cases starts to appear multiple times in the code.

In that cases, working with switch statements can be confusing. Therefore, you might think copying and pasting the same switch statements over and over is simpler than using the Strategy pattern. However, you can create a new strategy object for each case using the Strategy pattern instead of having the same switch statement everywhere.

Let’s go over one code example. We have the Manager class. The manager can have three levels: low, middle, and top. Based on its level, the manager gets the appropriate monthly salary.

class Manager
{
    public const int LOW_LEVEL = 0;
    public const int MIDDLE_LEVEL = 1;
    public const int TOP_LEVEL = 2;

    private int _managerLevel;
    private decimal _baseSalary;

    public Manager(int level, decimal baseSalary)
    {
        _managerLevel = level;
        _baseSalary = baseSalary;
    }

    public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
    {
        switch (_managerLevel)
        {
            case LOW_LEVEL:
                return _baseSalary;
            case MIDDLE_LEVEL:
                return _baseSalary * 1.25M;
            case TOP_LEVEL:
                return (_baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
            default:
                return 0;
        }
    }
}

We would like to eliminate the switch statement using the Strategy pattern. In this example, there is only one switch statement that has cases LOW_LEVEL, MIDDLE_LEVEL, and TOP_LEVEL. But in the real world, you would likely see switch statements with the same cases multiple times throughout the application. In those cases, you can eliminate switch statements using the Strategy pattern.

The first step is to encapsulate the _managerLevel field:

public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
{
    switch (GetManagerLevel())
    {
        case LOW_LEVEL:
            return _baseSalary;
        case MIDDLE_LEVEL:
            return _baseSalary * 1.25M;
        case TOP_LEVEL:
            return (_baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
        default:
            return 0;
    }
}

private int GetManagerLevel()
{
    return _managerLevel;
}

Next, define the Strategy structure for the manager level:

public abstract class ManagerLevel
{
    public abstract int GetManagerLevel();
}

public class LowManagerLevel : ManagerLevel
{
    public override int GetManagerLevel()
    {
        return Manager.LOW_LEVEL;
    }
}

public class MiddleManagerLevel: ManagerLevel
{
    public override int GetManagerLevel()
    {
        return Manager.MIDDLE_LEVEL;
    }
}

public class TopManagerLevel : ManagerLevel
{
    public override int GetManagerLevel()
    {
        return Manager.TOP_LEVEL;
    }
}

Next, change the type of the _managerLevel in the Manager class to ManagerLevel. We introduce the SetManagerLevel method to populate the _managerLevel field during class initialization:

class Manager
{
    public const int LOW_LEVEL = 0;
    public const int MIDDLE_LEVEL = 1;
    public const int TOP_LEVEL = 2;

    private ManagerLevel _managerLevel;
    private decimal _baseSalary;

    public Manager(int level, decimal baseSalary)
    {
        SetManagerLevel(level);
        _baseSalary = baseSalary;
    }

    private void SetManagerLevel(int level)
    {
        switch (level)
        {
            case LOW_LEVEL:
                _managerLevel = new LowManagerLevel();
                break;
            case MIDDLE_LEVEL:
                _managerLevel = new MiddleManagerLevel();
                break;
            case TOP_LEVEL:
                _managerLevel = new TopManagerLevel();
                break;
            default:
                throw new NotImplementedException("unknown level");
        }
    }

    public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
    {
        switch (GetManagerLevel())
        {
            case LOW_LEVEL:
                return _baseSalary;
            case MIDDLE_LEVEL:
                return _baseSalary * 1.25M;
            case TOP_LEVEL:
                return (_baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
            default:
                return 0;
        }
    }

    private int GetManagerLevel()
    {
        return _managerLevel.GetManagerLevel();
    }
}

Now, move the LOW_LEVEL, MIDDLE_LEVEL, and TOP_LEVEL to the ManagerLevel. They really belong to that class now.

public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
{
    switch (GetManagerLevel())
    {
        case ManagerLevel.LOW_LEVEL:
            return _baseSalary;
        case ManagerLevel.MIDDLE_LEVEL:
            return _baseSalary * 1.25M;
        case ManagerLevel.TOP_LEVEL:
            return (_baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
        default:
            return 0;
    }
}

Also, move the SetManagerLevel method to the ManagerLevel and make it static and change so it returns the instance of the ManagerLevel.

public static ManagerLevel SetManagerLevel(int level)
{
    switch (level)
    {
        case LOW_LEVEL:
            return new LowManagerLevel();
        case MIDDLE_LEVEL:
            return new MiddleManagerLevel();
        case TOP_LEVEL:
            return new TopManagerLevel();
        default:
            throw new NotImplementedException("unknown level");
    }
}

Then, use it in the constructor.

public Manager(int level, decimal baseSalary)
{
    _managerLevel = ManagerLevel.SetManagerLevel(level);
    _baseSalary = baseSalary;
}

Now, the code is in the ready state to perform Replace Conditional with Polymorphism refactoring.

  1. Move the GetMonthlySalary to the ManagerLevel class and fix the compile errors. I had to add the baseSalary as the method parameter.
public abstract class ManagerLevel
{
    public const int LOW_LEVEL = 0;
    public const int MIDDLE_LEVEL = 1;
    public const int TOP_LEVEL = 2;

    public static ManagerLevel SetManagerLevel(int level)
    {
        switch (level)
        {
            case LOW_LEVEL:
                return new LowManagerLevel();
            case MIDDLE_LEVEL:
                return new MiddleManagerLevel();
            case TOP_LEVEL:
                return new TopManagerLevel();
            default:
                throw new NotImplementedException("unknown level");
        }
    }

    public abstract int GetManagerLevel();

    public decimal GetMonthlySalary(decimal baseSalary, int numberOfPeopleInDepartment)
    {
        switch (GetManagerLevel())
        {
            case ManagerLevel.LOW_LEVEL:
                return baseSalary;
            case ManagerLevel.MIDDLE_LEVEL:
                return baseSalary * 1.25M;
            case ManagerLevel.TOP_LEVEL:
                return (baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
            default:
                return 0;
        }
    }
}

The Manager class then calls the new method.

public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
{
    return _managerLevel.GetMonthlySalary(_baseSalary, numberOfPeopleInDepartment);
}

Finally, define the GetMonthlySalary method in the strategy implementations. And mark the method in the base class as abstract. The final code:

class Manager
{
    private ManagerLevel _managerLevel;
    private decimal _baseSalary;

    public Manager(int level, decimal baseSalary)
    {
        _managerLevel = ManagerLevel.SetManagerLevel(level);
        _baseSalary = baseSalary;
    }

    public decimal GetMonthlySalary(int numberOfPeopleInDepartment)
    {
        return _managerLevel.GetMonthlySalary(_baseSalary, numberOfPeopleInDepartment);
    }
}

public abstract class ManagerLevel
{
    public const int LOW_LEVEL = 0;
    public const int MIDDLE_LEVEL = 1;
    public const int TOP_LEVEL = 2;

    public static ManagerLevel SetManagerLevel(int level)
    {
        switch (level)
        {
            case LOW_LEVEL:
                return new LowManagerLevel();
            case MIDDLE_LEVEL:
                return new MiddleManagerLevel();
            case TOP_LEVEL:
                return new TopManagerLevel();
            default:
                throw new NotImplementedException("unknown level");
        }
    }

    public abstract int GetManagerLevel();

    public abstract decimal GetMonthlySalary(decimal baseSalary, int numberOfPeopleInDepartment);
}

public class LowManagerLevel : ManagerLevel
{
    public override int GetManagerLevel()
    {
        return ManagerLevel.LOW_LEVEL;
    }

    public override decimal GetMonthlySalary(decimal baseSalary, int numberOfPeopleInDepartment)
    {
        return baseSalary;
    }
}

public class MiddleManagerLevel: ManagerLevel
{
    public override int GetManagerLevel()
    {
        return ManagerLevel.MIDDLE_LEVEL;
    }

    public override decimal GetMonthlySalary(decimal baseSalary, int numberOfPeopleInDepartment)
    {
        return baseSalary * 1.25M;
    }
}

public class TopManagerLevel : ManagerLevel
{
    public override int GetManagerLevel()
    {
        return ManagerLevel.TOP_LEVEL;
    }

    public override decimal GetMonthlySalary(decimal baseSalary, int numberOfPeopleInDepartment)
    {
        return (baseSalary * 1.25M) + (decimal)(numberOfPeopleInDepartment * 0.05);
    }
}

Conclusion

In summary, the Strategy pattern is a generic way of implementing an algorithm in an object-oriented programming language. Furthermore, it allows the algorithm to be chosen at runtime.

You can use the Strategy pattern when you want to provide a client multiple ways of performing the same task.

For example, a chess game might have a Strategy pattern for playing chess in an easy, medium, and hard mode. You can choose the difficulty you want at the start of the game, and the appropriate strategy will be selected and used.

Finally, you can use the Strategy design pattern to solve a wide range of problems.

If you want to learn more about design patterns, check the separate in-depth article about design patterns in C#.

Recent Posts