SOLID Principles In C# - Liskov Substitution Principle

Introduction

C# is an object-oriented programming language. These days whenever you talk about object-oriented programming you hear the acronym, SOLID. These are five design principles introduced by Michael Feathers to make our object-oriented applications easy to understand, maintain, and expand as future requirements change. Today, we will look at the third principle with an example. I covered the first two principles in my previous articles.

The SOLID principles

There are five principles to follow to ensure our application meets the SOLID requirements. These are as below.

  1. Single Responsibility Principle (SRP)
  2. Open Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

The Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) states that an instance of a child class must replace an instance of the parent class without affecting the results that we would get from an instance of the base class itself. This will ensure the class and ultimately the whole application is very robust and easy to maintain and expand, if required. Let us look at this with an example.

Let us create a new .NET Core 3.1 console application in Visual Studio 2019 Community Edition as below.

Create new project

Configure project

Inside this project, I have created a new class file “LiskovSubstitutionPrinciple”. In this file, I create the following base and child classes.

// Not following the Liskov Substitution Principle  
public class AccessDataFile  
{  
    public string FilePath { get; set; }  
    public virtual void ReadFile()  
    {  
        // Read File logic  
        Console.WriteLine($"Base File {FilePath} has been read");  
    }  
    public virtual void WriteFile()  
    {  
        //Write File Logic  
        Console.WriteLine($"Base File {FilePath} has been written");  
    }  
}  
  
public class AdminDataFileUser : AccessDataFile  
{  
    public override void ReadFile()  
    {  
        // Read File logic  
        Console.WriteLine($"File {FilePath} has been read");  
    }  
  
    public override void WriteFile()  
    {  
        //Write File Logic  
        Console.WriteLine($"File {FilePath} has been written");  
    }  
}  
  
  
public class RegularDataFileUser : AccessDataFile  
{  
    public override void ReadFile()  
    {  
        // Read File logic  
        Console.WriteLine($"File {FilePath} has been read");  
    }  
  
    public override void WriteFile()  
    {  
        //Write File Logic  
        throw new NotImplementedException();  
    }  
}   

This class does not follow the “Liskov Substitution Principle” as whenever we create an instance of the RegularDataFileUser class and try to replace the AccessDataFile base class, we will get an error when we try to call the “WriteFile” function. However, this works fine from the base class directly. Hence, the Liskov Substitution Principle is not followed in this design.

//Calling class not following Liskov Substitution Principle  
  
AccessDataFile accessDataFile = new AdminDataFileUser();  
accessDataFile.FilePath = @"c:\temp\a.txt";  
accessDataFile.ReadFile();  
accessDataFile.WriteFile();  
  
AccessDataFile accessDataFileR = new RegularDataFileUser();  
accessDataFileR.FilePath = @"c:\temp\a.txt";  
accessDataFileR.ReadFile();  
//accessDataFileR.WriteFile();  // Throws exception  

We can fix this as below,

// Following the Liskov Substitution Principle    
    
    public interface IFileReader     
    {    
        void ReadFile(string filePath);    
    }    
    
    public interface IFileWriter     
    {    
        void WriteFile(string filePath);    
    }    
    
    public class AdminDataFileUserFixed : IFileReader, IFileWriter    
    {    
        public void ReadFile(string filePath)    
        {    
            // Read File logic    
            Console.WriteLine($"File {filePath} has been read");    
        }    
    
        public void WriteFile(string filePath)    
        {    
            //Write File Logic    
            Console.WriteLine($"File {filePath} has been written");    
        }    
    }    
    
    public class RegularDataFileUserFixed : IFileReader    
    {    
        public void ReadFile(string filePath)    
        {    
            // Read File logic    
            Console.WriteLine($"File {filePath} has been read");    
        }    
    }    

In the above code, we see that we have created two separate interfaces for the file reader and file writer. Now our classes would implement only the interfaces needed and then the interfaces would be replaceable without any issues as below,

//Calling class following Liskov Substitution Principle  
  
IFileReader fileReader = new AdminDataFileUserFixed();  
fileReader.ReadFile(@"c:\temp\a.txt");  
  
IFileWriter fileWriter = new AdminDataFileUserFixed();  
fileWriter.WriteFile(@"c:\temp\a.txt");  
  
IFileReader fileReaderR = new RegularDataFileUserFixed();  
fileReaderR.ReadFile(@"c:\temp\a.txt");  

Summary

In this article, we have looked at implementing the Liskov Substitution Principle (SRP) in a practical example. I would recommend you look through your existing classes and identify places where you have violated this principle and then think of ways to fix it. This will help to get you thinking in terms of applying this principle and help you to apply it to your code in the future as well. In my next article, we will look at the Interface Segregation Principle.


Similar Articles