Referbruv!


Card image cap

Understanding SOLID principles - Open Closed Principle

SOLID Principles  • Posted 4 months ago

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.

"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. And 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. And 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".

For such scenarios, the principle suggests via the first phrase - "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!

Guidelines for designing Components according to the Open/Closed principle:

  1. We should always keep in mind to have an equal share of abstraction and concreteness in our components. 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.
  2. 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.
  3. 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.

Approaches for Implementing 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.

Case Study - 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. Now 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.

Also Read:

Understanding the Single Responsibility Principle

Getting started and Implementing Decorator Pattern in ASP.NET Core

Published 4 months ago

Sponsored Links
We use cookies to improve user experience. Learn More