DEV Community

Theofanis Despoudis
Theofanis Despoudis

Posted on

Understanding SOLID Principles: Open-Closed Principle

Open closed Principle

This is the 3rd part of the series of understanding SOLID Principles where we explore what is Open-Closed Principle and why it helps with creating layers of abstraction that make it easy to withstand unnecessary change while providing new features.

As a small reminder, in SOLID there are five basic principles which help to create good (or solid) software architecture. SOLID is an acronym where:-

S stands for SRP (Single responsibility principle)
O stands for OCP (Open closed principle)
L stands for LSP (Liskov substitution principle)
I stand for ISP ( Interface segregation principle)
D stands for DIP ( Dependency inversion principle)

We’ve discussed Dependency Inversion and Single Responsibility before.

Now we are going to address Open Closed Principle.

Open Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

The main idea behind this one is that in one part of your code, you have your abstractions such as classes, modules, higher order functions that do one thing and do it well. You want to keep them pretty, cohesive and well behaved.

On the other hand, you have changing requirements, scope changes, new feature requests and business needs. You cannot keep your precious abstractions in a pristine form for a long time. Inevitably you will have to add more functionality or add more logic and interactions. You want to have a way of adding that extra scope while keeping the code in good standing.

That’s where this particular principle of Open Closed comes in play to help formulate a way to do that nicely. When the time comes, where you have to modify existing code you need to make sure that you:

  • Keep the current functions, Classes, modules as they are or at least close to what they used to be — aka immutable.

  • Extend, in a composable (avoid inheritance) way the existing Classes, functions or modules so that they expose the new feature or functionality possibly in a different name.

I make a good note to try to avoid classical inheritance here as it tends to couple things in a bad way. Use Classical inheritance for well-defined structures while trying to avoid misuse.

A more modern approach is to use Composition or the Composite design pattern. Let’s see an example of a usage of Composition below:

interface IRunner {
  run: () => void;
}
class AsafaPowell implements IRunner {
  run(): void {
    console.log("9.78s");
  }
}
class Runner {
  private runnerClass: IRunner;
  constructor(runnerClass: IRunner) {
    this.runnerClass = new runnerClass();
  }
  run() {
    this.runnerClass.run();
  }
}
interface IJumper {
  jump: () => void;
}
class MikePowell implements IJumper {
  jump(): void {
    console.log("8.95,");
  }
}
class RunnerAndJumper {
  private runnerClass: IRunner;
  private jumperClass: IJumper;
  constructor(runner: IRunner, jumper: IJumper) {
    this.runnerClass = new runner();
    this.jumperClass = new jumper();
  }
  run() {
    this.runnerClass.run();
  }
  jump() {
    this.jumperClass.jump();
  }
}
const runnerAndJumper: RunnerAndJumper = new RunnerAndJumper(AsafaPowel, MikePowell)
// He can jump and he can run!
runnerAndJumper.run();
runnerAndJumper.jump();
Enter fullscreen mode Exit fullscreen mode

Note that you can improve this implementation by providing a Dependency injection container for the constructor arguments for better inversion of control.

The most tricky part is to have a good idea of what are the future changes that ahead of time in order to make your abstractions more resilient to changing requirements without rewriting code again and again.

In that case what would really help is having regular scope meetings, where you can discuss future items or business requirements that are coming ahead of time in order for you or your team come up with a solid design plan of your system modules with that respect.

The more you know the better. As an engineer, you probably don’t like many surprises in the middle of the sprint, much more than having to implement an efficient and reliable system within a short deadline. You probably going to cut corners.

What it means also is that we should always strive to write code that doesn’t have to be changed every time the requirements change. All new functionality can be added by adding new composite classes or methods, or by reusing existing code through delegation.

Plugins and Middleware

The open-closed principle also applies to plugin and middleware architecture. In that case, your base software entity is your application core functionality.
In the case of plugins, you have a base or core module that can be plugged with new features and functionality through a common gateway interface. A good example of this is Web browsers like Chrome.

In the case of middleware, for example, you have a request-response cycle and you can add intermediate business logic in between in order to provide extra services or crosscutting concerns to your application. A good example is the Redux Middleware.

Conclusion

I hope this article inspires some of you to apply this principle and benefit from its traits. Having the ability to compose programs is a very powerful technique and gives a well-defined interface for extensions. The key thing for this is to have a solid base software entity that can be extended in a way that it won’t affect the current functionality.

References

Wiki page
C2 Page

Coming up next is Understanding SOLID Principles: Interface segregation principle

If this post was helpful please share it and stay tuned on my other articles. You can follow me on GitHub and LinkedIn. If you have any ideas and improvements feel free to share them with me.

Happy coding.

If you would like to schedule a mentoring session visit my Codementor Profile.

Top comments (2)

Collapse
 
andrespineros profile image
Andrés Felipe Piñeros • Edited

Isn't this the Strategy Pattern? Or a mix between Strategy and dependency injection.

Collapse
 
theodesp profile image
Theofanis Despoudis

Yes it has a lot of similarities. The strategy pattern is used mostly for decisions at runtime, in order for solutions be unblocked by any concrete implementations and work on the interface level only. It is like the concept of policy-based decision making.