What is Liskov Substitution Principle?
It means if we have class A, and B is a subclass of A, then we should be able to replace objects of type A with objects of type B without breaking the behavior of the program.
class Employee {
}
class SoftwareEngineer: Employee {
}
Let's say, there's a method that accepts parameter of type Employee, we should still be able to pass a SoftwareEngineer instance on that method.
func promote(employee: Employee) {
}
promote(employee: Employee())
promote(employee: SoftwareEngineer())
There are more rules to check to make sure we don't violate the Liskov Substitution Principle. Here's a list of rules that we need to take into consideration when overriding methods of superclass.
Parameters in a method of a subclass should match or be more abstract than the one in superclass. Return types in a method of a subclass should match or be more abstract than the one in superclass.
A subclass should not throw any type of exception in which the superclass is not expected to throw.
A bad example violating LSP is when you override a method and simply throws an exception because you don't want your subclass to use that method. Or, you added a condition in your subclass and if it's not met, you throw an exception.
A subclass should not enforce a stricter input rule.
This means that subclasses should be able to accept parameter values that the superclass accepts.
Simple example would be if the superclass' method accepts any string, then, a subclass should not have any rules / stricter validation when accepting strings - like character count, or no spaces, or should not be alphanumeric, etc.
A subclass should not weaken post-conditions.
I could not think of a good example, but let me try. Example, we want to compute the median pace in our app. In the superclass, we are only saving 10 values in a list and then we compute the median pace from the list, after that, all values in the list are deleted. But in our subclass, if we don't delete all values after computing for median, then, we are violating LSP, because we have weakened the post-condition (which is to delete all values in a list) and may have a different result when we use the superclass or the subclass when computing for median pace.
The invariants of a superclass should be maintained.
A safe way to extend a class is to introduce new fields and methods and not mess with any existing fields of the superclass.
A subclass shouldn't change values of a private field of the superclass.
Apparently, this is possible with other programming languages allowing subclasses to access a superclass' private variables and modify it.
The return values of the overridden method must comply with the same rules as the superclass. Which means we can apply a stricter rule for the return values. Opposite of the input rule in #4.
A subclass can return a smaller subset than the superclass, so we can add an output validation.
Extending concrete classes
I would like to make it clear that in LSP, we are referring to a concrete class subclassing another concrete class.
Although, creating subclasses are not highly recommended, it doesn't mean we cannot use it. There might be some instances that we have to subclass, and when that happens, always remember this principle. The subclasses should be able to replace the objects of their superclass without breaking the behavior of the program.
Extending from interfaces / protocol
However, it is safer to implement interfaces / protocols than extending from superclasses. Interfaces don't have rules if we implement its methods, so we don't have to worry about that. You just have to implement it as is, and the types should be the same as defined in the interface.
Updating the Employee example above:
enum EmployeeType {
case engineer
case admin
}
protocol Employee {
var id: String { get set }
var type: EmployeeType { get set }
func foo()
}
We can have different types of employee to extend from Employee protocol:
struct SoftwareEngineer: Employee {
var id: String = UUID().uuidString
var type: EmployeeType = .engineer
func foo() {}
}
struct HR: Employee {
var id: String = UUID().uuidString
var type: EmployeeType = .admin
func foo() {}
}
If we want to promote an employee, then we can send any employee of any type as long as it extends from the Employee protocol.
promote(employee: HR())
promote(employee: SoftwareEngineer())
Now, we are able to achieve the same thing where we want to make sure that we can pass any type of Employee.
Before I end this post, I'd like to let you guys know that the list of requirements listed here to satisfy LSP are taken from the books "Dive Into Design Patterns" by refactoring.guru, and Clean Mobile Architecture by Petros Efthymiou summed up together. Both are great books, with illustrations and real examples to help us understand the topic more, and the topics are discussed very well such that the readers are able to understand it easily. I'd recommend this book to anyone who'd like to level-up their understanding on Design Patterns.
Comments