Previously we took a dive into solid principles including the single responsibility and the open/closed principle.
The Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping,
Supposing object S is a subtype of object T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of T.
Suppose we have the Employee class.
package com.gkatzioura.solid.liskov; public class Employee { public void work() { System.out.println("Employee is working"); } }
Also we have another class which inherits the Employee class.
package com.gkatzioura.solid.liskov; public class EmployeeOnVacation extends Employee { @Override public void work() { throw new IllegalArgumentException("Employees on vacation should not work"); } }
Supposing that we have a project.
package com.gkatzioura.solid.liskov; import java.util.List; public class Project { public void start(List<Employee> employees) { for(Employee employee:employees) { employee.work(); } } }
And we assign our employees to start working on it
List<Employee> employees = new ArrayList<>(); employees.add(new EmployeeOnVacation()); employees.add(new Employee()); Project project = new Project(); project.start(employees);
The outcome would be an exception due to the employee who is on vacation and thus the project will not be completed.
The employee on vacation is an employee however he will not work. Even if the method didn’t throw an exception the method work would do nothing and this would affect delivering our project since our actucal team’s velocity is not the one we originally thought it was.
In order to avoid violating the principle we shall use a different approach and change the class hierarcy.
We will change the original employee class.
package com.gkatzioura.solid.liskov; public class Employee { public String getTitle() { return "The employee's title"; } }
And we will make make two different employee interfaces.
The WorkingEmployee interface.
package com.gkatzioura.solid.liskov; public interface WorkingEmployee { public void work(); }
And the non working employee interface.
package com.gkatzioura.solid.liskov; public interface NonWorkingEmployee { void relax(); }
Then the project will use only employees who are implementations of the WorkingEmployee interface and extend the employee class.
package com.gkatzioura.solid.liskov; public class WorkingEmployeeImpl extends Employee implements WorkingEmployee { @Override public void work() { } }
package com.gkatzioura.solid.liskov; import java.util.List; public class Project { public void start(List<WorkingEmployee> workingEmployees) { for(WorkingEmployee workingEmployee:workingEmployees) { workingEmployee.work(); } } }
List<WorkingEmployee> employees = new ArrayList<>(); employees.add(new WorkingEmployeeImpl()); Project project = new Project(); project.start(employees);
You can find the source code on github. Next principle is the interface segregation principle.
Also I have compiled a cheat sheet containing a summary of the solid principles.
Sign up in the link to receive it.
I’ve been reading this article as well as the one on interface segragation and I am a bit puzzled about them, because I think they fail to focus the essence of the problem here, which is the semantical aspect of OOP, or to put it differently, the meaning of the extends and implements clauses from a semantical point of view.
In your original example of Employee you state that an employee is someone who can work, or by using a more radical statement, all instances of Employee can work. Yet in your implementation of EmployeeOnVacation you state that an employee on vacation is an employee (through the extends clause) who cannot work (through the overriding of method work).
So there is a clear semantical contradiction here: either EmployeeOnVacation is an Employee, and she should be able to work, or she isn’t, and the inheritance is wrong.
Of course there are plenty of examples like that. Think of the remove() method of an Iterator returned by an immutable collection, that refuses to remove an element.
But anyway to sum up, sure your example throw an exception, sure it is a violation of the Liskov principle, but all of that is due to a wrong understanding of what is an employee and a flawed modelling.
Also I wanted to point out that your example is quite unfortunate because being on vacation or not should probably not be modelled through inheritance, since it is a state of an employee, unless you have employees who are known to be on vacation from the day they are hired until they quit.
Also don’t throw a
new IllegalArgumentException(“Employees on vacation should not work”);
There is nothing wrong with the arguments of the call, since there are none. You should rather use UnsupportedOperationException.
I agree with every word, Sam.
On the other hand, I accept that some examples need to be simplified, even forced, to create a scenario that fits.