S.O.L.I.D – The Open-Closed Principle (OCP)

  

  
Abstract

This article is the 4th of a series of seven articles I wrote (described later on) about The Principles of Object Oriented Design, also known as SOLID.

This post describes the Open-Closed Principle (OCP).


As a prerequisite, please read the first 2 articles:
            2.     Software Design Smells


As architects and solution designers we’d like to design and write robust & clean solutions.

We’d like our code to be stable, maintainable, testable and scalable for as long as our product lives.

We’d like to implement and integrate new features as smoothly as possible in our existing (already tested and deployed) code, without any regression issues, and by maintaining dead-lines and production deployments.

In addition, we’d like our code to perform according to standard performance KPIs, and eventually, to serves its purpose.

Such desire has many obstacles, especially as our application evolves and our product become more complex.

More often than we’d like, we are experiencing these obstacles which could be described as software design smells.

The purpose of the SOLID principles is to achieve these desires and to prevent software design smells.

In this article series we’ll emphasize the importance of well-designed applications.

We’ll show how, by maintaining the SOLID principles in our code, we’ll create better robust solutions, provide easier code maintenance, faster bug fixing and the overall impact on the application performance, especially while adding new features to existing design, as the application evolves with new requirements.



The complete series:
           
Intro:
               1.      SOLID - The Principles of Object Oriented Design
               2.      Software Design Smells

S.O.L.I.D
               3.      The SingleResponsibility Principle (SRP)
               4.      The Open-ClosePrinciple (OCP)
               5.      The LiskovSubstitution Principle (LSP)
               6.      The InterfaceSegregation Principle (ISP)
               7.      The DependencyInversion Principle (DIP)




Content

  1. Introduction
  2. Preventing Design Smells
  3. OCP Implementation
  4. Case Study
    1. Object Composition – Example
-        A design that does NOT conform to the OCP
-        A design that conforms to the OCP
2.     Factory Method – Example
-        A design that does NOT conform to the OCP
-        A design that conforms to the OCP
  1. Summary 




Introduction

The open-closed principle states that:

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

This statement looks as a contradiction, meaning how could any software entity be open for extension and, at the same time, be closed for modification.

How could we extend our code without modifying it?

Well, the answer is by using abstraction…!


The open-closed principle attacks most of the software design smells, by guiding the developers to write code that does NOT modify the existing code (which was already tested and deployed), only by implementing new requirements as an extendable code.

This could be done by using abstraction and by smartly designing our code to be extended as the application evolves, right from the beginning.




Preventing Design Smells

When we’ll get a new requirement, whether it’s a new feature or a change to an existing feature, with a good design that conform to the OCP, we could extend our code with almost never modifying the existing code.

 This design would prevent:

1.      Rigidity – Since we don’t need to modify the existing code and won’t affect any other dependent entities (or modules).
2.      Fragility – Since we’ll only extend our code and won’t affect other areas that could be broken otherwise.
3.      Immobility – Since by extending our code, we are creating loosely coupled code, especially to common components, which are much easier to reuse.
4.      Software Viscosity – Since we could preserve the existing design by extending it, and won’t need to use ‘Hacks’ (new designs that are NOT compliant to the existing design) to add new requirements.
5.      Needless Repetition – Since we’ll mostly use abstraction, and thus avoid duplicate code.
6.      Opacity – Since by using self-explanatory known ‘extendable designs’ we’ll provide better, clear and relatively easy to understand code.




OCP Implementation

As aforementioned, we could create code that conforms to the open-closed principle by using abstraction in our solutions designs.

In C# we could achieve abstraction by using interfaces or abstract classes, depends on the requirement and on our code styling.

Both ways are fine, as long as new requirements could be extended by deriving from these abstractions, without modifying the existing code.




Case Study

There are numerous examples I could use to illustrate the OCP, however in this article I’ll only present the following two:



1.  Object Composition - Example

         -        A class is using another class (or a service) to perform some business logic (BL) operation.
         -        A new requirement indicates a change in the existing BL operation.
         -        I’ll use the following classes:
-        Person
-        PersonService
         -        The Person class uses the PersonService class to get a list of Persons objects.



A design that does NOT conform to the OCP


using SOLID_Examples.OCP.Services;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.BOs
{
    public class Person
    {
        // Public properties

        public int Id { get; set; }
        public string Name { get; set; }

        public PersonService PersonService { get; set; }


        // Public methods

        public List<Person> GetAllPersons()
        {
            return PersonService?.GetPersonsList();
        }
    }
}


using SOLID_Examples.OCP.BOs;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.Services
{
    public class PersonService
    {
        public List<Person> GetPersonsList()
        {
            // TODO: Need to implement.

            return null;
        }
    }
}


Description:

-        We could see that the ‘Person’ class contains an object of type ‘PersonService’.
-        And also uses its API in the GetAllPersons() method.
-        A new requirement arrives and we’ll need to change the BL behavior.
-        For instance, we need to get all the persons-list from another source (service).
-        This means that we’ll have to modify the ‘Person’ class to use the new service.
-        And also to modify the GetAllPersons() method to use the new service API.
-        This of course would create undesired coupling and dependency between the ‘Person’ class to the ‘PersonService’.
-        This design does NOT conform to the open-closed principle, and in time would create software design smells in our application, as described above.

  

 A design that conforms to the OCP


using SOLID_Examples.OCP.Services;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.BOs
{
    public class Person
    {
        // Public properties

        public int Id { get; set; }
        public string Name { get; set; }

        public IPersonService PersonService { get; set; }


        // Constructor

        public Person(IPersonService personService)
        {
            PersonService = personService;
        }


        // Public methods

        public List<Person> GetAllPersons()
        {
            return PersonService?.GetPersonsList();
        }
    }
}


using SOLID_Examples.OCP.BOs;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.Services
{
    public interface IPersonService
    {
        List<Person> GetPersonsList();
    }
}


using SOLID_Examples.OCP.BOs;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.Services
{
    public class PersonService : IPersonService
    {
        public List<Person> GetPersonsList()
        {
            // TODO: Need to implement.

            return null;
        }
    }
}


using SOLID_Examples.OCP.BOs;
using System.Collections.Generic;

namespace SOLID_Examples.OCP.Services
{
    public class PersonExternalService : IPersonService
    {
        public List<Person> GetPersonsList()
        {
            // TODO: Need to implement.

            return null;
        }
    }
}


using SOLID_Examples.OCP.BOs;
using SOLID_Examples.OCP.Services;

namespace SOLID_Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Old requirement: Using the PersonService.
            //Person person = new Person(new PersonService());


            // New requirement: Using the PersonExternalService.
            Person person =
                new Person(new PersonExternalService());


            var allPersons = person.GetAllPersons();
        }
    }
}


Description:

-        In this design we created the ‘Person’ class with the IPersonService interface, instead of a concrete object PersonService.
-        Using the IPersonService interface, we in fact, created an abstraction of this service.
-        This means that every service that implements this interface could be used in the ‘Person’ class.
-        In this solution, we provided an easy way to change the BL behavior, without modifying the ‘Person’ class nor the ‘PersonService’ class, only by extending the code.
-        Meaning, when a new requirement arrives, we would create a new proper service class and use it in the ‘Person’ class.
-        We created the new service PersonExternalService (that implements the IPersonService interface) to extract the persons-list from another source.
-        We only need to modify the initialization of the ‘Person’ class to use the new service.
-        We don’t necessarily need to change all the ‘Persons’ invocations, only where needed in the code, based on the BL requirements.
-        Meaning, in some places we could use the PersonService and others the PersonExternalService service, and so on.
-        In addition, if needed, we could also set a default service in the Person class’s constructor.


 Important:

In this solution we also created a dependency-injection constructor to inject the desired service into the ‘Person’ class before using it.
Any other solution would be fine, as long as we’ll provide the desired service before invoking the ‘person.GetAllPersons()’ method.
  
Examples:

      -        Method injection:

var allPersons = person.GetAllPersons(new PersonExternalService());


      -        Simple property assignment:

Person person = new Person();
person.PersonService = new PersonExternalService();

var allPersons = person.GetAllPersons();


      -        Using configuration:

In this example we used a configuration value (in app.config) to set the service we’d like to use in the entire application.
If a change in the requirement would arrive, we only need to create a new proper service, and to modify the configuration file respectively.
      
using SOLID_Examples.OCP.Services;
using System;
using System.Collections.Generic;
using System.Configuration;

namespace SOLID_Examples.OCP.BOs
{
    public class Person
    {
        // Public properties

        public int Id { get; set; }
        public string Name { get; set; }
        public IPersonService PersonService { get; set; }


        // Private static variables

        private static string personServiceFullNameKey = "PersonServiceFullName";
        private static string personServiceFullName;


        // Constructor
              
        public Person(IPersonService personService = null)
        {
            // In case the personService wasn't
            // provided, create it based on configuration.
            PersonService = personService ?? CreatePersonService();
        }      


        // Public methods

        public List<Person> GetAllPersons()
        {
            return PersonService?.GetPersonsList();
        }


        // Private methods

        private IPersonService CreatePersonService()
        {
            try
            {
                // Retrieve the desired service full-name from configuration.
                if (personServiceFullName == null)
                {
                    personServiceFullName =
                        ConfigurationManager.AppSettings[personServiceFullNameKey];
                }

                // Create the desired service.
                return
                    Activator.CreateInstance(Type.GetType(personServiceFullName))
                        as IPersonService;
            }
            catch (Exception exception)
            {
                // TODO: Log relevant message and consider behavior.
                throw new Exception(exception.Message);
            }
        }
    }
}




2.  Factory Method - Example

-        In this example I’ll create the ‘PersonServiceCreator’ class, which is a Factory-Method Design Pattern, to decide which service to create at run-time.
-        We’ll illustrate an integration between Design Patterns & Design Principles. (I’ll probably post another article elaborating on this)



A design that does NOT conform to the OCP



namespace SOLID_Examples.OCP.Services
{
    public enum PersonServiceType
    {
        PersonService,
        PersonExternalService,
    }

    public static class PersonServiceCreator
    {
        public static IPersonService Create(PersonServiceType personServiceType)
        {
            IPersonService personService;

            switch (personServiceType)
            {
                case PersonServiceType.PersonService:
                    personService = new PersonService();
                    break;

                case PersonServiceType.PersonExternalService:
                    personService = new PersonExternalService();
                    break;

                default:
                    personService = null;
                    break;
            }

            return personService;
        }
    }
}


Description:

-        We created the PersonServiceCreator class with a single ‘Create()’ method.
-        The ‘Create()’ method receives a PersonServiceType enumerator to indicate which service to create.
-        In case a new requirement would arrive, we’ll have to modify both the PersonServiceCreator class and the PersonServiceType enumerator.
-        This of course violates the OCP since such modification to an already tested and deployed code could create design smells.
-        This is a very simple example, however in real-life code, we could also experience additional dependencies, e.g. the PersonServiceCreator also performs additional dependent operations that could be fragile when modifying this class.
-        In addition, if this creator factory is in a separate common module, we’ll also have to build and redeploy to all other modules.
-        Thus, we need to design a solution that conforms to the OCP and use it instead.



A design that conforms to the OCP

           
using System;

namespace SOLID_Examples.OCP.Services
{
    public static class PersonServiceCreator
    {
        public static IPersonService Create(string serviceFullName)
        {
            try
            {
                // TODO: Log relevant message.

                // TODO: Perform additional common operations.

                return
                    Activator.CreateInstance(Type.GetType(serviceFullName))
                        as IPersonService;
            }
            catch (Exception)
            {
                // TODO: Log relevant exception and consider behavior.
                throw;
            }
        }
    }
}

           
string personExternalServiceFullName =
                "SOLID_Examples.OCP.Services.PersonExternalService";

var personService =  
          PersonServiceCreator.Create(personExternalServiceFullName);

var allPersons = personService.GetPersonsList();



Description:

-        We created the PersonServiceCreator class and the ‘Create()’ method.
-        The ‘Create()’ method receives a string of the full name of the service we’d like to create.
-        Using the Activator.CreateInstance method we are creating the desired service using reflection at run-time.
-        In this way we don’t need to modify this class when a new service is required, we only need to provide its full name.
-        Meaning, whom ever is using this class is responsible to provide the correct service full name, whether it was set as a constant or extracted from configuration or database, it’s definitely not the responsibility of the PersonServiceCreator class.
-        This way this class is open for extensions and closed for modification.


Important:

-        This solution does not obligate every Factory Method implementation.
-        Meaning, every design should be considered with respect to the relevant requirements and to other constraints (e.g. performance).
-        This, of course, is also true for all the Design Principles.
-        Meaning, when architecting & designing our solutions, we should always consider different development elements and use common-sense, and not follow any instructions blindly, even the Design Principles.




Summary

It’s not always possible to foresee the way our application would evolve, thus It’s not always possible to close our design to all unpredictable changes.

Meaning, for some changes our design would be closed, but for others, not necessarily.

Furthermore, we should NOT try to close our design for all possible changes, since it would result in needless complexity and over design, which in return would burden the entire system, and we’ll achieve the exact opposite of the OCP purpose.

Instead, we should try to estimate the way a specific design would evolve and design the OCP accordingly.

Simply put, when designing a solution with respect to the Open-Closed Principle, we should carefully estimate (according to experience) the way it could evolve, and close it respectively.

In addition, we should always remember to avoid over-engineering & needles complexity.


---
Next in the SOLID series is the Liskov Substitution Principle, which describes how to create a proper inheritance between objects, in order to use polymorphism correctly, without breaking our code, and in order to prevent software design smells.




The End

Hope you enjoyed!
Appreciate your comments…

Yonatan Fedaeli
  

No comments: