S.O.L.I.D – The Liskov Substitution Principle (LSP)




Abstract

This article is the 5th 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 Liskov Substitution Principle (LSP).


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. Implementation that does NOT conform to the LSP
1.     BOs implementation
2.     Builders implementation
3.     The ServiceManager class implementation
  1. Implementation that conforms to the LSP
1.     BOs implementation
2.     Builders implementation
3.     The ServiceManager class implementation
  1. Inheritance & the LSP
-        How should we determine a proper inheritance?
  1. Summary 

  


Introduction

The Liskov substitution principle states that:

Subtypes must be substitutable for their base types.

This means that base types could be replaced by their corresponding derived types without manipulating or breaking the code.

Simply put, we could use the base type in our code to invoke all its corresponding derived types.

Seemingly, this determination seems very implicit and simple to accomplish, we should only create a base type, derive from it, and use it in our code when needed.

However, we’ll illustrate how a simple inheritance, when created incorrectly, would violate the LSP, and as a result would also violate the OCP, and eventually would cause our application to suffer from software design smells.

Remark: When our design does NOT conform to the LSP it usually also violates the OCP.




Case Study

Consider the following requirements and different implementations:


Requirements:

1.      We have 3 business objects:

              1.      Person
              2.      Employee
              3.      Supplier

Person is the base class of Employee and Supplier.

2.      We need to create a Service Manager class that invokes a service.
3.      This service receives one of our BOs and builds a request respectively.
4.      Meaning, for each BO the request would be built differently.




Implementation that does NOT conform to the LSP

One possible implementation that violates the LSP and as a result also violates the OCP is the following:


1.    BOs implementation:


using DesignPrinciplesExamples.LSP.RequestBuilders;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Person
    {
        public int Id { get; set; }

        public PersonRequestBuilder PersonRequestBuilder { get; set; }

        public Person()
        {
            PersonRequestBuilder = new PersonRequestBuilder();
        }
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Employee : Person
    {
        public EmployeeRequestBuilder RequestBuilder { get; set; }

        public Employee()
        {
            RequestBuilder = new EmployeeRequestBuilder();
        }
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Supplier : Person
    {
        public SupplierRequestBuilder Builder { get; set; }

        public Supplier()
        {
            Builder = new SupplierRequestBuilder();
        }
    }
}


Description:

-        We could notice only from the BOs implementation that using these ‘request builders’ also violates the OCP. (as described in the ‘The Open-Closed Principle’ article I posted)



2.    Builders implementation:



namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class PersonRequestBuilder
    {
        public string BuildRequest()
        {
            // TODO: Need to implement.
            return null;
        }
    }
}



namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class EmployeeRequestBuilder
    {
        public string Build()
        {
            // TODO: Need to implement.
            return null;
        }
    }
}



namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class SupplierRequestBuilder
    {
        public string BuildSupplierRequest()
        {
            // TODO: Need to implement.
            return null;
        }
    }
}


 Description:

-        We could see that every builder was implemented in a different manner, without uniformity or a base class, in addition, every method was named differently.

-        It looks as if this implementation was done by different programmers, perhaps on different times, without any code integration process.

-        Later on, when I’ll provide the proper implementation that conforms to the LSP, I’ll also change the ‘requests builders’ implementation.



3.    The ServiceManager class implementation:


using DesignPrinciplesExamples.LSP.BOs;

namespace DesignPrinciplesExamples.LSP
{
    public class ServiceManager
    {
        public void InvokeService(Person person)
        {
            var request = BuildRequest(person);


            // TODO: Need to implement.
           
            //try
            //{
            //    service.Invoke(request);
            //}
            //catch (System.Exception)
            //{
            //    throw;
            //}
        }


        // Private methods

        private string BuildRequest(Person person)
        {
            string request = null;

            // Verify which type is the 'person'
            // and build the request respectively.
            if (person is Employee)
            {
                Employee employee = person as Employee;
                request = employee.RequestBuilder.Build();
            }
            else if (person is Supplier)
            {
                Supplier supplier = person as Supplier;
                request = supplier.Builder.BuildSupplierRequest();
            }
            else
            {
                request = person.PersonRequestBuilder.BuildRequest();
            }

            return request;
        }
    }
}



     Description:

-        The ServiceManager class contains a method to invoke the service, and builds the request according to the objects that it receives.

-        The ‘BuildRequest()’ method violates the LSP and as a result also violates the OCP.

-        We could see that instead of using polymorphism, we are using an if statement to verify which BO was sent, and building the request accordingly.

-        This, of course, would have been prevented with a proper design that uses correct inheritance and polymorphism implementation.

-        And for each new BO we’ll add (with new requirement) we would also have to modify the ServiceManager implementation, which obviously, violates the OCP.





Implementation that conforms to the LSP

There are different ways to design these requirements with respect to the LSP and OCP principles, one possible implementation is the following:


1.    BOs implementation:

using DesignPrinciplesExamples.LSP.RequestBuilders;
using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Person
    {
        public int Id { get; set; }
        private BaseRequestBuilder _requestBuilder;


        // Injecting the required 'Request Builder'
        // when initializing the person class. 
        public Person(BaseRequestBuilder requestBuilder = null)
        {
            // Setting a default 'request builder'
            // in case it wasn't provided.
            _requestBuilder = requestBuilder ?? SetDefaultRequestBuilder();
        }


        // Protected virtual methods

        protected virtual BaseRequestBuilder SetDefaultRequestBuilder()
        {
            return new PersonRequestBuilder();
        }


        // Public virtual methods

        public virtual string BuildRequest()
        {
            return _requestBuilder.BuildRequest();
        }
    }
}


Description:

-        The Person class contains a BaseRequestBuilder object.

-        In the constructor we are injecting the desired ‘Request Builder’ and setting a default value, in case it wasn’t provided.

-        We are using the virtual SetDefaultRequestBuilder() method to set the ‘Request Builder’ default value, thus derived classes could override it and implement with their own default ‘Request Builder’.

-        This design conforms to the OCP.

-        In addition, we added the virtual BuildRequest() method which invokes the BuildRequest() method of the current ‘Request Builder’.

-        This polymorphic design conforms to the LSP, since derived types could use this method and won’t need to create their own implementation.

-        In addition the BuildRequest() method was set as virtual in case future BOs would like to modify it, e.g. add relevant log messages.


using DesignPrinciplesExamples.LSP.RequestBuilders;
using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Employee : Person
    {
        public Employee(BaseRequestBuilder requestBuilder = null) :
            base(requestBuilder)
        {
        }


        // Overrindg the 'SetDefaultRequestBuilder()' method in case the
        // requestBuilder property wasn't provided in the constructor.

        // This method is invoked in the 'Person' base class's constructor.
        protected override BaseRequestBuilder SetDefaultRequestBuilder()
        {
            return new EmployeeRequestBuilder();
        }
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders;
using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.BOs
{
    public class Supplier : Person
    {
        public Supplier(BaseRequestBuilder requestBuilder = null) :
            base(requestBuilder)
        {
        }

        // Overrindg the 'SetDefaultRequestBuilder()' method in case the
        // requestBuilder property wasn't provided in the constructor.

        // This method is invoked in the 'Person' base class's constructor.
        protected override BaseRequestBuilder SetDefaultRequestBuilder()
        {
            return new SupplierRequestBuilder();
        }
    }
}


      Description:

-        Both the Employee and Supplier classes derive from the Person class and implemented the SetDefaultRequestBuilder() method with their own default ‘request builders’.


Remark: Open-Closed Principle

As stated above, I mentioned that this design conforms to the OCP, since we could extend the behavior of the Person, Employee and Supplier objects, in order to use a different builder, without modifying their code, only by implementing a new builder that derives from BaseRequestBuilder and use it instead.

However, if we’ll deeply examine these classes, we could notice that their ‘SetDefaultRequestBuilder()’ methods are not closed for modification, since in case we’d like to use another ‘default builder’, we’ll have to modify these methods.

We could probably create another object (e.g. CreateDefaultRequestBuilder) that sets the ‘default builder’ for each BO respectively, perhaps by using configuration for each BO.

This kind of decision should be addressed with respect to the particular requirements, meaning, whether we expect the ‘default builder’ to consistently stay the same, or will it be frequently changed, and the implementation should be accordingly.

In case we predict it to change frequently, we should support the OCP, and implement such an object: CreateDefaultRequestBuilder.

Otherwise, it would be over design & over engineering.




2.    Builders implementation:

namespace DesignPrinciplesExamples.LSP.RequestBuilders.Base
{
    public abstract class BaseRequestBuilder
    {
        public abstract string BuildRequest();
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class PersonRequestBuilder : BaseRequestBuilder
    {
        public override string BuildRequest()
        {
            // TODO: Need to implement.
            return null;
        }
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class EmployeeRequestBuilder : BaseRequestBuilder
    {
        public override string BuildRequest()
        {
            // TODO: Need to implement.
            return null;
        }
    }
}


using DesignPrinciplesExamples.LSP.RequestBuilders.Base;

namespace DesignPrinciplesExamples.LSP.RequestBuilders
{
    public class SupplierRequestBuilder : BaseRequestBuilder
    {
        public override string BuildRequest()
        {
            // TODO: Need to implement.
            return null;
        }
    }  
}


Description:

-        In this implementation we created an inheritance hierarchy between the builders, in order to create uniformity between all the request builders.

-        In addition, this conforms to the OCP, since in case of a change in the builders’ requirements, we would only need to extend the code, and implement a new proper builder. 


Remark:
In case we’ll get the builder from someone else (3rd party) that didn’t derive from our BaseRequestBuilder, we could always write a proper wrapper or adapter to that builder class, which would derive from our BaseRequestBuilder and thus would conform to our design.



3.    The ServiceManager class implementation:


using DesignPrinciplesExamples.LSP.BOs;

namespace DesignPrinciplesExamples.LSP
{
    public class ServiceManager
    {
        public void InvokeService(Person person)
        {
            var request = person.BuildRequest();


            // TODO: Need to implement.

            //try
            //{
            //    service.Invoke(request);
            //}
            //catch (System.Exception)
            //{
            //    throw;
            //}
        }
    }
}


Description:

-        We could see that in this implementation we are using polymorphism and only invoking the person’s BuildRequest() method, which invokes the proper builder.






Inheritance & the LSP


How should we determine a proper inheritance?

Meaning, how do we know whether a particular class should inherit from another base class?


Well, I would say that this question is at the heart of designing a solution that conforms to the LSP.

When designing a class that inherits from another base class, the derived class would inherit all the data members and functionality of the base class (in addition to its own new data members and functionality).

Sometimes, it’s very easy to identify a proper inheritance, but other times, not quite.

Meaning, as young developers, when we learned object oriented programing, we were taught that a class should inherit from another class if it is considered the same as the base class, but with additional functionalities.

E.g. an Employee Is A Person, thus it should inherit all its functionality.

However, this is not quite accurate, since some designs that rely on the Is A relationship could miss the behavior of both classes, meaning their behavior is different, even though they are the same.

For instance, conceptually, an Employee is the same as Person, however in our application (based on the requirements), an Employee behaves differently, e.g. a Person uses a service to receive all persons and the Employee class doesn’t need this behavior.

One thing that could indicate different behaviors is that the derived class inherits data members and functionality that it doesn’t need.

A proper inheritance that conforms to the LSP should be considered if the derived class has the same behavior as the base class.

This way, we could use polymorphism without breaking the code!





Summary

We managed to illustrate a simple use of the Liskov Substitution Principle and to understand its importance when designed correctly in our application.

We saw how, by creating a design that does NOT conform to the Liskov Substitution Principle, it would also violate the Open-Closed Principle, and our application would eventually suffer from software design smells.

Finally, we understood that in order to create a proper inheritance between our objects, they don’t have to be conceptually the same, rather they should have the same behavior.

To conclude, I would say that the LSP is very important to our design, in order to use object oriented polymorphism correctly, without breaking our code, and most importantly we need to remember that the S.O.L.I.D design principles are well-used when they are applied together and across the entire codebase.


---
Next in the SOLID series is the Interface Segregation Principle, which describes how to design small and efficient interfaces, as opposed to big and inefficient interfaces, which would lead to undesired coupling between the interface’s clients, and eventually to software design smells.




The End

Hope you enjoyed!
Appreciate your comments…

Yonatan Fedaeli

No comments: