The ‘L’ in SOLID

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:

  1. We know for sure that we’d need to pull Tea out of this hierarchy. Though Coffee looks 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 behavior of Teas & Coffees, introduce a Drink type
    • Both Tea and Coffee can then be subtypes of Drink
    • Caffeinated can just be an interface which is implemented as needed

    Upon this change, we don’t cringe anymore to say that Tea IS-A Drink, with Caffeinated behaviour. Whereas, a DecafTea differs from it. Another perspective could be, Coffee is substitutable both for Drink or Caffeinated, but DecafTea is substitutable ONLY for a Drink.

  2. Another approach is to follow Effective Java [Bloch, 2017, Item 18]: Favor composition over inheritance. With this, Drink becomes a member of Tea and Coffee, and Caffeinated (interface) is implemented by all but, say, DecafTea.

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 Tea/Coffee IS-A 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 StudentEnrollment really extend Student, when all it wants is to access some Student properties?]
  • Concatenation test: Do the Parent and Child types sound right upon concatenation? [Example: While Flyer+Bird may sound correct, a Flyer+Chicken may 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?