Pragmatic SOLID – Part 3 – The Liskov Substitution Principle

Subtypes must be substitutable for their base types – Robert C. Martin February 1997

TL;DR

  • Is-a is not enough to determine inheritance – Ensure the expected behaviour of your base class is preserved in derived classes.
  • Use concepts from design by contract principles to help you.
  • Violating the Liskov Substitution Principle usually leads to violating the Open Closed Principle.
  • Has the potential to consume development time by thinking too long and hard about class structures.

What’s the business value?

Reduces the potential for errors which can’t easily be caught at compile time. Applying the Liskov Substitution Principle will help produce more robust architectures.

Come again?

The Liskov Substitution Principle (LSP) provides guidance to help develop appropriate inheritance and polymorphism architectures. The main thrust of the principle is to extend the meaning of “is a” relationships (derivation/inheritance) by also thinking about “behaves the same as”. Application of the Liskov theory helps prevent violations of the Open Closed Principle, thus the two are tightly coupled.

When I started to read Robert C Martin’s book, I noticed the Liskov chapter had a markedly different feel to its predecessors. It starts with the predicate logic proposition from Barbara Liskov, followed by an example which looks very much like the Open Closed Principle. Thus, I aim to isolate an example for you that doesn’t blur the lines between the Liskov principle and the Open Closed Principle. I want to demonstrate the Liskov Principle in isolation or, at least, with the OCP obscured from view. Here’s Barbara’s logic preposition:

If for every 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 behaviour of P is unchanged when O1 is substituted for O2, then S is a subtype of T. – Barbara Liskov

Remove the techno-babble please!

Lets say we have a new class which can read a text file (ignoring the classes already provided in .NET!)

class TextFile
{  
   public virtual void Load(String fileName){ 
      /* loads the file given the fileName */ }
   public virtual long Size { 
      get; /* the size in bytes without moving position */
      private set; }
   public virtual string ReadLine() {
      /* reads the line of text and moves to the next line */ } 
}

Which is consumed by this function

void DumpFileSizesAndFirstLine(List<TextFile> files)
{
   foreach (TextFile file in files)
   {
       Debug.WriteLine("file size " + file.Size);
       Debug.WriteLine("first line: " + file.ReadLine());
   }
}

Imagine we found that some files were too big, so we needed a buffered version which was more memory efficient. We introduce the following class which only moves forward (in the text file) and is memory efficient.  The developer thinks “A buffered text file is-a text file”.

class BufferedForwardOnlyTextFile : TextFile
{
   public virtual void Load(String fileName)
   {
      // don't really load the file, just open it
      // so we keep memory low
   }

   public virtual long Size
   {
      // move our position to the end of the file 
      // to work out the size, then return it
   }

   public virtual string ReadLine()
   {
      // read the next line and move forward, keeping memory use low
   }
}

When DumpFileSizesAndFirstLine() is called with a BufferedForwardOnlyTextFile in the list of files,  the file will have moved to the end when Size is called.

Thus the first line of the text file is never produced in the debug window. From an Open/Closed point of view, we did not need to recompile the  function to introduce our class but we still have silently introduced a bug. This, I believe is a solid example of LSP violation which isn’t an obvious OCP violation.

Although BufferedForwardOnlyTextFile “is a” TextFile it is not the same behaviour. By introducing this new variant into the inheritance hierarchy, which has different behaviour, BufferedForwardOnlyTextFile cannot be substituted for a TextFile class. Consumers of all TextFile derivations would reasonably expect the Size method not to change behaviour like this.

Are there any ways in which we can detect violations?

One way of detecting a violation is by analysing the pre-conditions and post-conditions of the overridden methods in the derived class.

  • If a derivation has made preconditions of internal state or parameter validation stronger then it will likely break existing code.

E.g. If a drawing Shape class had a DoubleSize() method that permitted usage when no size was set, but a derived class threw an exception if size was unset.

  • If a derivation has made post conditions result in an internal state that is weaker than the expected behaviour then it is likely to break existing code.

E.g. If the DoubleSize() method above didn’t grow the shape at all.

So how would we solve our BufferedForwardOnlyTextFile problem?

The least effective solution would be to start testing the typeof() TextFiles to determine if it was a forward only implementation. That would violate the Open/Closed Principle. 

Another which is doomed to fail is if we extended the base class to accommodate the new buffering behaviour. Expecting clients to interrogate the base class to work out how to call methods is asking for trouble and just as bad as the above approach.  Doing so maybe an indication that you will violate LSP, OCP and SRP. The code could be telling you the derivation isn’t really “a type of the same thing”, especially if the change makes no sense to other derivations.

You could start looking at implementing interfaces, or just looking how .NET solves these problems. A text file is a concept in its own right. The mechanism which reads the text file is also a concept in its own right. There could be many ways in which to consume a text file. C#.NET achieves this separation through having a File class which provides StreamReader (derived from TextReader) classes. Classes like FileStream, BufferedStream further extend separation of concerns to enable gigabytes of data within text files to be parsed.

Further References

Liskov Substitution Principle article (C++)

Design by contract (Wikipedia)

Where’s the Middleton Rule? 

The only (vague) rule I can think of is:

  • If you think your derivation is not providing the same ‘obvious’ experience as your base class, you probably shouldn’t inherit.
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s