SOLID design principles in .NET: the Open-Closed Principle

In the previous post we talked about the letter ‘S’ in SOLID, i.e. the Single Responsibility Principle. Now it’s time to move to the letter ‘O’ which stands for the Open-Closed Principle (OCP). OCP states that classes should be open for extension and closed for modification. You should be able to add new features and extend a class without changing its internal behaviour. You can always add new behaviour to a class in the future. At the same time you should not have to recompile your application just to make room for new things. The main goal of the principle is to avoid breaking changes in an existing class as it can introduce bugs and errors in other parts of your application.

How is this even possible? The key to success is identifying the areas in your domain that are likely to change and programming to abstractions. Separate out behaviour into abstractions: interfaces and abstract classes. There’s then no limit to the variety of implementations that the dependent class can accept.

Demo

In the demo we’ll first write some code that calculates prices and does not follow OCP. We’ll then refactor that code to a better design. The demo project is very similar to the e-commerce one in the previous post and partially builds upon it so make sure to check it out as well.

Open Visual Studio and create a new console application. Insert a new folder called Model. The following three basic domain objects are the same as in the previous demo:

The TotalAmount function counts the total price in the cart. You can imagine that shops use many different strategies to calculate prices:

Price per unit

Price per unit of weight, such as price per kilogram

Special discount prices: buy 3, get 1 for free

Price depending on the Customer’s loyalty: loyal customers get 10% off

And there are many other strategies out there. Some of these are represented in the TotalAmount function by magic strings retrieved from the Identifier of the product. The decimals ‘5m’ etc. are the dollar prices. So here every product has the same price for simplicity.

Such pricing rules are probably changing a lot in a real word business. Meaning that programmer will need to revisit this if-else statement quite often to extend it with new rules and modify the existing ones. That type of code gets quickly out of hand. Imagine 100 else-if statements with possibly nested ifs with more complex rules. If it’s Christmas AND you are a loyal customer AND you have a special coupon then the final price may depend on each of these conditions. Debugging and maintaining that code would soon become a nightmare. It would be a lot better if this particular method didn’t have to be modified at all. In other words we’d like to apply OCP so that we don’t need to extend this particular code every time there’s a change in the pricing rules.

Extending the if-else statements can introduce bugs and the application must be re-tested. We’ll need to test the ShoppingCart whereas we’re only interested in testing the pricing rule(s). Also, the pricing logic is tightly coupled with the ShoppingCart domain. Therefore if we change the pricing logic in the ShoppingCart object we’ll need to test all other objects that depend on ShoppingCart even if they absolutely have nothing to do with pricing rules. A more intelligent solution is to separate out the pricing logic to different classes and hide them behind an abstraction that ShoppingCart can refer to. The result is that you’ll have a higher number of classes but they are typically small and concentrate on some very specific functionality. This idea refers back to the Single Responsibility Principle of the previous post.

There are other advantages to creating new classes: they can be tested in isolation, there’s no other class that’s dependent on them – at least to begin with-, and as they are NEW classes in your code they have no legacy coupling to make them hard to design or test.

There are at least two design patterns that can come to the rescue: the Strategy Pattern and the Template Pattern. We’ll solve our particular problem using the strategy pattern. If you don’t know what it is about then make sure to check out the link I’ve provided, I won’t introduce the pattern from scratch here.

The purpose of the IsMatch method will be to determine which concrete strategy to pick based on the OrderItem. This could be performed by a factory as well but it would probably make the solution more complex than necessary.

We still base the strategy selection strategy on the product identifier. This may be good or bad, but that’s a separate discussion. The main point is that the strategy selection and price calculation logic is encapsulated within this separate class. We’ll do something similar to the other strategies:

We store the list of possible strategies in the constructor. In the CalculatePrice method we select the suitable pricing strategy based on LINQ and the IsMatch implementations and we call its CalculatePrice method.

All the consumer of the ShoppingCart class needs to do is to specify a concrete IPriceCalculator object, such as the DefaultPriceCalculator one and let it calculate the price based on the items in the shopping cart. The ShoppingCart is no longer responsible for the actual price calculation. That has been factored out to abstractions and smaller classes that are easy to test and carry out very specific tasks.

What if the domain owner comes along and tell you that there’s a new pricing rule? Now instead of having to go through the if-else statements you can simply create a new pricing strategy:

Now you may think that you’ll need to introduce abstractions everywhere in your code for every little task. That’s not entirely correct. If you have a domain whose functionality changes a lot then you can apply OCP right away. Otherwise you may be better off not to introduce abstractions at first because they also make your code somewhat more complex. This may be the case with brand new domains in your application where you just don’t have enough experience and the domain expert cannot help you either. In such a case start off with the simplest possible design, even if it involves an if statement with a magic string. It may even be acceptable to later introduce an else statement with another magic string to accommodate a change in the logic. However, as soon as you see that you have to change and/or extend that particular functionality then factor it out to an abstraction. The following motto applies here:

“Fool me once, shame on you;fool me twice, shame on me.”

OCP doesn’t come for free. Implementing OCP will cost you some hours of refactoring and will add complexity to your design. Also, keep in mind that there’s probably no design that guarantees that you won’t have to change it at some point. The key is to identify those areas in your domain that are volatile and likely to change over time.

You’ll obviously need to extend the list of pricing strategies if there’s a new one… The structure of DefaultPriceCalculator doesn’t change to the outside world. It still has the same constructor and methods as before.
//Andras

Wrong. You should not modify the DefaultPriceCalculator to add extra strategy. Instead, inject the strategies as dependencies in such a way that we can use inversion of control to manage the different types of strategies which should be used our calculator.

You can do this quiet easily by injecting the strategies as an ienumerable (IEnumerable). This way it is up to the caller to specify which strategies this calculator should use. Makes the calculator flexibel, totaly independent on specific implementations of IPriceStrategy.

You can now use inversion of control to manage which strategies should be used for your specific (or more then one) pricecalulators.

The example of the author violates 2 SOLID principles. OCP is one of them. But have alook how the calculator is dependend on specific implementations instead of abstractions? Thats a violation against the Dependency Inversion principle.

Short rule to keep in mind: try to avoid instantiation of classes which contains more than just properties. So you do not want any “new PricePerUnitStrategy()” etc in your application…

I love this series of tutorials Andras, but where you have identified (as seems clear with the conversation with frederikprijck above) a clear example of a better way of coding your example, I think it would be a good idea to refactor the code in line with those suggestions in the main tutorial as many people won’t read down through the comments.

I realise, of course, that sometimes there will be matters of subjective opinion that aren’t so simply identified as enhancements/improvements to your example but in this case it seems to be quite clear that frederikprijck’s version of the calculator is a better way of approaching this example?