Skip to main content

Command Palette

Search for a command to run...

Implementing the Open-Closed Principle with Strategy and Factory Patterns

Updated
5 min read
Implementing the Open-Closed Principle with Strategy and Factory Patterns
K

Results-driven Full Stack .NET Developer specializing in building robust, maintainable enterprise applications. I leverage ASP.NET Core Web API and modern front-end frameworks like Blazor and React to deliver high-quality solutions

When building software, you want your code to be flexible and easy to extend without disrupting the existing logic. The Open-Closed Principle (OCP)—part of the SOLID principles—helps with exactly that. It says your code should be “open for extension but closed for modification”. Sounds great, but how do you actually do it? One clean way is to combine the Strategy Pattern and the Factory Pattern. Let’s walk through an example in C# to show how these patterns team up to make your code OCP-compliant, using a discount calculation system as our playground.

The Problem: Rigid Discount Logic

Imagine you’re building an e-commerce system where customers get discounts based on their membership tier (Silver, Gold, Platinum). Initially, you might be tempted to write a big if-else block to handle different discount calculations:

public decimal CalculateDiscount(string membershipType, decimal amount)
{
    if (membershipType == "Silver")
        return amount * 0.05m;
    else if (membershipType == "Gold")
        return amount * 0.10m;
    else if (membershipType == "Platinum")
        return amount * 0.15m;
    else
        return 0;
}

This works for a small system, but it’s a maintenance nightmare. Adding a new membership tier means cracking open the code and adding another else if. That’s exactly what OCP tells us to avoid—modifying existing code for new functionality. Plus, it’s hard to test and scale. Enter the Strategy and Factory patterns.

Step 1: Define the Strategy with an Interface

The Strategy Pattern lets us define a family of algorithms (in this case, discount calculations), encapsulate each one, and make them interchangeable. We start by creating an interface that all discount policies will follow:

internal interface IDiscountPolicy
{
    decimal CalculateDiscount(decimal amount);
}

This interface is the contract. Any discount policy we create must implement CalculateDiscount and return a decimal. This keeps things clean and ensures all policies are interchangeable.

Step 2: Implement Concrete Strategies

Now, let’s create the actual discount policies for each membership tier. Each policy implements the IDiscountPolicy interface and defines its own discount logic:

internal class DiscountSilverPolicy : IDiscountPolicy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.05m;
    }
}
internal class DiscountGoldPolicy : IDiscountPolicy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.10m;
    }
}
internal class DiscountPlatinumPolicy : IDiscountPolicy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.15m;
    }
}

Each class is simple and focused, handling one specific discount calculation. If we need a new tier, like “Diamond,” we just create a new class implementing IDiscountPolicy—no need to touch the existing ones. This is the “open for extension” part of OCP in action.

💡
To keep things simple, I used a straightforward example. In the code, I hardcoded the discount value. You can optimize this in your real-world scenario.

Step 3: Use a Factory to Create Strategies

So, how do we decide which policy to use at runtime? This is where the Factory Pattern comes in. Instead of scattering logic to pick the right policy across your codebase, a factory centralizes that decision. Here’s how we can implement it:

internal static class DiscountPolicyFactory
{
    private static readonly Dictionary<DiscountType, IDiscountPolicy> _discountPolicies = new()
    {
        { DiscountType.Silver, new DiscountSilverPolicy() },
        { DiscountType.Gold, new DiscountGoldPolicy() },
        { DiscountType.Platinum, new DiscountPlatinumPolicy() }
    };

    public static IDiscountPolicy GetDiscountPolicy(DiscountType discountType) =>
        _discountPolicies.TryGetValue(discountType, out var policy)
            ? policy
            : throw new ArgumentException("Invalid discount type");
}

We’re using an enum DiscountType to represent the membership tiers (Silver, Gold, Platinum). The factory maintains a dictionary mapping each DiscountType to its corresponding policy. When you call GetDiscountPolicy, it returns the right IDiscountPolicy implementation or throws an exception if the type is invalid.

Step 4: Putting It All Together

Here’s how you’d use this setup in your application:

var discountType = DiscountType.Gold;
decimal validAmount = 1000m;

// Get the right discount strategy from the factory
var discountPolicy = DiscountPolicyFactory.GetDiscountPolicy(discountType);

// Apply discount directly
decimal discount = discountPolicy.CalculateDiscount(validAmount);
decimal finalAmount = validAmount - discount;

Console.WriteLine($"Final amount after {discountType} discount: {finalAmount}");

Or, you can also create the DiscountCalculator class to encapsulate this logic:

internal class DiscountCalculator
{
    private readonly IDiscountPolicy _discountPolicy;

    public DiscountCalculator(IDiscountPolicy discountPolicy)
    {
        _discountPolicy = discountPolicy;
    }

    public decimal CalculateFinalAmount(decimal amount)
    {
        return amount - _discountPolicy.CalculateDiscount(amount);
    }
}

And the implementation will be like this:

var discountType = DiscountType.Gold;
decimal validAmount = 1000m;

// Get the right discount strategy from the factory
IDiscountPolicy discountPolicy = DiscountPolicyFactory.GetDiscountPolicy(discountType);

// Pass it into the calculator
var discountCalculator = new DiscountCalculator(discountPolicy);

// Now, the calculator takes care of the final amount logic
Console.WriteLine($"Final amount after {discountType} discount:
{discountCalculator.CalculateFinalAmount(validAmount)}");

The beauty here is that the client code doesn’t need to know how discounts are calculated, or which policy is being used. It just asks the factory for a policy and calls CalculateDiscount. If you add a new discount tier, you only need to:

  1. Create a new policy class implementing IDiscountPolicy.

  2. Update the factory’s dictionary with the new policy.

No existing code needs to change, satisfying OCP’s “closed for modification” rule.

💡
You can check the full implementation in this repository: kristiadhy/SolidPractical

Why This Works

  • Strategy Pattern: Encapsulates discount logic into separate classes, making them easy to swap or extend.

  • Factory Pattern: Centralizes the creation of strategies, keeping the client code clean and decoupled.

  • OCP Compliance: Adding a new discount tier doesn’t require modifying existing classes—just extend with a new policy and update the factory.

By combining Strategy and Factory patterns, you’ve got a clean, extensible system that’s easy to maintain and scales like a charm. Give it a try in your next project, and you’ll see how much easier it is to add new features without breaking a sweat.