Thus far in my journey to explain the why of the so-called SOLID principles I've covered Single Responsibility Principle and Open/Closed Principle. This brings us to "L" for Liskov Substitution Principle which the originator, Barbara Liskov, describes as:
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
Whoa! Math meets English and kicks its... well, you know. Robert Martin boils it down a bit:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
If we have a consuming function that takes a Customer class, it shouldn't matter if that gets a GoldCustomer specialization. We don't want, in our consumer code, to rely on the is or as keywords to branch our consumer accordingly.
// For shame!
void Consumer(Customer customer)
{
if (customer is GoldCustomer)
{
var goldCustomer = customer as GoldCustomer;
goldCustomer.DoSomethingElse();
}
else
{
customer.DoSomething();
}
}
Seems sensible, but recall: we're dealing with the why and operating under the strict belief that it's unacceptable to invoke these principles without understanding their deeper reasons and benefits. It's particularly important that we be able and ready to articulate the reasons for these tried truisms as we mentor new developers and win entrenched developers over to known better ways of doing things.
Mind Your Coupling
A lot of these principles come down to coupling and cohesion. LSP is all about controlling coupling. Notice how in the code example we've touched on two types? Customer and GoldCustomer are both used by our (rather creatively named) Consumer method. So what if we add a PlatinumCustomer? We've created a permalink between our consumer and a wider-than-desired surface area of our customer model.
Respect the OCP
As Robert Martin points out, ignoring LSP is a clear violation of "the Open-Closed principle because [that function] must be modified whenever a new derivative of the base class is created." So LSP and OCP are intrinsically linked, and, if you remember, the why behind OCP has to do with stability and moving forward by only ever adding new behavior. If I have to change the Consumer function every time I create a specialization of Customer is that function closed to modification? Clearly not.
Fragile APIs
Ignoring this has particularly nasty effects when we're writing code or an API to be consumed by someone else. By "someone else" I mean either:
- Another team in the same timeline.
- Another developer that has to come along and maintain our code.
- Ourselves way down the timeline; we'd have to remember this bit of code exists!
LSP must be taken into account when we design our class hierarchies. We have to respect this principle upstream and within the context of our layered architectures. I believe LSP and everything it supports form part of the justification for another principle: favor composition over inheritance.
Sure inheritance seems conceptually useful when we're inside the model or framework using it, but when we think about our system as a whole -- external consumers, future dependents -- and apply LSP, that initial perceived value diminishes quickly. Since it's so hard to get these "is a" relationships right and they are fewer and further between than we might initially think, why not just avoid it and treat our classes as a kind of surgical tray? Small, single-purpose instruments that can collaborate to solve a number of problems...
Leveraging DbC
Design-by-Contract and LSP go hand-in-hand. DbC's notion of preconditions and postconditions have import when we delve into the mire of class hierarchies. A subclass may override a parent method only under certain conditions:
- Preconditions can only be weaker.
- Postconditions can only be stronger.
If you want to take advantage of DbC features in the future -- and I think a lot of us will -- your class hierarchies need to meet this guideline. They should meet it already right now, but a DbC framework should prove this at compile time. DbC gets us closer to the promised power of static languages with verifiably correct software, which has the benefit of achieving a high bar of initial quality after the development process while lightening our testing and specification burden.
Posted
Mon, Sep 22 2008 11:52 AM
by
Dave Laribee