SOLID design principles in .NET: the Liskov Substitution Principle

After visiting the letters ‘S‘ and ‘O‘ in SOLID it’s time to discuss what ‘L’ has to offer. L stands for the Liskov Substitution Principle (LSP) and states that you should be able to use any derived class in place of a parent class and have it behave in the same manner without modification. It ensures that a derived class does not affect the behaviour of the parent class, i.e. that a derived class must be substitutable for its base class.

The principle is named after Barbara Liskov who first described the problem in 1988.

More specifically substitutability means that a caller that communicates with an abstraction, i.e. a base class or an interface, should not be aware of and should not be concerned with the different concrete types of those abstractions. The client should be able to call BaseClass.DoSomething() and get a perfectly usable answer regardless of what the concrete class is in place of BaseClass. For this to work the derived class must also “behave well”, meaning:

They must not remove any base class behaviour

They must not violate base class invariants, i.e. the rules and constraints of a class, in order to preserve its integrity

The first point means the following: if a base class defines two abstract methods then a derived class must give meaningful implementations of both. If a derived class implements a method with ‘throw new NotImplementedException’ then it means that the derived class is not fully substitutable for its base class. It is a sign that the base class is ‘NOT-REALLY-A’ base class type. In that case you’ll probably need to reconsider your class hierarchy.

All who study OOP must at some point come across the ‘IS-A’ relationship between a base class and a derived class: a Dog is an Animal, a Clerk is an Employee which is a Person, a Car is a vehicle etc. LSP refines this relationship with ‘IS-SUBSTITUTABLE-FOR’, meaning that an object is substitutable with another object in all situations without running into exceptions and unexpected behaviour.

Demo

As usual in this series on SOLID we’ll start with some code which violates LSP. We’ll then see why it’s bad and then correct it. The demo is loosely connected to the one we worked on in the SRP and OCP posts: an e-commerce application that can refund your money in case you send back the item(s) you purchased. At this company you can pay using different services such as PayPal. Consequently the refund will happen through the same service as well.

Open Visual Studio and create a new console application. We’ll start off with an enumeration of the payment services:

public enum PaymentServiceType
{
PayPal = 1
, WorldPay = 2
}

It would be great to explore the true web services these companies have to offer to the public but the following mockup APIs will suffice:

We concentrate on the Refund logic which the two services carry out slightly differently. What’s common is that the MakeRefund methods return a string that describes the result of the action.

We’ll eventually need a refund service that interacts with these API’s somehow but it will need some object that represents the payments. As the payments can go through the two services mentioned above, and possible others in the future, we’ll need an abstraction for them. An abstract base class seems appropriate:

Each concrete Payment class will communicate with the appropriate payment service to log on and request a refund. This follows the Adapter pattern in that we’re wrapping the real API:s in our own classes. We’ll need to be able to identify the correct payment type. In the previous post we used a variable called IsMatch in each concrete type – here we’ll take the Factory approach just to see another way of selecting a concrete class:

We get the payment type using the factory. We then immediately need to check its type in order to be able to assign values to the the different properties in it. There are multiple problems with the current implementation:

We cannot simply take the payment object returned by the factory, we need to check its type – therefore we cannot substitute the subtype for its base type, hence we break LSP. Such if-else statements where you branch your logic based on some object’s type are telling signs of LSP violation

We need to extend the if-else statements as soon as a new provider is implemented, which also violates the Open-Closed Principle

We need to extend the serviceResponse.Contains bit as well if a new payment provider returns a different response, such as “OK”

The client, i.e. the RefundService object needs to intimately know about the different types of payment providers and their internal setup which greatly increases coupling

The client needs to know how to interpret the string responses from the services and that is not the correct approach – the individual services should be the only ones that can do that

The goal is to be able to take the payment object returned by the factory and call its Refund method without worrying about its exact type.

First of all let’s introduce a constructor in each Payment class that force the clients to provide all the necessary parameters:

The input parameters are hard-coded to keep things simple. In reality these can be read from a configuration file or sent in as parameters to the GetPaymentService method. We can now improve the RefundService class as follows:

We got rid of the downcasting issue. We now need to do something about the need to inspect the strings in the Contains method. This if statement still has to be extended if we introduce a new payment service and the client still has to know what “Success” means. If you think about it then ONLY the payment service objects should be concerned with this type of logic. The Refund method returns a string from the payment service but instead the string should be evaluated within the payment service itself, right? Let’s update the return type of the PaymentBase object:

There’s no need to downcast anything or to extend this method if a new service is introduced. Strict proponents of the Single Responsibility Principle may argue that the Payment classes are now bloated, they should not know how to process the string response from the web services. However, I think it’s well worth refactoring the initial code this way. It eliminates the drawbacks we started out with. Also, in a Domain Driven Design approach it’s perfectly reasonable to include the logic belonging to a single object within that object and not anywhere else.

A related principle is called ‘Tell, Don’t Ask‘. We violated this principle in the initial solution where we asked the Payment object about its exact type: if you see that you need to interrogate an object about its internal state in order to branch your code then it may be a candidate for refactoring. Move that logic into the object itself within a method and simply call that method. Meaning don’t ask an object about its state, instead tell it to perform what you want it do.

Andras, please is the PaymentFactory class another implementation of the Factory pattern, or you expecting the reader to implement it based on examples given. I am asking this because, it’s obvious we will have to extend the switch statement in case of a new payment option. Thanks

Hi David,
It’s an implementation of the factory pattern so it will need to be extended with new implementations of PaymentBase.
In the post on factories you’ll find another example of finding all implementing types using Reflection instead.
//Andras

Hi Andras,
Very nice articles on SOLID principles and very helpful for a beginner like me.
While going through this article I think there is a small mistake, I think in PayPalPayment class the statement should be if (response.Contains(“Auth”)) and for WorldPayPayment class the line should be replaced with if (response.Contains(“Success”)), else while executing the program will always return false.

My goal with this blog is to offend everyone in the world at least once with my words… so no one has a reason to have a heightened sense of themselves. We are all ignorant, we are all found wanting, we are all bad people sometimes.