The Liskov Substitution Principle — and why you might want to enforce it

James Ellis-Jones
HackerNoon.com
Published in
5 min readMay 2, 2019

--

This is probably the most technical of the 5 SOLID principles and the one I would guess fewest people consider much. However, it does have some important consequences for designing object-oriented software.

The very formal way in which it is expressed doesn’t help:

Let φ(x) be a property provable about objects x of type T. Then φ(y) should also be true for objects y of type S where S is a subtype of T.

Barbara Liskov and Jeanette Wing wrote this in a paper in 1994.

This rule helps us avoid subtle bugs when we are writing code which is abstract. It will be easier to understand this by looking at an example.

public abstract class ApiConnection
{
public abstract T[] ReadData<T>(Filter filter);
}
public class SimpleApiConnection : ApiConnection
{
public virtual T[] ReadData<T>(Filter filter)
{
... gets data from api ...
}
}
public class TokenAuthApiConnection : SimpleApiConnection
{
private object authToken;
public virtual void GetAuthToken(string tokenUrl)
{
... gets auth token ...
this.authToken = authTokenFromApi;
}
public override T[] ReadData<T>(Filter filter)
{
... get data using this.authToken ...
}
}

There are three classes here: the abstract ApiConnection class because it has no code, has no properties other than it’s type signature. The ‘properties provable’ about this class are therefore only properties about its typing which the compiler knows about and enforces on its inheritors. So we can be sure that the inheritors also have these properties.

Then we have the class. This is inherited by the TokenAuthApiConnection. We can say the ReadData() method does not require any other method to be called before it in order not to throw an exception. This is a ‘property provable’ about SimpleApiConnection. However the TokenAuthApiConnection class does require the GetAuthToken() method to be called before ReadData(), or the connection will not succeed. Hence the Liskov Substitution Principle is broken.

What does this mean? It means that any code using SimpleApiConnection and relying on being able to safely call ReadData() will break if the compiler-allowed substitution of a TokenAuthApiConnection is made when calling that code. That code cannot call GetAuthToken because it doesn’t exist on SimpleApiConnection and so the TokenAuthApiConnection substitution will never work as the call will never be authenticated.

This is a pretty obvious violation, but they can be much more subtle and insidious in the way they cause errors.

The LSP allows us to use polymorphism reliably

If we can rely on the LSP, it allows us to use polymorphism in our code.

Wikipedia has a good article on polymorphism. We’re talking about the parametric or subtype forms of polymorphism in the context of the LSP. Polymorphism is a key way to avoid repetition in code as it allows you to maximize the generality of the code you are writing. In the language of the LSP definition, you want to write code that puts the minimum requirements in terms of provable properties on the objects the code is processing.

When we write a function that takes an argument T, polymorphism lets us use T or any subtype of T as that argument. That means if we define a type or interface T which expresses the minimum we need to know about our argument in order for the function to work with it, we can write a function which is as widely applicable as possible. An example, you want to build an HTML select list from a list of people:

public SelectList MakeSelectList(IEnumerable<Person> people)
{
var selects = new SelectList();
foreach (var person in people)
{
selects.Add(person.Id, person.Name);
}
}

Clearly, you could make this function more general as it can operate on more than just people. So now we create an interface which expresses the minimum constraint upon our argument:

public interface INamedEntity
{
int Id { get; set; }
string Name { get; set; }
}
public SelectList MakeSelectList(IEnumerable<INamedEntity> entities)
{
var selects = new SelectList();
foreach (var entity in entities)
{
selects.Add(entity.Id, entity.Name);
}
}

The problem here is that we expect certain things from a Select List, so there are implicit constraints on the ‘properties provable’ about an IEnumerable<INamedEntity> which are not enforced in this code. Can you see what they are? Stop reading here for a moment if you want to try!

A select list requires that not only the Id but the Name is unique for each item in the list. Otherwise, the user is presented with multiple select list items which look the same, which is clearly wrong. This leads us on to the problem of how to enforce the LSP.

Enforcing the LSP

When we get to the D of SOLID, Dependency Inversion, we’ll find that we are recommending the use of abstractions in our core code to enable client code to provide the dependencies it should use. Typically in the big Object Oriented languages, this is done with an interface or abstract class. However, as we have seen it is highly likely our core code will rely implicitly on ‘properties provable’ about our abstraction which are not enforced on concrete implementations of it.

That is asking for trouble. The client code can introduce implementations which will break our core code, and it can be very tricky to handle the errors which can potentially be generated without a lot of thought.

Mainstream languages lack much ability to enforce the LSP beyond ensuring that the expected fields, properties, and methods exist. In my specialty, .Net, there exists a feature called Code Contracts, which allow for preconditions and postconditions to be specified for methods. These will at worst catch runtime issues of consistency, and static code analysis can highlight violations of these conditions which can be checked before runtime.

A fix for our select list example would look like this:

public SelectList MakeSelectList(IEnumerable<INamedEntity> entities)
{
Contract.Requires(entities.GroupBy(e => e.Id).Count() == entities.Count() && entities.GroupBy(e => e.Name).Count() == entities.Count());
var selects = new SelectList();
foreach (var entity in entities)
{
selects.Add(entity.Id, entity.Name);
}
}

The somewhat complex `Requires` expression is just checking for no duplicate `Id` or `Name` values. You can find a good article on implementing this here.

Another approach to dealing with this issue is to write a set of unit tests which can be applied to any class created in client code to ensure it won’t break the core code into which it’s injected. This article has a nice technique for this you may be able to use.

This is one of a series of articles about the SOLID principles. The previous one on the Open/Closed Principle is here. The next one on the Interface Segregation Principle is here.

--

--