Open Close Principle By Example

SOLID principles are pretty useful to guide a good Object Oriented Programming flow, telling us which features a good system should have.

Open Close Principle is one of those features, meaning that our system/application/class/module should be open to extension, and close to modification.

This definition is kind of abstract to understand, so, in this post, we are going to design a module, iterating over multiple solutions, ending with a module open to extension, and close to modification.

TRY IT YOURSELF: You can find the source code of this post here.

Open Close Principle Basics

Let’s start with the abstract definition of this principle: our module/class/system should be open to extension and close to modification. We are going to split this in the two main parts as follows.

Close to Modification

Now, what does “close to modification” mean? In the following diagram, we illustrate this concept:

Module closed to modification

There, we see a closed module, that means, we cannot/need to modify anything into that module, it should behave as a black box to use, offering some services, and we should use them as the module defines.

How can I close features to be unmodified? well, that’s not a easy tasks. Meanwhile a developer has access to the source code, he will be able to modify it. What can I do? There are some options:

  • Define a repository for that module where just some developers are able to push modifications.
  • Create an artifact (jar, exe, war….) so, you will need to add that module as dependency, and use it as it is.
  • Create the module as a exposed service through the network, so, you can call its services using HTTP or something similar.

If those options cannot be applied in your context, at least, you should define when and how a change should be made in the module.

NOTE: You might think: “Come one, defining a closed module is a difficult task, what if I choose the wrong services to expose and at the end, my module needs to be modified a lot?”. You are right, as we are going to see later, we will iterate over our software to find a better solution each time. It is pretty difficult to find the best solution at the first try, this is software after all.

Open to Extension

Well, we know now that a closed module should be treat as a black box, but, this is software, we will need to modify somehow that module, adding new features or services. If I cannot modify the module, how can I add more features? well, you prepare the module to be extendible. The following diagram shows how this work:

Adding extension points to the closed module

As we can see, we identify some extension points into the closed module, and expose them. An extension point is a part of the module we think we might have multiple ways/implementations to accomplish the goal of that part, or we just think that that part should be defined by the client of the module, whose will be the only one to truly understand the problem he wants to solve.

CAREFUL: When we said “a part of the module”, we mean, a part. A module should have a main responsibility (an algorithm/flow), with some extensions points. If you need to override the whole module, why in the first place do you want to use that module?

How can I define an extension point? well, the easy way is an interface, where you define what we expect, as closed module, to be provided for someone, in some moment. Of course, the natural flow is to provide a default implementation of that interface, if that implementation doesn’t fit the requirements of someone, that one should be able to add its own extension.

Now, how can I define which, when and how an extension will be used? well, Inversion of Control and Dependency Injection could help here.

Open Close Principle Example

Well, as we see, defining the Open Close Principle is not a easy task, so, let’s work in a problem, from the first requirement, evolving the module to find an acceptable solution.

A Resume Generator Module

First, let’s define the requirement:

As a business, I want to generate a person’s resume in PDF with the person’s name and the date until the resume is valid which is the current date

NOTE: For the propose of this post, we won’t generate any PDF, just a String.

Well, we start defining a TemplateRender interface:

public interface TemplateRender {
  String render(String name, 
                Map<String, Object> data);
}

TemplateRender renders a template with a specific template’s name and a map of data.

NOTE: We won’t create a real implementation of TemplateRender as that is out of the scope of this post, however, you can use different libraries for that like Mustache.

Now, we define the main algorithm to generate the person’s resume:

public class ResumeTemplateProcess {
  private final TemplateRender templateRender;

  public ResumeTemplateProcess(
          TemplateRender templateRender) {
    this.templateRender = templateRender;
  }

  public String processResumeTemplate(
          Map<String, Object> data) {

    if (!isValid(data)) {
      throw new IllegalArgumentException("Not valid data for resume");
    }

    data.put("validUntil", LocalDateTime.now());

    return templateRender
            .render("resumeTemplate.template", data);
  }

  private boolean isValid(Map data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName");
  }
}

There, we receive a map of data with the person’s name, do some validations of that data regarding the resume requirement, add the current date as validUntil date and render the template.

As we can see, we use the TemplateRender interface, not a specific implementation, so, we just create an extension point of the ResumeTemplateProcess class. That extension point can be implemented depending the need. The main algorithm to generate a person’s resume keeps the same, despite of changing the implementation of TemplateRender. That means, ResumeTemplateProcess class is open to extension.

Adding a New Requirement

Okay, as this is software, the business wants to add a new feature into the module:

As a business, I want to generate another kind of resume, a long person’s resume in PDF with the person’s name, a description and the date until the resume is valid, which is the current date plus 7 days.

This means, we have now two kinds of resume, the basic resume and the long resume.

So, we add this new requirement to the module:

public class ResumeTemplateProcess {
  private final TemplateRender templateRender;

  public ResumeTemplateProcess(
          TemplateRender templateRender) {
    this.templateRender = templateRender;
  }

  public String processBasicResumeTemplate(
          Map<String, Object> data) {

    if (!isValidBasicResume(data)) {
      throw new IllegalArgumentException("Not valid data for basic resume");
    }

    data.put("validUntil", LocalDateTime.now());

    return templateRender
            .render("basicResumeTemplate.template", data);
  }

  private boolean isValidBasicResume(Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName");
  }

  public String processLongResumeTemplate(
          Map<String, Object> data) {
    if (!isValidLongResume(data)) {
      throw new IllegalArgumentException("Not valid data for long resume");
    }

    data.put("validUntil", LocalDateTime.now().plusDays(7));

    return templateRender
            .render("longResumeTemplate.template", data);
  }

  private boolean isValidLongResume(Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName") && data
            .containsKey("description");
  }
}

There, we create a new method named processLongResumeTemplate to receive a map of data with the person’s name and description, do some validations of that data regarding the long resume requirement, add the current date plus 7 days as validUntil date and render the template.

Well, this solution works fine, but, there is something weird: what if the business wants to add more kind of resumes? we will need to add a new method to ResumeTemplateProcess class by kind of resume. Seems like a kind of resume could be an extension point of our module.

Refactoring a Little Bit

Well, we realize that processBasicResumeTemplate and processLongResumeTemplate have the same signature, so, we move both process to a generic method:

public class TemplateProcess {
  private final TemplateRender templateRender;

  public TemplateProcess(
          TemplateRender templateRender) {
    this.templateRender = templateRender;
  }

  public String processTemplate(String templateName,
                                Map<String, Object> data) {
    if (templateName.equals("longResume")) {
      if (!isValidLongResume(data)) {
        throw new IllegalArgumentException(
                "Not valid data for long resume");
      }

      data.put("validUntil",
              LocalDateTime.now().plusDays(7));

      return templateRender
              .render("longResumeTemplate.template",
                      data);
    } else {
      if (templateName.equals("basicResume")) {
        if (!isValidBasicResume(data)) {
          throw new IllegalArgumentException(
                  "Not valid data for resume");
        }

        data.put("validUntil", LocalDateTime.now());

        return templateRender
                .render("basicResumeTemplate.template",
                        data);
      }else{
        throw new IllegalArgumentException(
                "Template not supported: " + templateName);
      }
    }
  }

  private boolean isValidBasicResume(
          Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName");
  }

  private boolean isValidLongResume(
          Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName") && data
            .containsKey("description");
  }
}

As we can see, we just generalized the API, with a new method named processTemplate, which receives the template’s name and the data to fill that template. Plus, we added a condition to check which template we want to render (longResume or basicResume) and executing the specific logic by each one. If the templateName parameter doesn’t match, we just throw an exception.

We didn’t change any requirement, we just refactor. However, seems like the longResume and basicResume logic are pretty different, both need different data and define a different validUntil date. So, using the Single Responsibility Principle, let’s extract that logic apart.

Using the Single Responsibility Principle

Now, we define a generic interface to encapsulate what the logic of long resume and basic resume shares. In this case, they seem to be validating and preparing data before the template is rendered. They validate the required inputs plus adding a new data for the validUntil date. The detail is as follows:

  • basicResume:
    • Validate that personName is present.
    • Add as validUntil the current date.
    • Render the template with the template name and data.
  • longResume:
    • Validate that personName and description are present.
    • Add as validUntil the current date plus 7 days.
    • Render the template with the template name and data.

As we can see, both kinds of resume shares a validation and preparation of data. So, we define the following interface:

public interface DataAggregator {
  Map<String, Object> aggregate(Map<String, Object> data);
}

There, we just define a DataAggregator. The DataAggregator receives data, and return data. You can process that data, deleting, adding, modifying, and validating properties.

We create both implementations of this DataAggregator interface. Let’s see the basic resume one:

public class BasicResumeDataAggregator implements DataAggregator {
  @Override
  public Map<String, Object> aggregate(
          Map<String, Object> data) {
    if (!isValidBasicResume(data)) {
      throw new IllegalArgumentException(
              "Not valid data for basic resume");
    }

    data.put("validUntil", LocalDateTime.now());

    return data;
  }

  private boolean isValidBasicResume(
          Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName");
  }
}

There, we just move the logic we had in TemplateProcess class regarding the basicResume template.

Now, let’s see the long resume template aggregator.

public class LongResumeDataAggregator implements DataAggregator {
  @Override
  public Map<String, Object> aggregate(
          Map<String, Object> data) {
    if (!isValidLongResume(data)) {
      throw new IllegalArgumentException(
              "Not valid data for long resume");
    }

    data.put("validUntil",
            LocalDateTime.now().plusDays(7));

    return data;
  }

  private boolean isValidLongResume(
          Map<String, Object> data) {
    return data != null && !data.isEmpty() && data
            .containsKey("personName") && data
            .containsKey("description");
  }
}

Again, we just move the logic we had in TemplateProcess class regarding the longResume template.

NOTE: Is this the Strategy design pattern? well, yes, it is.

Then, we can refactor the TemplateProcess class to use those two aggregators:

public class TemplateProcess {
  private final TemplateRender templateRender;
  private final DataAggregator resumeDataAggregator;
  private final DataAggregator longResumeDataAggregator;

  public TemplateProcess(
          TemplateRender templateRender,
          DataAggregator resumeDataAggregator,
          DataAggregator longResumeDataAggregator) {
    this.templateRender = templateRender;
    this.resumeDataAggregator = resumeDataAggregator;
    this.longResumeDataAggregator = longResumeDataAggregator;
  }

  public String processTemplate(String templateName,
                                Map<String, Object> data) {
    if (templateName.equals("longResume")) {
      data = longResumeDataAggregator.aggregate(data);

      return templateRender
              .render("resumeLongTemplate.template",
                      data);
    } else {
      if (templateName.equals("basicResume")) {
        data = resumeDataAggregator.aggregate(data);

        return templateRender
                .render("basicResumeTemplate.template",
                        data);
      }else{
        throw new IllegalArgumentException(
                "Template not supported: " + templateName);
      }
    }
  }
}

There, we just leave the code related with choosing which aggregator to use depending on the template we want to render. Now, the responsibilities are better defined.

However, if we want to add another kind of template, we still will need to modify the TemplateProcess class, to add the new aggregator, breaking the close to modification idea.

Closing to Modification the TemplateProcess class

Now, let’s see how we can avoid to modify the TemplateProcess class when we need to add another kind of resume.

As we see, the TemplateProcess requires some DataAggregators, one by each kind of resume we want to define. A DataAggregator is defined by a template name, in this case, basicResume has one DataAggregator and longResume has another. This means, we can define a Map of DataAggregators, where the key is going to be the aggregator name and the value the aggregator.

With this refactor, TemplateProcess looks like this:

public class TemplateProcess {
  private final TemplateRender templateRender;
  private final Map<String, DataAggregator> dataAggregators;

  public TemplateProcess(
          TemplateRender templateRender,
          Map<String, DataAggregator> dataAggregators) {
    this.templateRender = templateRender;
    this.dataAggregators = dataAggregators;
  }

  public String processTemplate(String templateName,
                                Map<String, Object> data) {
    if (dataAggregators.containsKey(templateName)) {

      DataAggregator dataAggregator = dataAggregators
              .get(templateName);

      data = dataAggregator.aggregate(data);

      return templateRender
              .render(templateName + ".template",
                      data);
    } else {
      throw new IllegalArgumentException(
              "Template not supported: " + templateName);
    }
  }
}

Now, TemplateProcess class only knows about a Map of aggregators, searching for the right aggregator to process the data. If we want to add a new DataAggregator for a new kind of resume, we just need to add that aggregator to the Map.

This helps us to accomplish the close to modification idea, as we won’t need to modify TemplateProcess class to add a new DataAggregator. Besides, as we can add new DataAggregator to the map, we create the TemplateProcess class open to extension.

Now, our closed resume module looks like this:

Closed Resume Module open to extension, close to modification

This looks better now, but, how, when and where do I create that DataAggregators Map? well, let’s use Inversion of Control and Dependency Injection with Spring.

Spring to the Rescue

Well, Spring handles the Inversion of Control and Dependency Injection for us.

We define a TemplatesFactory class. This class has multiple factory methods, one by each object we handle in our module:

@Configuration
public class TemplatesFactory {
  @Bean
  public DataAggregator basicResume() {
    return new BasicResumeDataAggregator();
  }

  @Bean
  public DataAggregator longResume() {
    return new LongResumeDataAggregator();
  }

  @Bean
  public TemplateRender templateRender() {
    return new BasicTemplateRender();
  }

  @Bean
  public TemplateProcess resumeTemplateProcess(
          TemplateRender templateRender,
          Map<String, DataAggregator> dataAggregators) {
    return new TemplateProcess(templateRender,
            dataAggregators);
  }
}

First, @Configuration tells Spring that this class should be processed to search Spring configurations, in this case, we define @Bean methods. Each method is a factory method, so, it creates an object and push it to the Spring context.

Next, in lines 4 and 9, we create the basicResume and longResume aggregators.

In line 14 we create a basic implementation of TemplateRender interface.

And finally, in line 21, we create the TemplateProcess class, injecting the TemplateRender implementation plus the DataAggregator map.

You might think: “Where do you create the DataAggregator map?”, well, I don’t create it. Spring creates it by itself. As we are injecting a Map<String, DataAggregator> object, Spring searches in his context any DataAggregator implementation, and group them in a map, where the key is the factory method name (basicResume and longResume).

Final Thought

SOLID principles guide us to create maintainable systems. The Open Close Principle is one we should try to apply, as they helps us to decouple the application, forcing us to use the Single Responsibility Principle too.

As we see, it is difficult to find the best approach to solve a problem the first time, we should iterate over and over, applying the SOLID principles each time, until we feel the design is good enough to solve the problem. This helps us to create a Emergent Architecture.

Finally, remember that the SOLID principles are just a guidance to use better OOP, you should validate your context before applying them as they are, at least, you will need to adapt them to your problem.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s