The “L” in SOLID stands for Liskov Substitution Principle, named after Barbara Liskov, who initially introduced in 1987 keynote address. LSP provides guidelines for creating inheritence heirachy where a client can use any class or subclass without breaking the expected behavior.
We will begin by looking at the official definition first.
If S is a subtype of T, then objects of type T may be replaced with objects of type S,without breaking the program.
It would be unwise to explain this theoratically, hence like always, let hit the Visual Studio and write some code. I would like borrow an example given by Tim Correy in his famous video.
Assume we have an Employee class defined as follows.
public class Employee { public string Name{get;set;} public Employee AssignedManager{get;protected set;} public virtual double GetPay() { var currentSalary = 500; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } public virtual void AssignManager(Employee manager) { Console.WriteLine("Manager Assigned"); AssignedManager = manager; } }
Now let’s assume we have a Manager class, which inherits from Employee
public class Manager:Employee { public override double GetPay() { var currentSalary = 750; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } public override void AssignManager(Employee manager) { Console.WriteLine("Manager Assigned"); AssignedManager = manager; } }
Let’s write some client code now which consumes these classes.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new Employee(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
Output
Current Pay 750 Current Pay 500 Manager Assigned
As you can observe, the output is perfectly fine. Let us try making some changes to the Client Code now. Let us replace the Employee class (programmer instance) with Manager.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new Manager(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
Output
Current Pay 750 Current Pay 750 Manager Assigned
This has turned out fine as well. Yes the output is different, which is expected considering the Pay for Manager and Employee are different, but the client hasn’t behaved unexpectedly. Let’s write one more class, inheriting from Employee and call it CEO.
public class CEO:Employee { public override double GetPay() { var currentSalary = 750; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } public override void AssignManager(Employee manager) { throw new Exception("Am the CEO, no Manager for me !!"); } }
The difference this time is of course the CEO doesn’t have a Manager and it raises an exception in the AssignManager method. Let’s head back to out Client code and replace Employee with CEO in our original code.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new CEO(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
Output
Exception:Am the CEO, no Manager for me !!
Aha, we have an exception here. When replaced the Employee (Base type) with a subtype (CEO), the expected behavior of program has been broken. This is a violation of LSP.
Let’s examine the violation further. It is often said that Inheritence is the IS-A relation. However, if you truely examine CEO and Employee, do they follow a IS-A relation in every sense ? Not really, not according the current definition of Employee. Yes, that is right. The problem doesn’t quite lie in CEO, but it originate from the definition of Employee class. While designing the Employee class, we “assumed” that all Employees would have a manager.
So what is the best solution for this scenario ? What if we were to ensure the Employee class had only those functionalities which are common to every employee ? Obviously, the Manager is not applicable for everyone, so let’s remove it and create an interface which contains only the basic employee functionalities.
public interface IBaseEmployee { string Name{get;set;} double GetPay(); } public abstract class BaseEmployee:IBaseEmployee { public string Name{get;set;} public virtual double GetPay(){ return 0;} }
Remember, we still have to consider the Manager assignment. What we would do here is define another interface IHasManager, which would wrap the functionalities required for Employees who has Manager.
public interface IHasManager { IBaseEmployee AssignedManager{get;set;} void AssignManager(IBaseEmployee manager); }
Let’s now define our Employee and Manager classes.
public class Employee:BaseEmployee,IHasManager { public IBaseEmployee AssignedManager{get; set;} public override double GetPay() { var currentSalary = 500; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } public virtual void AssignManager(IBaseEmployee manager) { Console.WriteLine("Manager Assigned"); AssignedManager = manager; } } public class Manager:BaseEmployee,IHasManager { public IBaseEmployee AssignedManager{get; set;} public override double GetPay() { var currentSalary = 750; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } public void AssignManager(IBaseEmployee manager) { Console.WriteLine("Manager Assigned"); AssignedManager = manager; } }
As you can see, the Manager and Employee inherits from BaseEmployee and implements the IHasManager interface. The next step is to define our CEO Class, and this time, we know that he doesn’t need a Manager.
public class CEO:BaseEmployee { public override double GetPay() { var currentSalary = 750; Console.WriteLine($"Current Pay {currentSalary}"); return currentSalary; } }
That’s it. Let’s rewrite our Client code now.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new Employee(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
The above code compiles and executes as expected. Let’s replace the Employee with a Manager first.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new Manager(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
That works fine as well. Let’s try again, and this time replace with CEO.
var manager = new Manager(); manager.Name = "John Doe"; manager.GetPay(); var programmer = new CEO(); programmer.Name = "Roel Doel"; programmer.GetPay(); programmer.AssignManager(manager);
Not surprisingly, that doesn’t compile at all. CEO doesn’t have the AssignManager method as it doesn’t implement the IHasManager interface. Your client no longer have unexpected behavior as such errors are caught during compilation.
That was pretty easy, isn’t it ? But we are not done yet. There is more to the definition of LSP. In the next part of this series, we will examine the Contact and Variance rules which must be followed for LSP compliance.
2 thoughts on “SOLID : Liskov Substitution Principle (Part 1)”