Liskov Substitution Principle

This is the third post in covering the SOLID Principles, if you have not already gone through the first two covering Single Responsibility and Open Closed Principles, please do! This principle builds on some of what I’ve covered previously. I am going to go over what the Liskov Substitution Principle (LSP) is and why it matters to writing quality code. LSP uses abstraction and inheritance, but in a different way than the Open Closed Principle does. If you haven’t caught on yet, these principles work with one another and are there as guidelines to help us write more flexible and maintainable code.

Liskov Substitution Principle in Action

In the previous post, I covered the Open Closed Principle and we had an example of using an abstract base class for sending messages. We could also have used an Interface to achieve the same. Below is the abstract class for Message

public abstract class Message
{
    protected Customer Customer { get; private set; }
 
    public Message(Customer customer)
    {
        Customer = customer;
    }
    public abstract void SendMessage();
}
 
public class Customer
{
    public int Id;
    public string FirstName;
    public string LastName;
    public string Email;
    public string Phone;
}

Note that in C# we cannot directly implement an abstract class, abstract classes must be inherited from and cannot be used directly. This is one use of inheritance such as how we used in the Open Closed Principle post

When using LSP we also use base classes or interfaces, but in a different way. LSP states that “Derived classes should be substitutable for their base classes (or interfaces)” and that “Methods that use references to base classes (or interfaces) have to be able to use methods of the derived classes without knowing about it or knowing the details.” What does this mean exactly? Well in practice it means we will take instances of our specific class or interfaces and use those to be passed to methods or other classes, but the class does not care about the actual implementation, only that it is of a specific type.

So let’s take a look at some code for a common need, we are going to look at logging.

public  interface ILogger
{
    void Log(string message);
}
 
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

This should all hopefully make sense, and if we wanted to we could make an additional implementation using the ILogger interface, such as a FileLogger, or if you use elastic search of some sort, an ElasticSearchLogger implementation could be created. Keep in mind these are all pretty simple implementations as I want to give an understanding of the ideas and not get too bogged down in actual details.

Using Our New Interface

Below is a snippet showing how we could use our new logging interface in our abstract class, we have protected field which is set using our class constructor, this is the base class and the SendTextMessage class shows that we are using the same field. The type is of our ILogger interface, which means that we can pass any type of logger as long as it inherits from the ILogger interface.

public abstract class Message
    {
        protected Customer Customer { get; }
        protected ILogger Logger { get; }
 
        public Message(Customer customer, ILogger logger)
        {
            Logger = logger;
            Customer = customer;
        }
 
        public abstract void SendMessage();
    }
 
    public class SendTextMessage : Message
    {
        public SendTextMessage(Customer customer, ILogger logger)
            : base(customer, logger)
        {
        }
 
        public override void SendMessage() 
        {
            //Send the message via text...
            //log the message being sent
            Logger.Log($"Sending {Customer.FirstName} a text message");
        }
    }

So in practice then we could call our new code by using something like the following:

var consoleLogger = new ConsoleLogger();
var cust = new Customer()
{
   Id = 1,
   FirstName = "Nick",
   Phone = "1-303-666-9999"
};
 
var customerTextMessage = new SendTextMessage(cust, consoleLogger)
customerTextMessage.SendMessage();

And if we had a different type of logger class for example FileLogger which uses our ILogger interface we could just use that in substitution of our ConsoleLogger. Hopefully, that makes sense and as always please leave comments or questions!

Happy Coding!

2 thoughts on “Liskov Substitution Principle”

Leave a Reply

Your email address will not be published. Required fields are marked *