S.O.L.I.D. Principles in C# (Series)

S.O.L.I.D. Principles in C# (Series)

C#
S.O.L.I.D.
Programming
Software Engineering
Design Principles
2020-08-04

Hey there, Nate here again! Let’s dive a bit deeper into the Open/Closed Principle (OCP) and talk about why it’s such a big deal in software design. OCP can be summarized as “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.” Essentially, once you have a piece of code that works, you don’t want to keep opening it up and editing it whenever there’s a new requirement. Instead, your design should allow you to add new functionality by extending the existing code without breaking anything.

From personal experience, I’ve learned that we often don’t consider OCP until we’ve been burned once or twice by constantly tweaking our tried-and-tested classes. This principle encourages us to make classes flexible enough to handle new use cases—usually through inheritance, interfaces, or well-placed abstractions—while leaving the original code intact.

One of the biggest gotchas with OCP is that it’s easy to go overboard. You might be tempted to anticipate every future scenario. This can result in overengineering, leading to code that’s hard to understand. My advice: plan for probable scenarios that make sense for your application, but don’t over-design. Strike that balance between flexibility and maintainability.

Let’s illustrate OCP with a common scenario: discount calculation. Suppose we have a DiscountCalculator class that must handle different discount rates depending on the type of customer. The naive approach might look like this:

public class DiscountCalculator { public decimal CalculateDiscount(string customerType, decimal total) { if (customerType == "Regular") return total * 0.10m; else if (customerType == "VIP") return total * 0.20m; // And so on... return 0m; } }

This works for a while, but if Marketing wants to add a new “Platinum” discount or a special “SummerSale” discount, you’re forced to edit the existing code every time. Eventually, this method becomes a giant chain of if-else statements.

To adhere to OCP, we can design an interface and create strategies for different discount types. That way we merely add new classes—no need to change what’s already working. Here is a more flexible approach:

public interface IDiscountStrategy { decimal CalculateDiscount(decimal total); } public class RegularDiscountStrategy : IDiscountStrategy { public decimal CalculateDiscount(decimal total) => total * 0.10m; } public class VipDiscountStrategy : IDiscountStrategy { public decimal CalculateDiscount(decimal total) => total * 0.20m; } public class DiscountCalculator { private readonly IDiscountStrategy _strategy; public DiscountCalculator(IDiscountStrategy strategy) { _strategy = strategy; } public decimal CalculateDiscount(decimal total) { return _strategy.CalculateDiscount(total); } }

In this design, DiscountCalculator is closed for modification (it knows nothing about the specifics of each discount strategy) but open for extension (add a new strategy—like EmployeeDiscountStrategy—by simply implementing the interface).

Here’s a quick example of how you might use this in an application:

public class CheckoutService { public decimal GetFinalPrice(decimal total, string customerType) { IDiscountStrategy strategy; switch (customerType) { case "Regular": strategy = new RegularDiscountStrategy(); break; case "VIP": strategy = new VipDiscountStrategy(); break; // Additional strategies can be added here default: strategy = new RegularDiscountStrategy(); break; } var calc = new DiscountCalculator(strategy); return calc.CalculateDiscount(total); } }

Notice, we only switch on customerType once to decide which strategy is used. If new discount strategies arise, we simply add another class—no changes to existing calculator logic. This design patterns well with the Strategy Pattern, which is a natural fit for OCP scenarios.

A quick gotcha: If you find yourself adding a bunch of checks every time you create a new strategy—for instance, in constructing the appropriate strategy—ask yourself whether there's a more dynamic approach, like using a dictionary or dependency injection container, to map customer types to strategies.

  • Identify Volatile Parts Early: If a requirement is likely to change (like discount rules), create abstractions around it first. This allows you to add new behavior with minimal modifications to core code.
  • Don’t Overdo It: Overly abstracting every detail can lead to complexity. Follow a “pay as you go” approach—refactor to interfaces or new classes when a strong use case justifies it.
  • Start with Clear Interfaces: Good interfaces empower OCP by providing extension points. Spend time defining cohesive and focused interfaces up front.
  • Watch for Code Duplication: Some repetition might creep in among the different strategy classes. Consider extracting shared logic into an abstract base class, but only if it doesn’t overly complicate things.
  • Testing is Crucial: Because OCP often relies heavily on interfaces, thorough unit tests help ensure each new extension doesn’t break existing behavior.

In my journey as a developer, the most important lesson I’ve learned about OCP is that it’s not just about preventing changes to old code for the sake of it. Rather, it’s a tool for creating a robust system that fosters new features without bogging down your existing implementations.

By carefully applying interfaces, abstractions, or design patterns (like Strategy, Decorator, or Factory), you ensure you’re ready to adapt to evolving business needs with minimal friction. OCP isn’t about cutting corners or skipping out on new requirements—rather, it’s about planning for them in a way that avoids “breaking changes.”

Next up is the Liskov Substitution Principle (LSP), the third principle in the SOLID acronym. In discussing LSP, we’ll see how to keep your class hierarchies consistent, ensuring that subclasses can seamlessly sub in for their parent classes without throwing everything into chaos!

Onward to Part 3!
– Nate

Go to Part 3