S.O.L.I.D – The Interface Segregation Principle (ISP)



Abstract

This article is the 6th 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 Interface Segregation Principle (ISP).


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. Case Study
-        Requirements
  1. A design that does NOT conform to the ISP
1.     The IConfigManager interface
2.     The ConfigManager class
3.     A simple ‘Tasks’ design
  1. A design that conforms to the ISP
1.     The ICustomerConfigManager interface, and CustomerConfigManager class
2.     The ISalesOrderConfigManager interface, and SalesOrderConfigManager class
3.     The ISupplierConfigManager interface, and SupplierConfigManager class
4.     A simple ‘Tasks’ design
  1. Summary




Introduction

The interface segregation principle states that:

Clients should not be forced to depend on methods they do not use.

When we are designing interfaces that other classes could implement, sometimes with the aim to adhere to generic behavior, we add needles methods, properties etc., that are not needed by all clients of that interface.

In other words, the natural tendency is to include all ‘relevant’ methods on that interface, in order to guide the clients what to implement, according to our requirements.

This means that those clients would have to implement methods (or set default implementation) that they do not actually use, since not all clients would need all these methods.

Such design would cause undesired coupling between all clients of that interface, since, in case one client would change a method signature (as a result of new requirement), all clients would also have to be modified.

In addition, if some of the interface’s clients are in different modules (assemblies), such a change would require the developers to retest, rebuild and redeploy those modules as well.

This undesired coupling could eventually cause our application to suffer from software design smells, mainly rigidityfragility and viscosity.


In the following case-study, we’ll examine how a ‘big’ interface could lead to those disadvantages.





Case study

I’ll illustrate how to properly design a solution that conforms to the ISP using a configuration management example.


Requirements:

1.      We need to create a Configuration Manager that receives configuration parameters from different sources/providers, and is used in the entire codebase.

2.      The Providers could export configuration parameters from: Database, External services, web.config files, Custom configuration sections etc.

3.      Each Business Domain expects different configuration parameters.
-        Meaning, we have different business domains, such as: SalesOrders scenarios, Customers scenarios, Suppliers scenarios, etc.
-        Each business domain needs access to the configuration manager with respects to its scenarios, and invokes different operations.
-        Remark:
For the business logic implementation, I’ve added a simple BaseTask class that every business domain could inherit and implement with relevant logic, and invoke the proper configuration operations.

  


 A design that does NOT conform to the ISP


With the aim to get all the configurations in one place, developers tend to encapsulate all operations in one interface.

This way, they could have one entry point to all configuration parameters, meaning they could invoke the same class in every business domain, to retrieve the relevant configurations.

This is, of course, a violation of the Interface Segregation Principle, since we’ll get one ‘big’ interface with methods that are not relevant for all its clients.

Meaning, classes that would implement this interface will only need some of the interface’s methods. (The rest of the methods would get default implementation or throw an exception)

In addition, with every new business domain class, we’ll have to add methods to the interface and to all clients, which also violates the Open-Closed Principle.


More importantly, we’ll get undesired coupling between the interface’s clients.

As we know, sometimes we need to modify the interface API, since some of the client implementation was changed.

For instance, we have an interface that comprises the following method signature:

CustomerConfigItems GetVIPCustomersConfiguration();

New requirements instructed us to modify it as follows:

CustomerConfigItems GetVIPCustomersConfiguration(DateTime fromDate);


This would mean that other clients of that interface would also have to be modified respectively, and we’ll need to rebuild, retest and redeploy them as well.



We’ll examine the following design which violates the Interface Segregation Principle:


click the image to enlarge
click the image to enlarge





Description:


     1.     The IConfigManager interface

I’ve created the IConfigManager interface with all the relevant methods for all the business domains.

namespace Configuration.Manager.Config.Manager
{
    public interface IConfigManager
    {
        CustomerConfigItems GetCustomersConfiguration();

        CustomerConfigItems GetVIPCustomersConfiguration();

        SalesOrderConfigItems GetSalesOrderConfigId();

        SuppliersConfigItems GetSuppliersConfiguration();
    }
}



     2.     The ConfigManager class

The ConfigManager class implements the IConfigManager interface.

Remark: For our example and for simplicity I didn’t implement those methods.

using System;

namespace Configuration.Manager.Config.Manager
{
    public class ConfigManager : IConfigManager
    {
        public CustomerConfigItems GetCustomersConfiguration()
        {
            throw new NotImplementedException();
        }

        public SalesOrderConfigItems GetSalesOrderConfigId()
        {
            throw new NotImplementedException();
        }

        public SuppliersConfigItems GetSuppliersConfiguration()
        {
            throw new NotImplementedException();
        }

        public CustomerConfigItems GetVIPCustomersConfiguration()
        {
            throw new NotImplementedException();
        }
    }
}



Remark: Different Clients

Instead of creating one ConfigManager class, we could create separate classes for each business domain that would implement the IConfigManager interface.

E.g.  CustomerConfigManager, SalesOrderConfigManager and SupplierConfigManager.

However, the problem still remains, since the IConfigManager interface is still ‘big’ and contains methods that are implemented in different clients, thus the undesired coupling issue would just get worse (as described above).



     3.     A simple ‘Tasks’ design

I’ve created a simple ‘Tasks’ design to support the business domain logic.

We have an abstract BaseTask class that every business domain could inherit and implement respectively.

The BaseTask class contains a reference to the IConfigManager interface, thus every business domain class could use it to get its relevant configuration items.


using Configuration.Manager.Config.Manager;

namespace Configuration.Manager.Tasks.Base
{
    public abstract class BaseTask
    {
        // Protected variables

        protected IConfigManager _configManager;

       
        // Constructors

        public BaseTask(IConfigManager configManager = null)
        {
            // Setting the default ConfigManager
            // implementation in case it wasn't provided.
            _configManager = configManager ?? new ConfigManager();
        }       


        // Public abstract methods

        public abstract void Execute();
    }
}


using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.Customers
{
    public class CustomerTask : BaseTask
    {
        public override void Execute()
        {
            var customerConfiguration =
                _configManager.GetCustomersConfiguration();

            // TODO: Implementation...!
        }
    }
}


using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.SalesOrders
{
    public class SalesOrderTask : BaseTask
    {
        public override void Execute()
        {
            var salesOrderConfigId =
                _configManager.GetSalesOrderConfigId();

            // TODO: Implementation...!
        }
    }
}


using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.Suppliers
{
    public class SupplierTask : BaseTask
    {
        public override void Execute()
        {
            var suppliersConfiguration =
                _configManager.GetSuppliersConfiguration();

            // TODO: Implementation...!
        }
    }
}



Remark: Business domain tasks in different modules

We could see that each business domain task uses the same IConfigManager instance to retrieve its own relevant configuration methods.

Seemingly, since we’ve implemented only one client to the IConfigManager interface, the ConfigManager class, this design operates well.

However, in case we’ll need to modify the interface, and we have many business domains tasks, in different modules (assemblies), we’ll have to rebuild, retest and redeploy them as well.

This, of course, burden the system and eventually would cause software design smells.





A design that conforms to the ISP


There is more than one proper design for these requirements. For simplicity and for our purpose, I’ve created the following examples.

The basic idea is to divide the IConfigManager interface into different small interfaces that contains the relevant domain methods, and every business domain task would use its corresponding interface, instead of using the ‘big’ IConfigManager interface.

As a best practice it’s advisable to design small interfaces with as little methods as possible, to prevent ‘client coupling’, even if it’s for the same domain.

Meaning, if for example we have several configuration methods for the same business domain, we should create several ‘little interfaces’ in order to prevent clients-coupling as requirements would grow.



For our example, I’ve created the following design:


click the image to enlarge
click the image to enlarge





Description:

     1.     The ICustomerConfigManager interface, and CustomerConfigManager class


using Configuration.Manager.Config.Items;

namespace Configuration.Manager.Config.Manager.Customer
{
    public interface ICustomerConfigManager
    {
        CustomerConfigItems GetCustomersConfiguration();
        CustomerConfigItems GetVIPCustomersConfiguration();
    }
}


using Configuration.Manager.Config.Items;
using System;

namespace Configuration.Manager.Config.Manager.Customer
{
    public class CustomerConfigManager : ICustomerConfigManager
    {
        public CustomerConfigItems GetCustomersConfiguration()
        {
            throw new NotImplementedException();
        }

        public CustomerConfigItems GetVIPCustomersConfiguration()
        {
            throw new NotImplementedException();
        }
    }
}



     2.     The ISalesOrderConfigManager interface, and SalesOrderConfigManager class


using Configuration.Manager.Config.Items;

namespace Configuration.Manager.Config.Manager.SalesOrder
{
    public interface ISalesOrderConfigManager
    {
        SalesOrderConfigItems GetSalesOrderConfigId();
    }
}


using Configuration.Manager.Config.Items;
using System;

namespace Configuration.Manager.Config.Manager.SalesOrder
{
    public class SalesOrderConfigManager : ISalesOrderConfigManager
    {
        public SalesOrderConfigItems GetSalesOrderConfigId()
        {
            throw new NotImplementedException();
        }
    }
}



     3.     The ISupplierConfigManager interface, and SupplierConfigManager class


using Configuration.Manager.Config.Items;

namespace Configuration.Manager.Config.Manager.Supplier
{
    public interface ISupplierConfigManager
    {
        SupplierConfigItems GetSuppliersConfiguration();
    }
}


using Configuration.Manager.Config.Items;
using System;

namespace Configuration.Manager.Config.Manager.Supplier
{
    public class SupplierConfigManager : ISupplierConfigManager
    {
        public SupplierConfigItems GetSuppliersConfiguration()
        {
            throw new NotImplementedException();
        }
    }
}



     4.     A simple ‘Tasks’ design


namespace Configuration.Manager.Tasks.Base
{
    public abstract class BaseTask
    {
        public abstract void Execute();
    }
}


using Configuration.Manager.Config.Manager.Customer;
using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.Customers
{
    public class CustomerTask : BaseTask
    {
        // Private variables

        private ICustomerConfigManager _customerConfigManager;


        // Constructors

        public CustomerTask(ICustomerConfigManager customerConfigManager = null)
        {
            _customerConfigManager =
                customerConfigManager ?? new CustomerConfigManager();
        }      


        // Public override methods

        public override void Execute()
        {
            var customerConfiguration =
                _customerConfigManager.GetCustomersConfiguration();

            // TODO: Implementation...!
        }
    }
}


using Configuration.Manager.Config.Manager.SalesOrder;
using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.SalesOrders
{
    public class SalesOrderTask : BaseTask
    {
        // Private variables

        private ISalesOrderConfigManager _salesOrderConfigManager;


        // Constructors

        public SalesOrderTask(ISalesOrderConfigManager salesOrderConfigManager = null)
        {
            _salesOrderConfigManager =
                salesOrderConfigManager ?? new SalesOrderConfigManager();
        }     


        // Public override methods

        public override void Execute()
        {
            var salesOrderConfigId =
                _salesOrderConfigManager.GetSalesOrderConfigId();

            // TODO: Implementation...!
        }
    }
}


using Configuration.Manager.Config.Manager.Supplier;
using Configuration.Manager.Tasks.Base;

namespace Configuration.Manager.Tasks.Suppliers
{
    public class SupplierTask : BaseTask
    {
        // Private variables

        private ISupplierConfigManager _supplierConfigManager;


        // Constructors

        public SupplierTask(ISupplierConfigManager supplierConfigManager = null)
        {
            _supplierConfigManager =
                supplierConfigManager ?? new SupplierConfigManager();
        }
      

        // Public override methods

        public override void Execute()
        {
            var suppliersConfiguration =
                _supplierConfigManager.GetSuppliersConfiguration();

            // TODO: Implementation...!
        }
    }
}



Description:

In this design we’ve separated the configuration methods into 3 relatively small interfaces, based on the business domain requirements.

This design prevents the coupling between the interface clients, meaning in case we’ll have to modify one method signature, other clients won’t be affected by this change.

In other words, we managed to design these interfaces with respect to the Interface Segregation Principle.




Summary

We managed to illustrate a simple use of the Interface Segregation Principle and to understand the importance of ‘small’ designed interfaces.

We understood that, by creating ‘big’ interfaces, most times, some of the interface methods won’t be needed by all the interface’s clients.

These clients would have to provide default implementation, and this would naturally burden the entire codebase, with unnecessary maintenance, testing, bugs and so forth.

In addition, this eventually would create undesired coupling between the interface’s clients, and would lead to software design smells, mainly rigidity, fragility and viscosity.

To conclude I would say that, whenever a ‘big’ interface is required, we should remember that clients could implement more than one interface.

Thus, by dividing ‘big’ interface into few ‘small’ interfaces, we won’t affect the client’s implementation, on the contrary, we would provide a ‘healthier’ and much more tolerant to changes code.

  
---
Next in the SOLID series is the Dependency Inversion Principle, which describes the importance of loosely-coupled code, and how to prevent dependencies in our classes, by using abstractions.

We’ll design a better robust, easy maintainable and scalable solutions, which would naturally prevent software design smells.





The End

Hope you enjoyed!
Appreciate your comments…

Yonatan Fedaeli

No comments: