DEV Community

Cover image for Explaining the Open-Closed Principle to the Rubber Duck With a Hands-On Exercise
Emanuel Trandafir
Emanuel Trandafir

Posted on

Explaining the Open-Closed Principle to the Rubber Duck With a Hands-On Exercise

Understanding how the Opened-Closed principle, immutability, encapsulation, and unit tests are working with a simple, hands-on, interview problem.

Overview

FizzBuzz is a common interview question for junior developers. The initial requirement is to iterate through a list of numbers from 1 to 100 and print in the console:

  • Fizz - if the number is divisible by 3
  • Buzz - if the number is divisible by 5
  • Fizz Buzz - if the number is divisible by both 3 and 5 the number itself if no criteria were met

After that, the interviewer often adds extra requirements and use-cases, to see how easy it is for you to adapt and implement the new features.

The Design

Even though it can be tempting to start coding using a bunch of if statements and solve the problem quickly, let's try to take a step back and think about it first.

Image description

Firstly, we want to show that our code is testable. Asserting the console output can be hard and it will clutter the tests. Therefore, we'll try to separate the method which converts a number to the desired string from the one that writes the result into the console.

Secondly, the dividers are a potential point where the requirements might change. Consequently, we'll try to allow the creation of new dividers without affecting the rest of the code - conforming to the Open/Closed principle.

The Implementation

Let's start with the Divider class. It has a String "output" field and an integer "divisor".
They are both private and final, immutable. This means that, once a Divider is created, we can no longer change its output or the way is checking if the output needs to be printed.

public class Divider {
  private final String output;
  private final int divisor;

  public Divider(String output, int divisor) {
    this.output = output;
    this.divisor = divisor;
  }

  public boolean divides(int multiple) {
    return multiple % divisor == 0;
  }

  public String getOutput() {
    return output;
  }
}
Enter fullscreen mode Exit fullscreen mode

As a result, we will be able to create a list of Divider objects like this:

List<Divider> dividers = List.of(
    new Divider("Fizz", 3),
    new Divider("Buzz", 5)
);
Enter fullscreen mode Exit fullscreen mode

Now, let's take a look at the FizzBuzz class that s using these dividers:

For a given number, we'll concatenate the output of its dividers or return the number itself if none were found.

Unit Testing

It is now very simple to test the getOutput() method. In my case, I have used Junit5's parameterized test that allows me to specify pairs of input and expected output - but can easily be done manually in any testing framework:

class FizzBuzzTest {
  List<Divider> dividers = List.of(
    new Divider("Fizz", 3),
    new Divider("Buzz", 5)
  );
  FizzBuzz fizzBuzz = new FizzBuzz(dividers);

  @ParameterizedTest
  @CsvSource(value = {
    "1:1",
    "2:2",
    "3:Fizz",
    "5:Buzz",
    "15:FizzBuzz",
    "16:16"
  }, delimiter = ':')
  void getOutput(String input, String expected) {
    int number = Integer.parseInt(input);

    assertThat(fizzBuzz.getOutput(number))
      .isEqualTo(expected);
  }

}
Enter fullscreen mode Exit fullscreen mode

The Flexibility

Even if it doesn't look like much, this structure offers us a lot of flexibility.
We can add new Dividers just by creating new instances of the Divider object. For instance, we can print "Foo" for multiples of 7 with the code below:

List<Divider> dividers = List.of(
    new Divider("Fizz", 3),
    new Divider("Buzz", 5),
    new Divider("Foo", 7)
);
FizzBuzz fizzBuzz = new FizzBuzz(dividers);
Enter fullscreen mode Exit fullscreen mode

We can also remove dividers without touching the "production" code: we just need to remove them from the list when instantiating the FizzBuzz object. The various dividers are now simple plugins for our application.

Furthermore, we can change the order of outputs of the resulting string just by changing the order of the dividers from the list.
All this flexibility of changing the behavior of the application with minimal changes is a result of conforming to the Open/Closed and Single Responsibility principles.

Conclusion

In this article, we implemented a simple FizzBuzz while focusing on good coding practices, design, and principles.
Following the SOLID principles, especially the first two, and keeping an eye on testability lead to a decoupled and flexible design where we can easily adapt to new requirements with minimal risk.
In the next article, we'll see how to handle unexpected changes in requirements and how to use dependency inversion and polymorphism to solve the implement them.

Thank You!

Thanks for reading the article and please let me know what you think! Any feedback is welcome.

If you want to read more about clean code, design, unit testing, functional programming, and many others, make sure to check out my other articles.

If you like my content, consider following or subscribing to the email list. Finally, if you consider supporting my blog and buy me a coffee I would be grateful.

Happy Coding!

Top comments (0)