The Liskov Substitution Principle (LSP) applies to inheritance in such a way that the derived classes must be completely substitutable for their base classes. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.
Several circumstances may indicate that the LSP has been violated:
A subclass changes the behavior of the parent class.
A subclass overrides the methods of the parent class but throws an exception.
A subclass overrides the methods of the parent class, but with an empty implementation.
A subclass inherits methods from the parent class but is not used.
There are two ways to solve this problem:
Breaking the hierarchy.
Tell, don’t ask.
Example 1 - a subclass changes the behavior of the parent class
To illustrate this, we will go with a classic example of squares and rectangles that people often use to explain LSP because it is very simple and easy to understand.
Looking at the example above, we see that all operations are very reasonable. Since a square has 2 sides, every time we set the length of one side, we reset the length of remaining side.
However, the Square class inherits from the Rectangle class but the Square class has different shapes and it changed the behavior of the Rectangle class, resulting in the LSP violation.
As the example above, because class Square inherits from class Rectangle, we can use the following:
publicclassLSPExample1 {publicvoidexample2() {Rectangle rect =newSquare();rect.setWidth(5);rect.setHeight(10);System.out.println(rect.calculateArea()); // 100 }}
The result is obviously wrong, the area of the rectangle should be 5 * 10 = 50.
According to this principle, we must ensure that when a class inherits from another class, it will not change the behavior of that class.
In this case, to avoid the LSP violation, we have to create a superclass, e.g. Shape class, and then for Square and Rectangle to inherit this Shape class.
The above implementation classes have no problem, everything runs fine. Now we add a new SystemFileService class. With the requirement not allowed to delete the file system, this class will generate an UnsupportedOperationException error. A method that is designed but not used is not a good design either.
When executing the deleteFiles() method, the SystemFileService class throws an error at runtime. It cannot replace its parent FileService class, so it is already LSP violation.
interfaceCrudRepository<T,ID> {Iterable<T> findAll();TfindOne(ID id);Tsave(T entity);voidupdate(T entity);voiddelete(ID id);}classUserRepositoryimplementsCrudRepository<User,Long> { @OverridepublicIterable<User> findAll() {// find all users ...return list; } @OverridepublicUserfindOne(Long id) {// find user by id ...return user; } @OverridepublicUsersave(User entity) {// save user ...return savedUser; } @Overridepublicvoidupdate(User entity) {// update user ... } @Overridepublicvoiddelete(Long id) {// Set flag to delete user }}
The above implementation looks good. However, we now have a MasterDataRepository, with requirements that the master data is read-only, cannot be added, modified, or deleted. We have the implementation as follows:
classMasterDataRepositoryimplementsCrudRepository<MasterData,Long> { @OverridepublicIterable<MasterData> findAll() {// find master data ...return list; } @OverridepublicMasterDatafindOne(Long id) {// find master data by id ...return masterData; } @OverridepublicMasterDatasave(MasterData entity) {// Do nothing, don't allow to add master datareturnnull; } @Overridepublicvoidupdate(MasterData entity) {// Do nothing, don't allow to delete master data } @Overridepublicvoiddelete(Long id) {// Do nothing, don't allow to delete master data }}
Because class MasterDataRepository inherits from class CrudRepository, we can use the following: