Uncle Bob‘s aptly coined SOLID Design Principles form the basis of a robust software application. Today, I want to talk about one of those principles, the Liskov’s Substitution Principle (LSP) because it’s easy to deviate from, and a few conscious design choices can prevent us from doing so.
In the simplest terms, LSP suggests that:
Any change that makes a subtype not replaceable for a supertype, should be avoided.
Suppose, we have a class hierarchy like so:
At the first glance, the relationships here seem fine, but if we carry out an
IS-A test, the issue becomes obvious: that
Tea isn’t necessarily a
CaffeinatedDrink (for instance: there’s decaf!).
Thus, this design violates LSP, because it indicates that all Teas are Caffeinated Drinks. Now, a naïve approach would be to try to retrofit this design, to allow for decaf teas as well — by adding a flag or suchlike — but that would be clumsy!
There are several ways to deal with this anomaly, and the decision can be based on the stage of development we’re at, along with other factors. So, let’s continue with our example and see how can it be dealt with:
- We know for sure that we’d need to pull
Teaout of this hierarchy. Though
Coffeelooks more justified there, we can pull that out as well, to keep things crisp (and also because someone told you about ‘Decaf Coffee’ as well!).
- A better option, thus, seems to be:
- For common behaviour of
Coffees, introduce a
Coffeecan then be subtypes of
Caffeinatedcan just be an interface which is implemented as needed
Upon this change, we don’t cringe anymore to say that
Caffeinatedbehaviour. Whereas, a
DecafTeadiffers from it. Another perspective could be,
Coffeeis substitutable both for
DecafTeais substitutable ONLY for a
- Another approach is to follow Effective Java [Bloch, 2017, Item 18]: Favor composition over inheritance. With this,
Drinkbecomes a member of
Caffeinated(interface) is implemented by all but, say,
Here, we do away with the class hierarchy, and directly use the concrete instances of individual drinks. However, we do keep the
Caffeinated behaviour separated, and again, can safely say that
Caffeinated drink. Moreover, we’re also getting a more robust design because of disallowing (class-based-) inheritance.
- How do we ensure we come-up with LSP-compliant design? Well, there are few simple things that can be borne in mind while working on class associations:
- Intuition: Is it sounding right? [Example: Should
Student, when all it wants is to access some
- Concatenation test: Do the Parent and Child types sound right upon concatenation? [Example: While
Birdmay sound correct, a
Chickenmay not. So does ‘
Flyer‘ need to be a class type or an interface type?], and finally and most importantly,
- IS-A test: Is the IS-A condition holding good?