SOLID Design Principles in C# - Part 2 - Open-Closed

2022, Aug 28

The Open-Closed Principle (OCP) is a principle that helps us to think about extending class behaviours, and this is applied in object-oriented programming (OOP) through inheritance, interfaces or composition.

This post is part of a series where we explore the SOLID design principles, arguably the most popular design principles for object-oriented software development.

Open-Closed Principle

You should be able to extend a class behavior, without modifying it. Robert C. Martin

This principle should be considered when requirements change, and code changes are required. Rather than modifying the implementation, you should be able to extend the implementation. A class should be open for extension but closed for modification.

There are great benefits of applying OCP:

  • Avoid introducing code bugs - the chance of introducing bugs is very low, as the existing implementation is not modified.
  • Dependent classes don't need to adapt - By not modifying existing implementations, there is no need for dependent classes to change.

The wrong example

Let's take the example from SRP, in specific the OrderSaver. A violation of the Open-Closed Principle is shown below:

using SOLID_SRP.Models;

public class OrderSaver
{
    private readonly Order _order;

    public OrderSaver(Order order)
    {
        _order = order;
    }

    public void Save()
    {
        //TODO: save order
        Console.WriteLine($"save order to sql db: {_order.OrderNumber}");
    }

    public void SaveCosmos()
    {
        //TODO: save order to cosmos db
        Console.WriteLine($"save order to cosmos db: {_order.OrderNumber}");
    }
}

Looking at the implementation above we can see that the implementation for OrderSaver now has an extra method SaveCosmos(). We have directly modified an existing class, and for this method to be called, the dependent classes will need to adapt. Not only that, but we are also violating the Single Responsibility Principle (SRP)!

The right example

When we are extending class behaviours, in object-oriented programming (OOP) we can use inheritance, interfaces or composition. In my opinion, the use of interfaces is cleaner, as we dictate the use of a contract without any implementation.

Let's look at splitting concerns of Save() for saving Order to Sql, and SaveCosmos() for saving Order to Cosmos, using an interface IOrderSaver:

using SOLID_OCP.Models;

namespace SOLID_OCP
{
    public interface IOrderSaver
    {
        void Save();
    }
}
using SOLID_OCP.Models;

namespace SOLID_OCP
{
    public class SqlDbOrderSaver : IOrderSaver
    {
        private readonly Order _order;

        public SqlDbOrderSaver(Order order)
        {
            _order = order;
        }

        public void Save()
        {
            //TODO: save order
            Console.WriteLine($"save order to sql db: {_order.OrderNumber}");
        }
    }
}
using SOLID_OCP.Models;

namespace SOLID_OCP
{
    public class CosmosDbOrderSaver : IOrderSaver
    {
        private readonly Order _order;

        public CosmosDbOrderSaver(Order order)
        {
            _order = order;
        }

        public void Save()
        {
            //TODO: save order
            Console.WriteLine($"save order to cosmos db: {_order.OrderNumber}");
        }
    }
}

This way the OrderProcessor doesn't need to change drastically, we should only replace OrderSaver with IOrderSaver:

using SOLID_OCP.Models;

namespace SOLID_OCP
{
    public class OrderProcessor
    {
        private readonly OrderValidator _orderValidator;
        private readonly IOrderSaver _orderSaver;
        private readonly OrderNotifier _orderNotifier;

        public OrderProcessor(OrderValidator orderValidator, IOrderSaver orderSaver, OrderNotifier orderNotifier)
        {
            _orderValidator = orderValidator;
            _orderSaver = orderSaver;
            _orderNotifier = orderNotifier;
        }
        
        public void Process()
        {
            _orderValidator.Validate();
            _orderSaver.Save();
            _orderNotifier.Notify();
        }
    }
}

I really like the Open-Closed principle, because it is very easy to add new behaviours with a very minimal chance of introducing bugs to existing implementations.

The Open-Closed principle example is available on PlayGoKids repository.

Full Series

Single-Responsibility Principle (SRP)

Open-Closed Principle (OCP)

Liskov Substitution Principle (LSP)

Interface Segregation Principle (ISP)

Dependency Inversion Principle (DIP)