Liskov Substitution Principle (LSP) is a slick one. Violating this principle leads up to bugs which are hard to spot since the overriding subclasses change the internal state of the instance of the parent in such way so parent’s representation and the semantics of it is broken. Let’s have a look at the following Cat class from our hypothetical Farm game:
public class Cat {
private AnimalSound sound;
public Cat(AnimalSound animalSound) {
this.sound = animalSound;
}
public void sing() {
soundEffect(sound);
}
}
In our game we have cats singing. How they sing is determined by AnimalSound which is the internal state of Cat we maintain, and defines its behaviour, respectively. Everthing looks so far good. We can now instantiate new Cat objects and let them sing as follows:
Cat cat1 = new Cat(AnimalSoundLib.MEOW);
cat1.sing(); // meooww
Cat cat2 = new Cat(AnimalSoundLib.MEOOO);
cat2.sing(); // meoooo
// more cats
Now, our game grows and we need new animals in our game’s universe. Let’s imagine that the farm is located right next to the woods and there are bigger cats like lions, leopards, etc. Since lions are also cats - “is a” relationship, we can easily create a subclass of Cat type for Lion, right?
public class Lion extends Cat {
public Lion(AnimalSound animalSound) {
super(animalSound);
}
public void hunt() {}
// and other lion related methods.
}
Now, let’s create a new lion instance which roars:
Lion lion = new Lion(AnimalSoundLib.ROAR);
lion.sing(); // roarrr
But, the problem comes to light once you attempt to use the Lion instance in place of a Cat instance while relying on runtime polymorphism. Let’s have a Bar of Cats in which we only expect cute little meowing cats singing happily after work:
public class CatBar {
private List<Cat> catFriends = new ArrayList<>();
public void enter(Cat cat) {
catFriends.add(cat);
}
public void singTogether() {
for (Cat cat: catFriends) {
cat.sing();
}
}
}
At some level in our application, we rely on a cat service which gives us cat objects back and of which implementation is contributed from an external Cat services team. Since the cat service library is introduced as a dependency in our project, we just want to use the service to have some cats without knowing internal implementation details of it. It is also legitimate to have Lion instances out of catService, since lions are also cats!
Cat cat = catService.getCat(); // a real cat created which meows
catbar.enter(cat);
Cat lion = catService.getCat(); // this time the service gives you a lion instance back.
catbar.enter(lion);
catbar.singTogether(); // party begins: meoww moewww ROOARRR meow
But, what happens if the service returns a Lion object, which is technically a Cat and a valid return type, and we pass it to our CatBar where only meowing cats are expected to enter? Now, we should expect to have a trouble since we changed the semantics of the Cat instance by overriding its internal representation from a subclass as we set the cat’s sound to a ROAR. Now, we will not be able to use the Cat instance above since it roars!
LSP tells us, we should be able to use the subclasses of class A, for instance, class B which extends class A in place of A, everywhere in our program without breaking the program’s behaviour.
The behavior of Cat instance has changed. On the other hand, runtime polymorphism allows to use subclasses of Cats, like Lion in our example, as Cats where we need which makes such an assignment valid. So, we violated the LSP which tells us, we should be able to use the subclasses of class A, for instance, class B which extends class A in place of A, everywhere in our program without breaking the program’s behaviour. In our CatBar, therefore, we don’t only hear meows but also roars out of our CatBar.