Importance of interface segregation principle in C#

If you are a software developer primarily working with object-oriented programming languages, you absolutely must be familiar with SOLID principles. Nothing will help you to write clean code as much as SOLID principles would.

For those who aren’t yet familiar with what SOLID principles are, here is the list:

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

In my previous posts, I have already covered the importance of single responsibility principle, open-closed principle and Liskov substitution principle and have shown how to use all of them in C#. In this post, I will cover interface segregation principle.

What is interface segregation principle

In object-oriented programming, interfaces are used to define signatures of methods and properties without specifying the exact logic inside of them. Essentially, they act as a contract that a class must adhere to. If class implements any particular interface, it must contain all components defined by the interface as its public members.

Interface segregation principle states that if any particular interface member is not intended to be implemented by any of the classes that implement the interface, it must not be in the interface. It is closely related to single responsibility principle by making sure that only the absolutely essential functionality is covered by the interface and the class that implements it.

And now, we will see why interface segregation is important.

Importance of interface segregation

We will continue with the same code that we have ended up with after we have implemented Liskov substitution principle in the previous article.

So, we have a base class called TextProcessor that converts paragraphs in the input text into HTML paragraphs by applying relevant tags to them.

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();
        }
    }
}

We also have a derived class, MdTextProcessor, that adds a new processing capability – the ability to detect specific Markdown symbols and convert them into corresponding HTML tags. To make sure that any places in the code that accept original TextProcessor class don’t start to behave differently if an instance of MdTextProcessor passed instead, the additional functionality is implemented via a new method – ConvertMdText().

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);
        }
    }
}

Currently, neither of the classes implement any interfaces. But we may want to be able to use similar functionality in other classes. Perhaps, we would want to convert input text into different formats, not just HTML. Perhaps, we want to simply be able to mock the functionality in some unit tests. So, it would be beneficial to us to get our classes to implement some interfaces.

As our MdTextProcessor has two methods, ConvertText(), which is inherited from TextProcessor and ConvertMdText(), which is it’s own, we can have an interface like this:

namespace TextToHtmlConvertor
{
    public interface ITextProcessor
    {
        string ConvertText(string inputText);
        string ConvertMdText(string inputText);
    }
}

And implement it like this:

using System.Collections.Generic;

namespace TextToHtmlConvertor
{
    public class MdTextProcessor : TextProcessor, ITextProcessor
    {
        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);
        }
    }
}

So far so good. However, there is a problem. If we want to implement this interface in the original TextProcessor class, we will now have to add ConvertMdText() method to it. However, this method is not relevant to this particular class. So, to let the programmers know that this method is not intended to be used, we get it to throw NotImplementedException.

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

namespace TextToHtmlConvertor
{
    public class TextProcessor : ITextProcessor
    {
        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();
        }
        public string ConvertMdText(string inputText)
        {
            throw new NotImplementedException();
        }
    }
}

And that introduces a problem. We have ended up with a method that we will never use. And what if we would want to create other derived classes that are designed to deal with text formats other than MD? We will then have to expand our interface and keep adding new unused methods to every class that implements it.

So, we can implement the following solution. Our base interface will only have a single method that the base class will use:

namespace TextToHtmlConvertor
{
    public interface ITextProcessor
    {
        string ConvertText(string inputText);
    }
}

The good news that, in C#, we can use inheritance for interfaces, just like we can use it for classes. So, we can create another interface that will be relevant to those classes that are specific to processing of MD-formatted text:

namespace TextToHtmlConvertor
{
    public interface IMdTextProcessor : ITextProcessor
    {
        string ConvertMdText(string inputText);
    }
}

And so, we would implement the interface by the base class without having to add any new methods:

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

namespace TextToHtmlConvertor
{
    public class TextProcessor : ITextProcessor
    {
        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 our derived class will remain clean too:

using System.Collections.Generic;

namespace TextToHtmlConvertor
{
    public class MdTextProcessor : TextProcessor, IMdTextProcessor
    {
        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);
        }
    }
}

When NotImplementedException is appropriate

In C#, NotImplementedException is an error type that is specifically designed to be thrown from the members that implement the interface, but aren’t intended to be used. And it comes directly from the core system library of C#.

At the first glance, it may seem that with this feature in place, the language was designed to violate interface segregation principle. However, there are situations where leaving unused interface methods in your class will be appropriate without necessarily violating this principle.

One situation where throwing NotImplementedException will be appropriate if your class is a work in progress and this has been put there temporarily. You may decide all the essential members for your class ahead of time and create the interface immediately, before you forget what you intended to do.

However, creating the class may take a relatively long time and you may not be able to do it all in one go. Despite this, you will already have to implement every interface member. Otherwise your code would not compile. And gradually, you will populate every member with some valid logic.

This is precisely why Visual Studio populates all auto-generated members with NotImplementedException if you want to auto-implement the interface. Clicking the action button will ensure that your code will compile immediately, while you can populate all the members with whatever logic you want at your own convenience.

And, because every interface member is intended to eventually be implemented and is absolutely required for the finished product, this will not violate interface segregation principle.

The other case is when you want to write an interface implementation to enable easy testing of your code. You may have an original class that is intended to be placed in production. This class may have various pieces of logic that you will not be able to replicate on development machine. For example, it may be sending requests to a certain server or reading messages from a certain queue. And in that class, every method and property is absolutely required to be there.

However, there may be some pieces of functionality in the class that you would be able to run anywhere. So, you may have version of the class that only contains those components, which will allow you to easily run certain tests on any machine.

And this is where NotImplementedException serves its purpose. In this context, it is there to notify the developer that, although this method or property is an absolutely essential member of the class under normal circumstances, it is not relevant in this specific context.

The alternative would be to mock those methods up. But doing so would give them logic that is completely different from how the production class behaves. Therefore, the behavior you will see in your tests will not be representative of the actual behavior of the deployed software and the tests may be pointless. The methods that are testable, however, will behave exactly as they would in production.