Do you REALLY know what SOLID principles means? Think again! (#3: Liskov Substitution Principle)

Liskov Substitution

Formal definition of Liskov substitution principle is –
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 sub-type of T.

Or we can say – If class A is a sub-type of class B, then we should be able to replace B with A without disrupting the behaviour of our program.

Another way we can describe this as – If a piece of code that depends on an object of type P should be able to operate properly with objects of type C, where C is a sub-type of P.

Or in another way – Sub-types must not change any super-type’s significant behaviour. Here, “significant behaviour” means behaviour upon which clients of those objects expressly depend.

While the other principles are concerned with coupling and cohesion, LSP stands alone in dealing with code correctness. The LSP is about semantic consistency in a type hierarchy. It’s not enough to implement the same public interface across said hierarchies, the behaviour should also be consistent.

English, Please!

Still cryptic, right?
Let me simplify with a code example:

public abstract class ApiConnection
{
  public abstract T[] ReadData<T>(Filter filter);
}

public class SimpleApiConnection : ApiConnection
{
  public virtual T[] ReadData<T>(Filter filter)
  {
    ... fetch 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)
  {
    ... use this.authToken to Connect to some service ...
    ... and use that connection to fetch data ...
  }
}

There are three classes here: the ApiConnection class is abstract; 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 type 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 SimpleApiConnection class implementing ApiConnection.
We see that ReadData() method does not require any other method to be called before it in order to perform properly. It can function on its own. This is ‘property provable’ or contract binding of SimpleApiConnection.

Now this class inherited by the TokenAuthApiConnection.
However the TokenAuthApiConnection class does require the GetAuthToken() method to be called before ReadData() to set authToken which is subsequently used in ReadData, or the connection will not succeed. But the ‘provable’ of this ‘property’ was that it will not depend on any other method to operate properly.

Hence it breaks the Liskov Substitution Principle.

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.

Another Way to Look at: Contracts

In language of contracts, Sub-types must respect the contracts of their super-types. Sub-types must pass the same set of contract tests that their super-types pass. Super-classes clients are constrained to their restricted domain, and sub-classes can extend this domain. All they have to do is preserving the “old behaviour” for the super-class in regular conditions. So that excludes what happens when contract is violated, because contract must not be violated.

For example the old client use super-class “Money” that prescribe “only positive values”. Thus using negatives are not allowed and raise exceptions, and should not be used. The subclass introduces the idea of debt; hence in that case negative money is allowed, as the precondition is weakened, which covers the case of no exceptions are raised anymore for negatives.

Now any contract tests meant to check “irregular behaviour” for the super-class (precondition violation) will behave differently for sub-classes. Because this negativity is not a violation anymore in the Sub-classes.

Input parameter and return value

An overridden method of a subclass needs to accept the same input parameter values as the method of the super-class. That means we can implement less restrictive validation rules, but we are not allowed to enforce stricter ones in our subclass. Otherwise, any code that calls this method on an object of the super-class might cause an exception, if it gets called with an object of the subclass. Similar rules apply to the return value of the method. The return value of a method of the subclass needs to comply with the same rules as the return value of the method of the super-class. We can only decide to apply even stricter rules by returning a specific subclass of the defined return value, or by returning a subset of the valid return values of the super-class.

The Liskov Substitution Principle extends the Open/Closed principle and enables us to replace objects of a parent class with objects of a subclass without breaking the application.
This requires all sub-classes to behave in the same way as the parent class.
But; these 2 principles are not same concept and achieving one doesn’t mean automatically achieving the other one.

Violating one principle but following the other: Follows LSP but not OCP

Lets look at some code that follows LSP but not OCP.

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

The code does LSP because the context can use LiskovBase without knowing the actual type. We might think this code follows OCP as well; but look closely, is the class really closed?
What if the doStuff method did more than just print out a line?

The truth is; it doesn’t follow OCP; because in this object design we’re required to override the code completely with something else. This opens up the cut-and-paste issues as we have to copy code over from the base class to get things working. The doStuff method sure is open for extension, but it wasn’t completely closed for modification.

Remedy: Applying template method pattern

We can apply the Template method pattern on the above scenario. Here is that one way to close the doStuff method for modification and making sure it stays closed by marking it with final keyword. That keyword prevents anyone from sub-classing the class further.

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

Now this code follows both OCP and LSP.

In essence, OCP says: If you will add new function, create a new class extending an existing one, rather than changing it.
LSP says: If you create a new class extending an existing class, make sure it’s completely interchangeable with its base.

Liskov substitution principle is about letting the user handle different objects that implement a super-type without checking what the actual type they are. This is inherently what polymorphism is about.This principle provides an alternative to do type-checking and type-conversion, that can get out of hand as the number of types grow, and can be achieved through pull-up refactoring or applying patterns such as Visitor.