This post is the third one in the SOLID principles series. The first post discussed the single responsibility principle and in the second post we discussed the open / closed principle. Next, as the title suggests, we will take a look at the principle represented by the letter L from the SOLID acronym. L is for the Liskov Substitution Principle (LSP).
In simple terms LSP requires that supertypes and subtypes be swappable without affecting the correctness of a program.
Barbara Liskov is an ACM Turing Award receiver and she introduced LSP during a keynote in 1987. LSP is a principle, similar to Design by Contract, where emphasis is placed on the contract a base class defines for its children to adhere to. Remember a contract is a term used to describe the assumptions we can explicitly make about a class.
Subtypes must be behaviorally substitutable for their base types. - Barbara Liskov
LSP requires that subclasses implement the same properties, methods and behavior of their parent classes. Inheritance hierarchies should not have any behavior that distinguishes parent and child classes from each other.
LSP provides a clear criteria for creating maintainable inheritance hierarchies. Below is a list of 6 benefits LSP brings to our applications when we adhere to its guidelines.
- LSP removes conditionals that distinguish between parent and child classes. Applications with fewer conditionals are easier to maintain.
- When we adhere to LSP we don't have any side effects rippling through our inheritance hierarchies.
- LSP guides us away from generalizing prematurely and so reduces the costs of incorrect abstractions.
- We know what to expect from subclasses that adhere to LSP and that makes them easier to use.
- LSP instills faith into our code base as we can always trust that subclasses strictly adhere to the contracts set by their superclasses.
- Testing subclasses can be simplified by making use of a mock subclass that adheres to LSP.
There is clearly value in following the guidelines set forth by LSP. Next we will look at practical ideas for making sure our programs have maintainable inheritance hierarchies.
There are 5 key concepts that make implementing LSP in our projects clear and simple.
Make sure that subclasses respond to every message in their publicly accessible interface. Subclasses must accept the same types of input and respond with the same types of output. Empty subclass methods is a sign that LSP has been violated.
Limitations on input parameters may be relaxed for subclasses. However, child classes may not relax the restrictions parent contracts set on return values.
Child classes may not modify an object's state in a manner that would not match up with how their base classes would modify state.
One example of this type of violation would be when a square class inherits from a rectangle class. The ability to change the shape's size would be a violation, since the size of a square means something significantly different than a rectangle.
Conditions that are true for superclasses must remain true for their subclasses. In other words, child classes may not have side effects that don't match up with their superclass.
Consider a class that works with files, if a superclass method leaves a file open or closed, then the child class needs to do so as well.
We need to be very careful when we introduce side effects via a child class. Side effects are often hard to find and debug.
Subclasses are not permitted to throw exceptions that don't match the type of exceptions thrown by their superclass.
We can test a common inherited contract with the use of a shared test. By including the shared test in every child class we can prove that the inherited contract is honored.
We may create testable mock-subclasses only when we strictly adhere to the guidelines that LSP provides. This mock object could simplify many tests where interaction with a specific child class is not required.
LSP encourages transparent swappable parent and child classes. Adherence to LSP yields maintainable inheritance hierarchies. Implementing swappable parent and child classes requires that we implement the same properties, methods and behaviors defined by the base classes we inherit from.
We are rewarded when we adhere to LSP with fewer conditionals, less side effects, consistent subclasses and simpler tests.
To apply the theory of LSP in practice it is helpful to remember to honor inherited contracts, protect inherited state and respect inherited conditions. Finally, use tests and mock objects to demonstrate that a parent class can be replaced by any of its child classes.
If your inheritance hierarchies have become inconsistent and unmaintainable, then OmbuLabs can help. We are professionals at applying the Liskov Substitution Principle where it is needed so your application can be a joy to work with again.
- L is for the Liskov Substitution Principle
- Liskov Substitution Principle in 3 Minutes
- Practical Object-Oriented Design, An Agile Primer Using Ruby
- Software Engineering Design & Construction Slides
- The Wrong Abstraction
- Liskov Substitution Principle
- The Liskov Substitution Principle Explained
- Refactoring Slides
- Liskov Substitution Principle Slides