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

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

  • Discussed the Open/Close Principle.
  • How to refactor your existing code from classes designed for Single Responsible Principle to fit for Open Close Principle i.e., we designed our classes to be open for any of the changes without modifying existing classes or methods, but extending the classes through inheritance.

In this article, we are going to discuss the third principle of SOLID design principles i.e. the Liskov Substitution Principle.

Now open the solution used for OCP to start understanding and changing our classes according to LSP.

Liskov Substitution Principle

The following is collected from the actual document of the LSP:

"If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T".

This is purely in terms of software engineering and to understand it, we now take the easy definition of the same by Robert Martin as follows:

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it." - Robert Martin, LSP paper linked from The Principles of OOD.

In simple terms, a sub class should behave nicely when used in place of their base class. If not there is a flaw in your inheritance architecture. You would understand what I mean by this at the end of this article.

Let us go to our class diagrams that we have created in the previous article. If you see only a few classes in the class diagram space, just try to drag and drop all the classes onto the Classdiagram.cd area. Your class diagrams with inheritance should look as follows:

Data Access Layer

SOLID1.jpg



Business Logic Layer

SOLID2.jpg

Let us move ahead with LSP from here.

If you open our Program.cs file in the Client project, you observe that our code is already in compliance with LSP:

EmployeeData employeeData = new EmployeeOracleData();
employeeData.DisplayEmployeeData();
Console.ReadLine();

Here already we are substituting the child class EmployeeOracleData in the place of the parent class EmployeeData. Similarly we would find the same in the Business and Data Access classes in our application.

But if you keenly observe we have named the classes which they do not make sense in reality. In our application the class EmployeeData is supposed to be dealing with employees information from the SQL Server database and EmployeeOracleData is dealing with data got from the Oracle database. So, according to the best practices, there are two things that are different:

  • The name EmployeeData is not appropriate. We may need to change it to a more appropriate name that reveals its functionality.
  • EmployeeData should not be the parent class of EmployeeOracleData as they both have specific behavior rather than a common behavior in the true sense.

So, what we need to do is to create a common class for both which does not deal with any specific database.

So now we need to take care of the two things discussed above. So, what we do first is to change the name of the classes to appropriate ones that truly reveal their functionality.

First I would be changing the name of the DataAccess class to the SqlDataAccess class as follows:

public class SqlDataAccess

{    

}
SOLID3.jpg

Next I am going to change the name of EmployeeData class to EmployeeSqlData as follows:

public class EmployeeSqlData
{  
}

Similarly we are going to change the name of DisplayEmployee to DisplayEmployeeSqlData and the same for the DataFormatter class.

public class SqlDataFormatter

public class DisplayEmployeeSqlData

SOLID4.jpg

Now we are going to define an abstract class for each set of classes and let the two related classes inherit from the one abstract class.

Our new abstract classes for DataAcess are as follows:

public abstract class DataAccess
{
    public abstract IList<Employee> GetEmployeeData();
}

And the other two classes are now changed to:

public class OracleDataAccess : DataAccess
{
    public override 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 : DataAccess

{

    public override IList<Employee> GetEmployeeData()

    {

        var employees = new Employee[1];

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

        return new List<Employee>(employees);

    }

}

Our new abstract classes for Business Logic Layer are as follows:

public abstract class DataFormatter
{
    public abstract string FormatEmployeeData();
}

public abstract class DisplayEmployee
{
    public abstract void DisplayEmployeeData();
}

public abstract class EmployeeData
{
    public abstract void DisplayEmployeeData();
}

Now your program.cs looks as in the following (note now we are typecasting the base class object to the sub class (a specific class)):

EmployeeData employeeData = new EmployeeOracleData();

employeeData.DisplayEmployeeData();

Console.ReadLine();


Similar to above, now all the classes created in the OCP project must be changed from a baseclass (abstract) class object type cast to a sub-class.

For example, now our DisplayEmployeeOracledata looks like:

public class DisplayEmployeeOracleData : DisplayEmployee
    {
        public override  void DisplayEmployeeData()
        {
        DataFormatter dataFormatter = new OracleDataFormatter();



The same for all other classes. Now I have moved all my abstract classes to separate files and our class diagrams should be like as follows:

SOLID5.jpg

Our Business classes are now:

SOLID6.jpg

This completes the refactoring our code and let us run the application to see if we are getting the same output as we were getting earlier.

SOLID7.jpg

Now we see the output is the same and  our changes to the code has changed nothing.

Let us recap what we have done in this article:

  1. We named the classes with appropriate names relevant to the functionality or the responsibility of each class.

  2.  We have also created abstract classes as the base classes to simplify the object creation and made the classes that are related through inheritance are now freed up by attaching to the common parent.

  3. Anywhere in the application, we should be able to substitute any of the subclasses for the parent class without fail. If you cannot substitute, you may need to revisit your design of classes.

  4. We also made sure that by designing classes in this manner, we are following the best practices dictated in the OOAD methodologies.

Another great definition for LSP comes from this motivational poster that the folks at Los Techies put together:

SOLID8.jpg

But, there is a deviation to the LSP principle. So, whenever we are designing the classes we need to understand clearly if we are inheriting the classes by checking the responsibilities of Classes properly. That is, it is not always appropriate that you inherit class A from Class B and always expect that a Class A object can be substituted for a class B.

There is an example from Robert Martin on the deviation of this principle. If a class SQUARE inherits from RECTANGLE (mathematically it is logical), you cannot substitute SQUARE class objects everywhere a RECTANGLE class should be working. This basically fails because the area of a square is side * side and rectangle is length * breadth. If you assign properties for a square, you always have one property for square "side" and so, its area value can never be equal to the rectangle area.

So, always we need to analyze how we design our classes and how we are accomplishing inheritance. The bottom line is if there are two classes with different behavior, then do not relate them through substitution principles. They should always have different implementations.

Note: If you have observed, our code will never break with respect to the preceding example rectangle vs. square. We have taken care of eliminating this kind of relationship between two classes through designing a separate abstract class and then making the two classes as derived classes.

To be clear, this is what we did:

In the previous code for OCP, our Data Access class for Oracle inherited the Data Access class for SQL Server. So, this is the same as the case of Square and Rectangle example. According to LSP, we can substitute the Data Access for Oracle in the place of Data Access for SQL Server. This will lead to the wrong results.

So, we named the DataAccess class SQLDataAccess as it was getting data from the SQL database. Then we found that both the classes SQLDataAccess and OracleDataAccess are for accomplishing different responsibilities and we cannot inherit one from the other. So, what we did is, to eliminate unexpected results, created a new abstract class which is more generic and made the two classes inherit the abstract class. Now, if I want to substitute a base class to get data from the SQL Server then I can use the SQLDataAccess class; it does not break anywhere. Similarly for Oracle.

SO ALWAYS WISELY DESIGN CLASS INHERITANCE. IF THEY DO NOT MAKE SENSE BE INHERITED DESIGN ANOTHER BASE CLASS AND MAKE THE TWO CLASSES TO INHERIT THE BASE CLASS TO AVOID UNEXPECTED RESULTS.

Hope you liked this article. You can find the code base used for this article attached. Feel free to download and play with it to understand it better.

Up Next
    Ebook Download
    View all
    Learn
    View all