• 4 hours
  • Easy

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 2/18/22

"O" for the Open/Closed Principle

I worked on a medical data system that had many incoming data sources. After the data arrived, it was cleaned, and then sent through the rest of the system. The original implementation was a bit of a mess. Every time we added a new customer, the code needed to be modified. Not a good approach if you want lots of customers.

I recognized the opportunity to use the open/closed principle:

A class should be open to extension, but closed to modification.

The original programmer had missed this! The system could be made open to extension (we could add new features) and could be made closed to modification (the old code didn't necessarily have to be modified when we added these new features). In other words, new functionality did not require a rewrite of existing code. 

Wait, but how? I thought you had to modify it every time you had a new customer! 

That’s what we were doing, but it didn’t have to be that way. I was able to isolate most of the old implementation. Then I concentrated on the incoming data sources (the new stuff). I was able to make this more generic. From then on, when we added a new customer, we either didn’t modify anything in the old code or if we did, we only added a new input data source type.

Why Use the Open/Closed Principle? 

If you aren’t modifying the existing code, you know you aren’t breaking any of it. All the new functionality is contained in the newly added class(es).

The hardest part about this principle is recognizing when it can be applied before you begin coding. Here are a couple of guidelines to help you recognize when open/closed may be applicable.

  • When you have algorithms that perform a calculation (cost, tax, game score, etc.): it is likely that the algorithm will change over time. Create an interface first, and then provide specific implementations in classes, picking the class at run time.

  • When you have data coming or or going from the system: the endpoint (file, database, another system) is likely to change. So is the actual format of the data. Again, come up with an interface first, and then a specific implementation for getting or saving the data as needed.

How Do We Apply the Open/Closed Principle to Our Code? 

One place for modification is in the winner evaluation. In the last chapter, we extracted a GameEvaluator class. What if we changed the game so that the lowest card wins? We could pass in a boolean parameter to say "find the high card winner" vs. "find the low card winner":

evaluateWinner(List<Player> players, bool findHighCardWinner) {
   if (findHighCardWinner) { /* find them */ }
   else { /* find the other */ }
};

But now we have a method doing two things. 😨 Not a good idea.

Is it really that big of a deal? 

Well, what if the winner evaluation added even more rules? This method becomes difficult to understand, test, and maintain.

So what should be we do? 

We need to convert GameEvaluator to an interface. That way, different rules can easily be created as specific implementations. All the rules evaluator classes will have an  evaluateWinner  method, taking the list of players. They can then check whatever they want about the player's hands, and apply whatever algorithms they need to determine the winner.

In our situation, depending on which game is chosen, we will create a high card wins, or a low card wins GameEvaluator.

Let's do it together!

public interface GameEvaluator {
    public Player evaluateWinner(List<Player> players);
}

public class HighCardGameEvaluator implements GameEvalutor {
}
public class LowCardGameEvaluator implements GameEvalutor {
}

Great, but how do we connect this to the controller class?

We can use the concept of dependency injection. Injection is the idea that an object is given (or "injected") an object to use, instead of making the object itself. You have the object that has the injection, have a method that takes an interface as a parameter. That way, some other place in the code can instantiate an object that implements the interface, and then use that object for what gets injected.

In our game, rather than have the controller class instantiate one of the two types of GameEvaluators, we will "inject" the one we want. We add a parameter to the controller constructor, which is of the type GameEvaluator. Then we can pass in the correct specific implementation as that parameter.

public GameController (GameEvaluator _gameEvaluator) {
   gameEvaluator = _gameEvaluator;
}

OK, we still have to deal with one more detail. We need to somehow create the actual GameEvaluator object to be injected into the GameController. You will see how to do that in a future section of the course.

The hard part is knowing what the open and closed parts are. This is where interfaces help:

  • The closed class (our controller) is the class that doesn't change. It always walks through the sequence of events, eventually needing to determine a winner.

  • The open part (GameEvaluator interface) allows for flexibility of implementation, so that we can easily change the rules of the game.

This is possible because the closed class thinks about the open class, but only as an interface. Neat, huh? 🤓

Try It Out for Yourself! 

Give this a try:

Extract out the GameEvaluator interface, and implement the two (high and low) specialized evaluators.

If you need help, check out the solution provided below in the screencast.

Let's Recap!

  • The open/closed principle says that classes should be open to extension, but closed to modification.

  • In existing systems, it might take some rework to get the code in a position to take advantage of open/closed.

In the next chapter, we will look at avoiding misuse of inheritance with Liskov substitution.

Example of certificate of achievement
Example of certificate of achievement