SOLID — From Zero To Potato 3
About Liskov Substitution Principle (LSP)
Today I will continue explain about Liskov Substitution Principle (LSP) in this article , you can read my previous article about Single-responsibility principle and Open/Closed Principle (OCP) there.
Before moving to the detail explain about LSP, we should know how LSP helpful in our daily jobs. LSP helps us model good inheritance hierarchies. It helps us prevent model hierarchies that don’t conform to the Open/Closed principle. Any inheritance model that adheres to the Liskov Substitution Principle will implicitly follow the Open/Closed principle.
Let jumps to the coding, by the way if you don’t understand about kotlin don’t worry LSP can implement in any language so you can try with your favourite one.
We have a Student
class that has a calculateGPA
method that calculates the GPA based on the student's grades:
open class Student(val grades: List<Double>) {
open fun calculateGPA(): Double {
// calculate the GPA based on the grades
}
}
After that we want to create a HonorsStudent
subclass that extends Student
and calculates the GPA based on a different set of rules:
class HonorsStudent(grades: List<Double>) : Student(grades) {
override fun calculateGPA(): Double {
// calculate the GPA based on a different set of rules for honors students
}
}
How do you think above new class HonorsStudent
class , it seems fine right? seem does not break ‘OCP’. But actually HonorsStudent
subclass violates the Liskov Substitution Principle because it changes the behavior of the calculateGPA
method from the base Student
class. This can cause problems if code that expects a Student
object is passed a HonorsStudent
object, as the behavior of the calculateGPA
method is now different.
To adhere to the LSP, we can modify the design of the classes. One way to do this is to create a new interface called GPA
that has a calculate
method:
interface GPA {
fun calculate(): Double
}
open class Student(val grades: List<Double>) : GPA {
override fun calculate(): Double {
// calculate the GPA based on the grades
}
}
class HonorsStudent(grades: List<Double>) : GPA {
override fun calculate(): Double {
// calculate the GPA based on a different set of rules for honors students
}
}
In this refactored version, we create a separate interface for the calculateGPA
method, called GPA
. The Student
and HonorsStudent
classes both implement this interface, and each provides its own implementation of the calculate
method.
This adheres to the Liskov Substitution Principle because any code that expects an object of type GPA
can be passed either a Student
object or a HonorsStudent
object, and the behavior of the calculate
method will be consistent across both objects.
By separating out the calculateGPA
behavior into a separate interface, we ensure that any subclass that implements that interface behaves consistently with other objects of that interface type.
Thank you for taking the time to read this article. I hope that my writing has been informative and thought-provoking.