The Open Close Principle (SOLID series)

OCP stated that all software entities (classes, functions, modules, etc..) should be open for extension but close for modification. Open for extension means the behavior of the entity can be altered or extended. Close for modification means any changes to the entity would not result in chages to the source code of that entity. Changes should be achieve by adding new code without changing old working code.

Suppose we decided to write a tictactoe game that can be played on the command line. Because we were so sure we just want to play on the command line, we decided to use the console output function directly everytime we need to output something, like this:

public class Game {
  ...
  public void gameIntro() {
    System.out.println("Let's Play TicTacToe");
  }
}
public class HumanPlayer {
  public int obtainCellSelection() {
    System.out.println("Please enter your selected cell number");
  }
}

These are just two among many classes where System.out.println is called. All is good with our command line game, until one day we decide it would be cool to play our tictactoe game on a web browser, or even better if we can switch effortlessly between different UI, which means we need to switch the IO. Looking at our current code, we realize that our existing code is so tightly coupled to the console that it is nearly impossible to implement any other UI. In this situation, if we switch to a web based game, everything class that calls System.out.println will have to change. This is a violdation of OCP.

What could have been done in order to make changes less painful. Abstraction can help. We can create an IO interface which is an abstraction with 2 concrete implementations of CommandLineIO and WebIO and have our Game class and Player class calling functions in this interface.

public interface IO {
    String obtainInput();
    void publishOutput(String output);
    void insertNewLine();
}

public class CommandLineIO implements IO {
    private Scanner scanner;
    private PrintStream outStream;

    public CommandLineIO(Scanner scanner) {
        this.scanner = scanner;
        this.outStream = System.out;
    }

    @Override
    public String obtainInput() {
        String input = this.scanner.nextLine();
        input = input.trim();
        return input;
    }

    @Override
    public void publishOutput(String output) {
        this.outStream.println(output);
    }

    @Override
    public void insertNewLine() {
        this.outStream.println("\n");
    }
}

public class WebIO implements IO {

    public WebIO() {
    }

    @Override
    public String obtainInput() {
      // Obtain input through a frontend app
    }

    @Override
    public void publishOutput(String output) {
     // Publish output on a web browser
    }

    @Override
    public void insertNewLine() {
     // Insert new line
    }
}

With these abstractions in place, now we can easily switch between the 2 IO options when we set up a new game and all the code in our Game class and Player class would not need to be changed. A game that plays on the console would look like this:

public static void main( String[] args ) {
        IO io = new CommandLineIO();
        Board board = new Board();
        Rules rules = new RulesFor3x3();
        Player playerOne = new Player();
        Player playerTwo = new Player() 

        Game game = new Game(playerOne, playerTwo, io, rules, board);
        game.play();
    }

If we need to switch to a web based game, the only line that needs to be changed is the instantiation of the IO, instead of new CommandLineIO(), we would do new WebIO() and everything else still works.

Following OCP sometimes means that we would write more code and that our system is more complicated and more difficult for users of the code to navigate and understand. However, code that follows the OCP is very extendable while code that violates OCP is often rigid and more difficult to changes if changes are required. In a situation where we know we will need to have multiple implementations down the line, it is definitely a good idea to put in those abstraction. But how often do we know for sure what we will get to implement next. The challenge is knowing when to put in abstraction. Putting in abstraction too early, anticipating changes that might never happen will make the code more complicated than it needs to be. It might be better to start with a simple implementation that possibly violates OCP, and gradually refactor with new layers of abstraction as we see fit.