S.O.L.I.D Design Principles Explained: Part 4

In the Third part of this series we discussed the following:

  • Liskov Substitution principle.
  • Also discussed, through examples, how improper selection of classes for inheritance causes unwanted results.

In this article, we discuss the "I" in the "SOLID" i.e. Interface Segregation Principle.

The ISP states that

"No Client should be forced to depend on the methods that they do not use". ISP advocates splitting interfaces which are very large into smaller and more specific ones so that clients will only have to know about the methods that are relevant to them. Such shrunken interfaces are also called role interfaces.

Now let us do it in practice and understand what it means by Interface Segregation Principle. Let us open the previous project that we have used for LSP.

Interface Segregation Principle in action

So in order to discuss this principle we need to bring interfaces first into our projects. What we are going to do is our base classes now should implement an interface. Some argue this is not a good one. But as a Microsoft programmers and also to make our code little more flexible we are going to do this implementation.

The main reason why it is recommendable that our abstract classes implement interfaces is that you will have an interface defining the contract and are hence independent of a concrete implementation. You have an abstract class where you provide a common base implementation for implementers of the interface which they can use and extend. If in case an implementer wants to implement the interface and not your base class, this is still possible since we have a separate interface available. Microsoft uses this design in most of the Framework classes. For example you can find it here.

So, I am changing my code a little bit to discuss the Interface Segregation Principle. Now classes in the DataAccessLayer look as in the following:

Solid1.jpg

One of the senior programmers has designed this interface thinking that the interface must be supporting 3 operations:

  1. Gets Employee Data from the database.
  2. Modify the Employee Data.
  3. Save the Employee Data.

So, what happened is that the abstract class has to reproduce the same methods as the abstract methods in order to make the derived classes to override the same methods. So, we ended up implementing all the methods in the derived classes.

Solid2.jpg

But in our project we are basically dealing with only getting employee details and we do not want other functionalities to be implemented.

Note: This is taken as an example. In the real world, it will be entirely different and there will be no project that only gets the data from a database and does not need to modify/save most of the time. But, you can imagine a situation where you are getting employee details from an external ERM system or getting financial data of a particular customer from the Moody's Risk Analyst or any third-party vendors where we only a request for data is possible and we will not be doing any other operations on the data.

So, now what we need to do is to apply the Interface Segregation Principle in our project by making our interface from being fat to thin as follows:

Solid3.jpg

Now, we have split up the interface IDataAccess into IDataAccess and IEmployeeDataAccess since our project does not need other methods. But, to maintain backward compatibility we may have to make the IDataAccess to inherit the IEmployeeDatAccess as a best practice or precaution so that in case if any other projects are using the Interface code it should not be breaking their functionality.

Solid4.jpg

Our DataAccess class still is implementing the IDataAccess interface. So, what we are going to do now is our sub-classes or concrete classes do not need to implement all the methods in the abstract class.
As we discussed earlier, the benefit of making the abstract class to implement the interface is if any of the classes do not want to override all the methods in the abstract class as in our scenario, they may want to implement the most appropriate interface instead.

So, in our example we are going to remove the relationship between the abstract class and sub-classes and make our sub-classes implement the IEmployeeDataAccess directly. Now our classes look as in the following:

Solid5.jpg

This has given more granularity in our inheritance level and any other projects which are using the DataAccess class and still works without breaking their code. Our classes in the project are more clean and free from errors.

Just for your reference with the code changes, the class code looks as in the following:

public abstract class DataAccess : IDataAccess

{

    public abstract IList<Employee> GetEmployeeData();

 

    public abstract void SaveEmployeeData(IList<Employee> employees);

 

    public abstract void ModifyEmployees(IList<Employee> employees);

}

public interface IDataAccess

{

   

    void SaveEmployeeData(IList<Employee> employees);

 

    void ModifyEmployees(IList<Employee> employees);

 

}

public interface IEmployeeDataAccess

{

    IList<Employee> GetEmployeeData();

}

{

    public IList<Employee> GetEmployeeData()

    {

        var employees = new Employee[1];

employees[0] = new Employee { EmployeeId = "Ora-123", EmployeeName = "Benjamin Franklin" ,Designation = "Manager"};

        return new List<Employee>(employees);

    }

 

}

public class SqlDataAccess : IEmployeeDataAccess

{

    public IList<Employee> GetEmployeeData()

    {

        var employees = new Employee[1];

employees[0] = new Employee { EmployeeId = "111", EmployeeName = "John Sherman" };

        return new List<Employee>(employees);

    }

 

 

}

Now our business classes creates instances of the type IEmployeeDataAccess as in the following:

public class OracleDataFormatter : DataFormatter
{
    public override string FormatEmployeeData()
    {
        IEmployeeDataAccess dataAccess = new OracleDataAccess();
        Console.WriteLine("Getting Employee Data...");
        var employessList = dataAccess.GetEmployeeData();
 
        Console.WriteLine("Formatting the Employee details...");
        return string.Format("Employee Name of Employee ID {0} is {1} and Designation of the Employee is {2}",
                                 employessList[0].EmployeeId, employessList[0].EmployeeName, employessList[0].Designation);
    }
}
public class SqlDataFormatter : DataFormatter
{
    public override string FormatEmployeeData()
    {
        IEmployeeDataAccess dataAccess=new SqlDataAccess();
        Console.WriteLine("Getting Employee Data...");
        var employessList = dataAccess.GetEmployeeData();

        Console.WriteLine("Formatting the Employee details...");
        return string.Format("Employee Name of Employee ID {0} is {1}",
                             employessList[0].EmployeeId, employessList[0].EmployeeName);
    }
}

Now let us run our new changes to see if anything has broken our actual output:

Solid6.jpg

We got the same output as in our previous article and so our new changes did not break anything in the previous code.

Points to ponder

  • We have split up the thick interface into thin interfaces.

  • To maintain backward compatibility, we make one of the interfaces still support all the operations that it was supporting by inheriting the new interface.

  • Our concrete sub-classes do not need to implement all the methods and so now instead of inheriting the abstract base class, we removed the inheritance from the base class and added the interfaces for inheritance.

  • Now instead of type-casting the base class object to sub-class in the business layer, now we are type-casting an interface type object to the sub-class.

  • This has made our code look clean and better to implement than the interfaces using the Interface Segregation Principle.

We will discuss the Dependency Inversion Principle (DIP) in out next article. You can find the source code of the project attached to this article.

Hope you liked this article. Any suggestions or comments or questions are most welcome.