Liskova Substitution Principle LSP
Meet Don Corleone (Ciao 👋🏽), the head of an Italian mafia family 🕵🏼♂️, he decides to hand over his empire to his youngest son Michael, however, Michael's decisions unintentionally puts the lives of his loved ones in grave danger.
Michael was supposed to behave like his father and not put any restrictions on his father's methods so that he wouldn't get into trouble with other families.
Derived classes must be substitutable for their base classes
Liskov Substitution Principle
Reading this principle we recognize two parts, the first part is about derived and base classes and the second one is about being substitutable.
A class being substitutable for a base class is about behaving as expected.
but wait! what does it mean to behave as EXPECTED!? let’s code 🧑🏻💻
Let's create a base view model interface...
interface ViewModelBaseInterface {
fun handleRetry()
fun showLoader()
}
Then we create a subtype of ViewModelBaseInterface
class SettingsViewModel: ViewModel(), ViewModelBaseInterface {
override fun handleRetry() {...}
override fun showLoader() {
// wait! I don't need loading
}
}
As you can observe, SettingsViewModel
does not need showLoader method, here we call SettingsViewModel
a Bad Substitutable class, Why? because it behaves unexpectedly and doesn't provide a proper implementation for showLoader method.
a good solution for this is to apply Interface Segregation on ViewModelBaseInterface
.
interface ViewModelBaseInterface {
fun handleRetry()
}
interface Loadable {
fun showLoader()
}
so if a subtype needs to show loader, it should implement a Loadable
interface, then the subtype will not have to ignore (not provide) a proper implementation of its supertype behaviors.
Law of Leaky Abstractions
Abstraction is a good thing to think about it while designing your types hierarchy, but at some point, you should not generalize (abstract) some behaviors by assuming that all subtypes will need this behavior and should provide a meaningful implementation!
All non-trivial abstractions to some degree, are leaky
Let’s see an example
interface FileInterface {
fun rename(newName: String)
fun changeOwner(ownerName: String, group: String)
}
We are assuming that any file should know how to rename itself, and how to change it’s owner. that’s fine, right?
Now I would like to create a DropboxFile subtype…
class DropboxFile(): FileInterface {
override fun rename() {...}
override fun changeOwner() {
// dropbox file should not allow changing a file owner
throw Exception("changing file owner is not allowed")
}
}
No one wants exceptions, so to avoid them we have to condition some behavior…
fun changeFileOwner(file: FileInterface, ownerName: String, group: String) {
// if the file is not DropboxFile call
if (file !is DropboxFile) {
file.changeOwner(ownerName, group)
}
}
here we are restricting the base type behavior, the expected behavior is any subtype of FileInterface
should be able to change a file owner! but in real life, if a dropbox file seems to prevent it, then we can call DropboxFile
a Bad Substitutable class.
How to solve it?
Segregate the FileInterface
interface FileInterface {
fun rename(newName: String)
}
interface ChangableFileOwner {
fun changeOwner(ownerName: String, group: String)
}
so DropboxFile
will not have the owner changing role, so anywhere I need to accept a file that has owner changing behavior I should replace FileInterface
with a ChangableFileOwner
fun changeFileOwner(file: ChangableFileOwner, ownerName: String, group: String) {
file.changeOwner(ownerName, group)
//as we can see we don't restrict/conditioning the inhertid behavior
}
the hierarchy will look like this:
Conclusion
- Some of Liskov's principle violations could be controversial.
- Derived class should provide a meaningful implementation for all the base behavior.
- Restricting/Conditioning a derived behavior makes the class a bad substitutable.
- LSP violation can accrue more in dynamically typed programming languages.
Peace ✌🏼