How to use Open Closed Principle the easy way

In this article, let's talk in detail about what is Open Closed Principle and how it helps in component's extensibility with examples.

Introduction

The term SOLID is an acronym denoting five principles which guide a developer or a designer on how an ideal component or a module is structured so that it is durable, testable and extensible for further iterations of improvements. These are at core, simple design principles in which each principle guide a specific design ideology.

In a previous article we have seen the first of these SOLID principles, the Single Responsibility Principle which talks about code flexibility and the responsibility for which the component must have reason to change.

While the first talks about having a single component that serves single purpose, the second of the principle talks about the need for the component to embrace extensibility.

It is called as the Open/Closed Principle.

What is Open/Closed Principle?

The principle states as follows –

Any component or module must be such that it is open to extension but closed for modification

In any application component we develop, we tend to modify the logic in every stage of development as per the requirements or the situations. Be it for a change request or for a feature request, we tend to change an already developed component in order to suit the new changes suggested or needed.

Let’s assume that such a component is being used at several other components and changes on such components may prove fatal to other modules or components which depend on the one which is changing.

Such situations result in more problems than before, since we end up testing more components than the one which is actually modified; for the modification shouldn’t cause any compatibility issues on the other modules.

This is what the second phrase of the principle states about – closed for modification. The principle suggests the first phrase as the solution – open to extension

The modules should be developed with their probable modification requests in mind and should be designed such that they embrace extension.

Extension in the realm of object oriented programming is a lot easier by means of inheritance and abstraction features which are offered by the programming languages. And so we must develop the components in such a way that they can be inherited by newer classes which are created for newer modifications.

In such a way we can avoid making changes to the original class (or the base types) which are already depended by other modules. And how do we achieve this? Talk about abstract methods and interfaces to begin with!

Open/Closed Principle Guidelines

Equal share of abstraction and concreteness

Too much of abstraction in our components makes designing and development complex, for there needs to have a lot of things to mess up with. And too much concreteness in our code makes it hard to extend and makes the component indeed heavy.

Clarity on Why

We should have an understanding on what for we are designing these components. And we should be able to recognize the possible directions in which these components can change according to the responsibility they serve. This can be the axis of change which we need to keep in mind, and should keep abstraction in place for future extension.

Don’t touch after its developed and tested

We should always prefer not to touch the component once it is developed and tested. And instead we should focus on developing a new component which can extend the developed module at its disposal for code reuse if needed, for any further modifications when required. This is the essence of the Open/Closed principle.

How to implement according to the Open/Closed principle

Since we are to refrain from modifying the developed component, we can instead use its services for building a new version by the below approaches:

  1. Parameters
  2. Inheritance
  3. Composition / Injection

Off these approaches, the most widely used approach is by inheritance, wherein we can extend the logic of a specific functionality by means of virtual base types and overriding derived types. One good example for this approach is the Decorator pattern which wraps an existing functionality for extended features.

Example – Extending the AuthRepo class

Consider the class AuthRepo which is responsible for Authenticating the input user token. The class is designed as follows.


    public class AuthRepo
    {
        private TokenManager _tokenManager;

        public AuthRepo()
        {
        
            _tokenManager = new TokenManager();
        }

        public AuthResult Authenticate(LoginModel credentials)
        {
            var user = ReaderStore.Users
            .FirstOrDefault(x => x.EmailAddress == credentials.Email);

            if (user != null)
            {
                return new AuthResult
                {
                    IsSuccess = true,
                    Token = _tokenManager.Generate(user)
                };
            }

            return new AuthResult { IsSuccess = false };
        }
    }

The class internally depends on another class TokenManager, which provides the functionality for generating tokens. And the class itself provides a functionality by means of a method Authenticate(), that which validates an input Login credentials for authenticity.

Let’s assume we need to add an additional logic for the Authentication method already provided by the AuthRepo class and return that as the actual pass criteria. This would require modification within the method Authenticate() which holds the base logic.

But doing so violates the Open/Closed principle which prohibits the modification of a developed component in the wake of a change.

Instead we can make use of Inheritance (Extension) to address the change. It can be done by simply marking the original functionality as virtual which adds scope for extension and simply extend the functionality in a derived class that extends the base type.


    public class AuthRepo : IAuthRepo
    {
        private ITokenManager _tokenManager;

        public AuthRepo()
        {
            _tokenManager = new TokenManager();
        }

        public virtual AuthResult Authenticate(LoginModel credentials)
        {
            var user = ReaderStore.Users
            .FirstOrDefault(x => x.EmailAddress == credentials.Email);

            if (user != null)
            {
                return new AuthResult
                {
                    IsSuccess = true,
                    Token = _tokenManager.Generate(user)
                };
            }

            return new AuthResult { IsSuccess = false };
        }
    }

    public class ExtendedAuthRepo : AuthRepo
    {
        public ExtendedAuthRepo(ITokenManager tokenManager)
        : base(tokenManager)
        {
        }

        public override AuthResult Authenticate(LoginModel credentials)
        {
		// fetch the original result which can now 
    // be extended as per requirement
            var res = base.Authenticate(credentials);
		// all new logic comes in here

	    return res;
        }
    }

By keeping these things in mind, we can develop simpler and extensible components which are ready for change without breaking the existing implementations.

Recommended Reading


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *