Open Closed Principle

Last time we talked about the problems John faces when encountering code that doesn’t follow the Single Responsibility Principle. This time we’ll discuss about his colleague, Jane.

Jane is another programmer from the team, specialized in accounting. She receives requests from the accounting department, such as:

Until now, all our services had the VAT of 18% and it was charged every 6 months. Starting on 15 Sept 2014, some of our services will have a VAT of 10% and will be charged every 3 months. We need the VAT to apply automatically for this type of services when creating invoices from the application and the financial reports to reflect the change.

Since all the services had the VAT of 18% until now, it’s safe to assume that the code for computing the price with VAT is some variation of:

public Money computePrice(...){
    .....
    price = price * 1.18
    .....
}

Jane faces a big problem because she’s about to create a lot of overhead for the testing of the application. Let’s discuss why.

Fragility and Efficiency

When a programmer makes a change to the code, she should ensure of two things:

  1. The change in the code implements the requested feature or bug request
  2. The change in the code doesn’t introduce problems in the existing features

It’s well known how to ensure that the first criteria passes. When the feature is defined, acceptance tests are defined and included in the test plan, testers add more complex cases while the developer writes the code and then both the developer and the testers verify the change according to the test plan.

The second criteria is much more difficult. We all know that changing a piece of code can have ripple effects over the codebase, making completely unrelated features fail for unforeseen and often strange reasons. This failure of the design is called fragility.

To ensure that this criteria passes, a team has two choices:

  1. Re-test everything
  2. Identify the potentially affected parts and re-test them in a process called impact analysis

Let’s assume for a moment that all tests are automated and you can easily re-test the whole application in a few hours. If the change has introduced a problem to other features, then many tests will fail. Developers will need to analyze the failures, understand them and come up with an improved solution. Once they do, they will re-test and do this process again. It will probably take a few days. If the test team has to validate the application manually, then this process might take longer. Doesn’t it strike you as being very ineffective?

Open Closed Principle

What if the second criteria would pass by default? What if Jane knew that no other feature is affected by her change? That would be much more effective, wouldn’t it?

There is a way to structure your design so that you obtain this benefit. You make sure that when you need to add a new feature or change a bug, you write new code and don’t modify the existing code. This is known in the design literature as the Open Closed Principle(OCP), and it’s stated like this:

The code should be open to extension but closed to modification

Design for Open Closed Principle

How should the code be structured to follow Open Closed Principle? You need to go through the following thinking process.

First, what is likely to change? In this case, VAT is a likely candidate because we don’t have control over it. If it is likely to change, than we need to introduce an abstraction: a class, a function or a module. VATCalculator sounds like a good name for it. It should probably look like this:

public class VATCalculator{
    public Money computeVAT(Money price){
        price = price * 0.18
    }
}

That’s enough for our initial requirement to always apply 18% to all services. How do we use it in code?

public Money computePrice(...){
    .....
    VATCalculator vatCalculator = new VATCalculator();
    price += vatCalculator.computeVAT(price);
    .....

}

It’s still not enough. When the VAT value changes, we need to change the computePrice() method. Following OCP means that whenever the VAT changes, we don’t have to change the computePrice() method. How can we do that?

Let’s see how the code would look like if the class VATCalculator allows for more values of VAT:

public class VATCalculator{

    public VATCalculator(double vatRate){

    public Money computeVAT(Money price){
        return price * vatRate
    }
}
public Money computePrice(...){
    .....
    VATCalculator vatCalculator = new VATCalculator(18);
    price += vatCalculator.computeVAT(price);
    .....

}

(Yes, this design suffers from primitive obsession – using the number 18 instead of a Percent type -, but we’ll talk more about it in a later article.)

However, when the VAT value changes the computePrice() method needs to change and look like this:

public Money computePrice(...){
    .....
    VATCalculator vatCalculator
    if(service.hasType("serviceWith18PercentVAT") vatCalculator = new VATCalculator(18);
    if(service.hasType("serviceWith10PercentVAT") vatCalculator = new VATCalculator(10);
    price += vatCalculator.computeVAT(price); 
    ..... 
}

It doesn’t look much better, does it? Every time we need a new value for VAT, there’s a new if or casestatement. Surely, that’s not good design. The good news is that it’s easy to improve:

public Money computePrice(VATCalculator calculator, ...){
    .....
    price += vatCalculator.computeVAT(price); 
    ..... 
}

The correct VATCalculator is passed into the computePrice() method. This means that any code change is controlled from outside this method. We associate a VATCalculator to each type of service outsidecomputePrice() and pass it in as a parameter. We can now extend the method computePrice()without modifying it, thus following OCP and allowing safe changes.

How is the correct VATCalculator created and passed in? Typically, two different instances will be created on the application startup: one for 18% and one for 10%. They will be passed in either through configuration magic or as parameters. You might recognize this as being dependency injection.

Limitations of OCP Applicability

As we have seen in the previous example, OCP involves identifying the things that are likely to change. After the Romanian government has decided to change VAT in less than a week, this is a type of change that I expect. But maybe you’re living in a country that hasn’t changed sales taxes in the past 20 years. Should you do the extra effort sometimes involved by following OCP?

Moreover, the design we implemented only protects us against one type of change: the percentage value of VAT. What if the formula changes to involve additional parameters than just the price? Should we change the design to be open to this type of change as well?

The truth is that making a design completely open to any change is impossible. You need to prioritize. You will be surprised from time to time and you will need to go through the ineffective cycle of change – test – find issues in existing code. The key is however to minimize the times this happens without investing too much time.

My personal criteria for picking the places where to apply OCP are based on history. I start with the simplest implementation, apply SRP to separate responsibilities and then see what part of the code changes more. The first time it changes, I start wondering whether the design needs to change. I don’t actually change it until I’ve gathered enough evidence that it’s worth it.

There is another limitation. This example assumes that the current behavior of theVATCalculator.computeVAT() method will remain unchanged. If it needs to change, you have two choices: either use OCP again at VATCalculator level (for example by extracting a VATCalculator interface) or changing its implementation. In the second case, the contract between computePrice and VATCalculatorshould be preserved. We will discuss more about contracts between classes in the next installments of this series.

Conclusion

We change code because we need to implement a feature or fix a bug. When changing the code, we should take care not only that the change does what it’s supposed to do but also that it didn’t introduce regressions. The second criteria is harder to validate than the first, therefore the ideal situation is when we know that we hadn’t introduced regressions. How can we know? Only if every time we make a change, we add code and don’t change the existing code. This is the Open Closed Principle: the code should be open to extension but closed to modification.

It’s impossible to fully apply OCP on your design. You need to prioritize, and the best way is to use information from the past, because what has changed is more likely to change again.

The next blog post will be about Liskov Substitution Principle (it relates to contracts between classes), so stay tuned!

Do you have a piece of code where OCP might help but you aren’t sure how? Send a code sample to Adrian Bolboaca or Alex Bolboaca, and we’ll be happy to give you a few solutions!

For guaranteed response, we offer remote support for your teams. Sounds interesting? Let’s discuss about it.

More from the Blog

Leave a Comment

Your email address will not be published. Required fields are marked *

0
    0
    Your Cart
    Your cart is empty
      Apply Coupon
      Available Coupons
      individualcspo102022 Get 87.00 off
      Unavailable Coupons
      aniscppeurope2022 Get 20.00 off
      Scroll to Top