Liskov substitution principle in C#

Every programmer who works with object-oriented programming languages absolutely must know SOLID principles, which are the following:

  • Single responsibility principle
  • Open-closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

In this article, we will cover Liskov substitution principle. I will explain why this principle is important and will provide an example of its usage in C#.

What is Liskov substitution principle

Liskov substitution principle was initially introduced by Barbara Liskov, an american computer scientist, in 1987. The principle states that if you substitute a sub-class with any of its derived classes, the behavior of the program should not change.

This principle was introduced specifically with inheritance in mind, which is an integral feature of object oriented programming. Inheritance allows you to extend the functionality of classes or modules (depending on what programming language you use). So, if you need a class with some new functionality that is closely related to what you already have in a different class, you can just inherit from the existing class instead of creating a completely new one.

When inheritance is applied, any object oriented programming language will allow you to insert an object that has the the derived class as its data type into a variable or parameter that expects and object of the sub class. For example, if you had a base class called Car, your could create another class that inherits from it and is called SportsCar. In this case, an instance of SportsCar is also Car, just like it would have been in real life. Therefore and variable or parameter that is of type Car would be able to be set to an instance of SportsCar.

And this is where a potential problem arises. There may be a method or a property inside the original Car class that uses some specific behavior and other places in the code have been written to expect that specific behavior from the instances of Car objects. However, inheritance allows those behaviors to be completely overridden.

If the derived class overrides some of the properties and methods of the sub class and modifies their behavior, then passing an instance of the derived class into the places that expect the sub class may cause unintended consequences. And this is exactly the problem that Liskov substitution principle was designed to address.

Implementing Liskov substitution principle in C#

In our previous article where we have covered open-closed principle, we have ended up with a solution that reads textual content of a file, encloses every paragraph in P HTML tags and makes conversion of certain Markdown markers into equivalent HTML tags.

And this is what we have ended up with.

We have TextProcessor base class that performs the basic processing of paragraphs in the text that has been passed to it:

using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace TextToHtmlConvertor
{
    public class TextProcessor
    {
        private const string openingParagraphTag = "<p>";
        private const string closingParagraphTag = "</p>";

        public virtual string ConvertText(string inputText)
        {
            var paragraphs = Regex.Split(inputText, @"(\r\n?|\n)")
                              .Where(p => p.Any(char.IsLetterOrDigit));

            var sb = new StringBuilder();

            foreach (var paragraph in paragraphs)
            {
                if (paragraph.Length == 0)
                    continue;

                sb.AppendLine(openingParagraphTag + paragraph + closingParagraphTag);
            }

            sb.AppendLine("<br/>");

            return sb.ToString();
        }
    }
}

And we have a class that derives from it, which is called MdTextProcessor. It overrides ConvertText() method by adding some processing steps to it. Basically, it checks the text for specific Markdown markers and replaces them with corresponding HTML tags. Both the markers and the tags are fully configurable via a dictionary.

using System.Collections.Generic;

namespace TextToHtmlConvertor
{
    public class MdTextProcessor : TextProcessor
    {
        private readonly Dictionary<string, (string, string)> tagsToReplace;

        public MdTextProcessor(Dictionary<string, (string, string)> tagsToReplace)
        {
            this.tagsToReplace = tagsToReplace;
        }

        public override string ConvertText(string inputText)
        {
            var processedText = base.ConvertText(inputText);

            foreach (var key in tagsToReplace.Keys)
            {
                var replacementTags = tagsToReplace[key];

                if (CountStringOccurrences(processedText, key) % 2 == 0)
                    processedText = ApplyTagReplacement(processedText, key, replacementTags.Item1, replacementTags.Item1);
            }

            return processedText;
        }

        private int CountStringOccurrences(string text, string pattern)
        {
            int count = 0;
            int currentIndex = 0;
            while ((currentIndex = text.IndexOf(pattern, currentIndex)) != -1)
            {
                currentIndex += pattern.Length;
                count++;
            }
            return count;
        }

        private string ApplyTagReplacement(string text, string inputTag, string outputOpeningTag, string outputClosingTag)
        {
            int count = 0;
            int currentIndex = 0;

            while ((currentIndex = text.IndexOf(inputTag, currentIndex)) != -1)
            {
                count++;

                if (count % 2 != 0)
                {
                    var prepend = outputOpeningTag;
                    text = text.Insert(currentIndex, prepend);
                    currentIndex += prepend.Length + inputTag.Length;
                }
                else
                {
                    var append = outputClosingTag;
                    text = text.Insert(currentIndex, append);
                    currentIndex += append.Length + inputTag.Length;
                }
            }

            return text.Replace(inputTag, string.Empty);
        }
    }
}

This structure implements open-closed principle quite well, but it doesn’t implement Liskov substitution principle. And although the overridden ConvertText() makes a call to the original method in the base class and calling this method on the derived class will still process the paragraphs, the method implements some additional logic, which may produce completely unexpected results. I will demonstrate this via a unit test.

So, this is a test I have written to validate the basic functionality of the original ConvertText() method. We initialize an instance of TextProcessor object in the constructor, pass some arbitrary input text and then check whether the expected output text has been produced.

using TextToHtmlConvertor;
using Xunit;

namespace TextToHtmlConvertorTests
{
    public class TextProcessorTests
    {
        private readonly TextProcessor textProcessor;

        public TextProcessorTests()
        {
            textProcessor = new TextProcessor();
        }

        [Fact]
        public void CanConvertText()
        {
            var originalText = "This is the first paragraph. It has * and *.\r\n" +
                "This is the second paragraph. It has ** and **.";

            var expectedSting = "<p>This is the first paragraph. It has * and *.</p>\r\n" +
                "<p>This is the second paragraph. It has ** and **.</p>\r\n" +
                "<br/>\r\n";

            Assert.Equal(expectedSting, textProcessor.ConvertText(originalText));
        }
    }
}

Please note that we have deliberately inserted some symbols into the input text that have a special meaning in Markdown document format. However, TextProcessor on its own is completely agnostic of Markdown, so those symbols are expected to be ignored. The test will therefore happily pass.

As our textProcessor variable is of type TextProcessor, it will happily be set to an instance of MdTextProcessor. So, without modifying our test method in any way or changing the data type of textProcessor variable, we can assign an instance of MdTextProcessor to the variable:

using System.Collections.Generic;
using TextToHtmlConvertor;
using Xunit;

namespace TextToHtmlConvertorTests
{
    public class TextProcessorTests
    {
        private readonly TextProcessor textProcessor;

        public TextProcessorTests()
        {
            var tagsToReplace = new Dictionary<string, (string, string)>
                {
                    { "**", ("<strong>", "</strong>") },
                    { "*", ("<em>", "</em>") },
                    { "~~", ("<del>", "</del>") }
                };

            textProcessor = new MdTextProcessor(tagsToReplace);
        }

        [Fact]
        public void CanConvertText()
        {
            var originalText = "This is the first paragraph. It has * and *.\r\n" +
                "This is the second paragraph. It has ** and **.";

            var expectedSting = "<p>This is the first paragraph. It has * and *.</p>\r\n" +
                "<p>This is the second paragraph. It has ** and **.</p>\r\n" +
                "<br/>\r\n";

            Assert.Equal(expectedSting, textProcessor.ConvertText(originalText));
        }
    }
}

The test will now fail. The output from ConvertText() method will change, as those Markdown symbols will be converted to HTML tags. And this is exactly how other places in your code may end up behaving differently from how they were intended to behave.

However, there is a very easy way of addressing this issue. If we go back to our MdTextProcessor class and change the override of ConvertText() method into a new method that I called ConvertMdText() without changing any of its content, our test will, once again, pass.

using System.Collections.Generic;

namespace TextToHtmlConvertor
{
    public class MdTextProcessor : TextProcessor
    {
        private readonly Dictionary<string, (string, string)> tagsToReplace;

        public MdTextProcessor(Dictionary<string, (string, string)> tagsToReplace)
        {
            this.tagsToReplace = tagsToReplace;
        }

        public string ConvertMdText(string inputText)
        {
            var processedText = base.ConvertText(inputText);

            foreach (var key in tagsToReplace.Keys)
            {
                var replacementTags = tagsToReplace[key];

                if (CountStringOccurrences(processedText, key) % 2 == 0)
                    processedText = ApplyTagReplacement(processedText, key, replacementTags.Item1, replacementTags.Item1);
            }

            return processedText;
        }

        private int CountStringOccurrences(string text, string pattern)
        {
            int count = 0;
            int currentIndex = 0;
            while ((currentIndex = text.IndexOf(pattern, currentIndex)) != -1)
            {
                currentIndex += pattern.Length;
                count++;
            }
            return count;
        }

        private string ApplyTagReplacement(string text, string inputTag, string outputOpeningTag, string outputClosingTag)
        {
            int count = 0;
            int currentIndex = 0;

            while ((currentIndex = text.IndexOf(inputTag, currentIndex)) != -1)
            {
                count++;

                if (count % 2 != 0)
                {
                    var prepend = outputOpeningTag;
                    text = text.Insert(currentIndex, prepend);
                    currentIndex += prepend.Length + inputTag.Length;
                }
                else
                {
                    var append = outputClosingTag;
                    text = text.Insert(currentIndex, append);
                    currentIndex += append.Length + inputTag.Length;
                }
            }

            return text.Replace(inputTag, string.Empty);
        }
    }
}

And we still have our code structured in accordance with single responsibility principle, as the method is purely responsible for converting text and nothing else. And we still have 100% saturation, as the new method fully relies on the existing functionality from the base class, so our inheritance wasn’t pointless.

We are still acting in accordance with open-closed principle, but we no longer violate Liskov substitution principle. Every instance of derived class will have the base class functionality inherited, but all of the existing functionality will work exactly like it did in the base class. So, using objects made from derived classes will not break any existing functionality that relies on the base class.

Conclusion

Liskov substitution principle is a pattern of coding that will help to prevent unintended functionality from being introduced into the code when you extend existing classes via inheritance.

However, certain language features may give less experienced developers an impression that it’s OK to write code in violation of this principle. For example, virtual keyword in C# may seem like it’s even encouraging people to ignore this principle. And sure enough, when and abstract method is overridden, nothing will break, as the original method didn’t have any implementation details. But a virtual method wold have already had some logic inside of it; therefore overriding it will change the behavior and would probably violate Liskov substitution principle.

The important thing to note, however, is that a principle is not the same as a law. While a law is something that should always be applied, a principle should be applied in the majority of cases. And sometimes there are situations where violating a certain principle makes sense.

Also, overriding virtual methods in C# won’t necessarily violate Liskov substitution principle. The principle will only be violated if the output behavior of the overridden method changes. Otherwise, if the override merely changes the class variables that are used in other methods that are only relevant to the derived class, Liskov substitution principle will not be violated.

So, whenever you need to decide whether or not to override a virtual method in C#, use common sense. If you are confident that none of the components that rely on the base class functionality will be broken, then go ahead and override the method, especially if it seems to be the most convenient thing to do. But try to apply Liskov substitution principle as much as you can.

Leave a Reply

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