Be cautious when implementing the Open Closed Principle

bourzayq_khalid

Khalid BOURZAYQ

Posted on February 10, 2022

Be cautious when implementing the Open Closed Principle

Caution

Without a clear understanding of polymorphism and composition, OCP and the rest of the principles of SOLID won't make much sense.

Overview Of OCP

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

What does this mean?

It means that your code should be written in a way that leaves it flexible to adapt to changes (Open), without modifying the code already written (Closed).

A simple use case: Filtering Customers by criteria

We have a user story in our application backlog which demand to us to retrieve all the required information about our customers stored in the database, based on different criteria.
So, let’s start with that:

    public enum CustomerType
    {
        UnRanked,
        Silver,
        Glod,
        Platinium
    }
Enter fullscreen mode Exit fullscreen mode

And we have the Customer domain class

    public class Customer
    {
        public string Name { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
        public CustomerType Type { get; set; }
    }
Enter fullscreen mode Exit fullscreen mode

Now, we need to implement our filtering functionality. For example, we want to filter by Customer types:

    public class CustomersFilter
    {
        public List<Customer> FilterByType(IEnumerable<Customer> customers, CustomerType type) =>
                customers.Where(m => m.Type == type).ToList();
    }
Enter fullscreen mode Exit fullscreen mode
public List<Customer> GetSilverCustomers()
{
    var customers = new List<Customer>
    {
       new Customer { Name = "C1", Type = CustomerType.Silver},
       new Customer { Name = "C2", Type = CustomerType.Silver},
       new Customer { Name = "C3", Type = CustomerType.Glod}
    };
    CustomersFilter customersFilter = new CustomersFilter();
    var result = customersFilter.FilterByType(customers, CustomerType.Silver);

    return result;
}
Enter fullscreen mode Exit fullscreen mode

The user story is ok, validated and the code goes on production. After a couple of days, we receive a new user story in which the product owner wants to have a filter by subscription as well in the application.

What should we do in this case?

It should be easy, right?
We are going to add a second filter method by subscription and it will work perfectly.

Let's add the subscription enum

    public enum SubscribtionType
    {
        Free,
        Basic,
        Pro,
        Enterprise
    }
Enter fullscreen mode Exit fullscreen mode

And now, we can add our new method in the CustomerFilter class

public class CustomersFilter
{
    public List<Customer> FilterByType(IEnumerable<Customer> customers, CustomerType type) =>
            customers.Where(m => m.Type == type).ToList();

    public List<Customer> FilterByScreen(IEnumerable<Customer> customers, SubscribtionType subscribtion) =>
        customers.Where(m => m.Subscribtion == subscribtion).ToList();
}
Enter fullscreen mode Exit fullscreen mode

By testing the code that we have just added, everything works as it should, on the other hand we have just violated the OCP principle here because we have modified in the class instead of extending it.

Another problem here is that once a new filter is going to be expressed as a need, another filter method must be added.

So, what should we do in such cases?

There are several possible solutions, so that our code respects the OCP principle.
Solution 1: Use inheritance or interface implementation.
In the code below we are using interface implementation.

public interface IFilter<T, E> where T : class where E : Enum
{
    List<T> Apply(IEnumerable<T> collection, E type);
}

public class FilterBySubscibtion : IFilter<Customer, SubscribtionType>
{
    public List<Customer> Apply(IEnumerable<Customer> collection, SubscribtionType type)
    {
        return collection.Where(m => m.Subscribtion == type).ToList();
    }
}

public class FilterByCustomerType : IFilter<Customer, CustomerType>
{
    public List<Customer> Apply(IEnumerable<Customer> collection, CustomerType type)
    {
        return collection.Where(m => m.Type == type).ToList();
    }
}
Enter fullscreen mode Exit fullscreen mode

And our calls will be like:

private List<Customer> customers { get; set; } = new List<Customer>
{
    new Customer { Name = "C1", Type = CustomerType.Silver, Subscribtion = SubscribtionType.Free},
    new Customer { Name = "C2", Type = CustomerType.Silver , Subscribtion = SubscribtionType.Basic},
    new Customer { Name = "C3", Type = CustomerType.Glod, Subscribtion = SubscribtionType.Basic}
};
public List<Customer> FilterCustomers(CustomerType customerType)
{
    IFilter<Customer, CustomerType> customersFilter = new FilterByCustomerType();
    var result = customersFilter.Apply(customers, customerType);
    return result;
}

public List<Customer> FilterCustomers(SubscribtionType subscribtion)
{
    IFilter<Customer, SubscribtionType> customersFilter = new FilterBySubscibtion();
    var result = customersFilter.Apply(customers, subscribtion);
    return result;
}

public List<Customer> GetSilverCustomers() => FilterCustomers(CustomerType.Silver);

public List<Customer> GetCustomersWithFreeSubscibtion() => FilterCustomers(SubscribtionType.Free);

Enter fullscreen mode Exit fullscreen mode

As you can see in the code, we have set up two different implementations per filter type (subscription and customer type).
We also have two methods that call these two filters but we have redundancy in the code.

To avoid this kind of redundance, we will try to make a second implementation based on the specification design pattern.

Solution 2: Use specification pattern

In computer programming, the specification pattern is a particular
software design pattern, whereby business rules can be recombined by
chaining the business rules together using boolean logic. The pattern
is frequently used in the context of domain-driven design.

Source: Wikipedia - Specification pattern

The specification design model allows us to check if an object meets certain requirements (usually functional)
So, with this design pattern, we can reuse specs and combine them when needed.

Let's create our specifications:

public interface ISpecification<T>
{
    bool IsSatisfied(T item);
}

public class CustomerTypeSpecification : ISpecification<Customer>
{
    private readonly CustomerType _type;
    public CustomerTypeSpecification(CustomerType type)
    {
        _type = type;
    }
    public bool IsSatisfied(Customer item) => item.Type == _type;
}

public class SubscribtionTypeSpecification : ISpecification<Customer>
{
    private readonly SubscribtionType _type;
    public SubscribtionTypeSpecification(SubscribtionType type)
    {
        _type = type;
    }
    public bool IsSatisfied(Customer item) => item.Subscribtion == _type;
}

Enter fullscreen mode Exit fullscreen mode

With the ISpecification interface, we can determine whether or not our criteria are met, and then we can send it to the Apply method from the IFilter interface.

Now, time to update our existing code to use these specifications:

    public interface IFilter<T> where T : class
    {
        List<T> Apply(IEnumerable<T> collection, ISpecification<T> specification);
    }

    public class CustomersFilter : IFilter<Customer>
    {
        public List<Customer> Apply(IEnumerable<Customer> collection, ISpecification<Customer> specification)
        {
            return collection.Where(m => specification.IsSatisfied(m)).ToList();
        }
    }
Enter fullscreen mode Exit fullscreen mode

After updating the filter class, let's go to our finale class that use it and make some updates.

public class FilterService
{
    private readonly IFilter<Customer> _filter;

    private List<Customer> customers { get; set; } = new List<Customer>
    {
        new Customer { Name = "C1", Type = CustomerType.Silver, Subscribtion = SubscribtionType.Free},
        new Customer { Name = "C2", Type = CustomerType.Silver , Subscribtion = SubscribtionType.Basic},
        new Customer { Name = "C3", Type = CustomerType.Glod, Subscribtion = SubscribtionType.Basic}
    };
    public FilterService(IFilter<Customer> customerFilter)
    {
        _filter = customerFilter;
    }
    public List<Customer> FilterCustomers(ISpecification<Customer> specification)
    {
        return _filter.Apply(customers, specification);
    }

    public List<Customer> GetSilverCustomers() => FilterCustomers(new CustomerTypeSpecification(CustomerType.Silver));

    public List<Customer> GetCustomersWithFreeSubscibtion() => FilterCustomers(new SubscribtionTypeSpecification(SubscribtionType.Free));
}
Enter fullscreen mode Exit fullscreen mode

As we can see we don’t have any code redundancy in our FilterService class.

Conclusion

We have just seen how OCP can help us create clean and maintainable code. But we must be careful when implementing this principle.

As we saw in our example, this wasn’t an efficient way to use class inheritance. But we shouldn't be afraid to do so, it's completely normal, but we should at least make these changes as discrete as possible.

In other cases, we will be led to use composition over inheritance and use a design pattern such as Strategy to achieve our goal.

To conclude, the idea of this article is to keep OCP in mind during our development and write extensible code as much as possible, because this leads to a maintainable, scalable and testable code base however how the implementation used.

Some useful links:
https://ardalis.github.io/Specification/
https://deviq.com/principles/open-closed-principle

💖 💪 🙅 🚩
bourzayq_khalid
Khalid BOURZAYQ

Posted on February 10, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related