Realize the Open Closed Principle using abstractions

Take advantage of the Open Closed Principle to design classes that are open for extensions but closed for modifications

Open Closed Principle

Open Closed Principle

The Open Closed Principle states that classes should be open for extension but closed for modification. In essence, you need not change the current code base when a new functionality needs to be incorporated -- you should instead be able to extend the types and implement the functionality. Conformance to the Open Closed Principle facilitates building applications that are reusable and can be maintained easily.

The intent of the Open Closed Principle is ensuring that once the classes, modules, and functions in your application are defined, they shouldn't change over time. Note that a module implies a class, a group of classes, or a component that represents a feature. So if you have any new requirement that has come in, you should be able to address that change by extending the types; you should never change the types once they are defined. The Decorator, Factory, and Observer design patterns are good examples of design patterns that enable you to design applications that help you to extend the existing code base sans the need of changes to them.

The easiest way to implement the Open Closed Principle is to define your types in such a way that they adhere to the Single Responsibility Principle. In doing so, the concerns in the application's code are separated. The Single Responsibility Principle states that a class should have one and only one reason for change, i.e., a subsystem, module, class, or a function shouldn't have more than one reason for change. Once done, you should be able to represent these concerns using abstractions and enable the consumers of your class access these concerns through these defined abstractions. So, you should be able to extend the types defined in your application without modifying them. Note that abstraction is the key to realize the Open Closed Principle. The derivatives that are created from this abstraction are closed for modification since the abstraction is fixed. However, you can extend the behavior by creating new derivatives of the abstraction that has already been defined.

A bit of code

Let's dig into some code. Refer to the following code snippet. It illustrates a simple logger class that can log data to a log target. The LogTarget enum defines two log targets, i.e., log sources.

   public enum LogTarget

    {

        File, Database

    };

    public class Logger

    {

        public void Log(string message, LogTarget logTarget)

        {

            if (logTarget == LogTarget.File)

            {

                LogDataToFile();

            }

            else //if log target is database

            {

                LogDataToDB();

            }

        }

        private void LogDataToFile()

        {

            //Code to write log messages to a file

        }

        private void LogDataToDB()

        {

            //Code to write log messages to a database table

        }

    }

The private methods LogDataToFile and LogDataToDB are called based on the log target specified as parameter to the Log method of the logger class. This code violates the Open Closed Principle and is an example of bad design. What happens when you need to incorporate another log target, i.e., you want your logger class to be able to log data to a new log target? Well, you would then need to modify your Log method in the Logger class, write another if statement and a private method that would correspond to the new log target. Of course, you should also update the LogTarget enum with the new log source. Anyway, this seems weird, doesn't it? The following code listing shows how you can modify your Log class to ensure that the Open Closed Principle is not violated.

 public enum LogTarget

    {

        File, Database

    };

    public abstract class Logger

    {

        public abstract void Log(string message);

    }

    public class FileLogger : Logger

    {

        public override void Log(string message)

        {

            //Code to write log messages to a file

        }

    }

    public class DBLogger : Logger

    {

        public override void Log(string message)

        {

            //Code to write log messages to a database table

        }

    }

    public class ObjectFactory

    {

        public static Logger GetLogger(LogTarget logTarget)

        {

            switch (logTarget)

            {

                case LogTarget.File:

                    return new FileLogger();

                case LogTarget.Database:

                    return new DBLogger();

                default:

                    return null;

            }

        }

    }

If you need to incorporate a new log target, all you need to do is define it in the enum and create a class that corresponds to the new log target -- you no longer need to change the Logger base class. Note that the Logger class is now abstract and declares the Log method -- the Log method needs to be defined in the respective logger classes. Note that the ObjectFactory class is used to create an instance of the logger class that corresponds to the log target passed as parameter to the GetLogger() method.

The following code snippet illustrates how you can create an instance of the FileLogger class using the ObjectFactory class and invoke the Log method on the new instance.

static void Main(string[] args)

        {

            Logger logObj = ObjectFactory.GetLogger(LogTarget.File);

            logObj.Log("Hello:");

        }

This article is published as part of the IDG Contributor Network. Want to Join?

From CIO: 8 Free Online Courses to Grow Your Tech Skills
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.