Why you must use single responsibility principle

Every software developer who uses object-oriented programming languages needs to be familiar with SOLID software design principles and know how to apply them in their daily job. For those who aren’t familiar with what they are, SOLID is an abbreviation that stands for the following:

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

In this particular post, we will focus on the first of these principles – single responsibility principle. I will explain it’s importance and provide some examples of it’s usage in C# code.

Incidentally, as well as being the first principle in the abbreviation, it is also the one that is the easiest to grasp, the easiest to implement and the easiest to explain. Arguably, it is also the most important principle from the list. So, let’s go ahead and find out what it is and why you, as a developer, absolutely must know it.

What is single responsibility principle

Single responsibility principle states that, for every self-contained unit of code (which is, usually, a class), there should be one and only one reason to change. In more simple language, this means that any given class should be responsible for only one specific functionality.

Basically, your code should be structured like a car engine. Even though the whole engine acts as a single unit, it consists of many components, each playing a specific role. A spark plug exists only to ignite the fuel vapors. A cam belt is there only to synchronize the rotation of the crankshaft and the camshafts and so on.

Each of these components is atomic (unsplittable), self-contained and can be easily replaced. So should be each of your classes.

Clean Code, a book that every developer should read (that you can get from here), provides an excellent and easy to digest explanation of what single responsibility principle is and provides some examples of it in Java. What I will do now is explain why single responsibility principle is so important by providing some C# examples.

Importance of single responsibility principle

For those who are not familiar with C#, this course will give you a basic introduction. Those who are familiar with the language will recognize that every C# application has a class that serves as an entry point, which is usually called Program.

Now, let’s imagine a basic console application that will read an input text from any specified text file, will wrap every paragraph in HTML p tags and will save the output in a new HTML file in the same folder that the input file came from. If we are to put entire logic into a single class, it would look something similar to this:

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

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

        static void Main()
        {
            try
            {
                Console.WriteLine("Please specify the file to convert to HTML.");
                var fullFilePath = Console.ReadLine();

                var inputText = ReadAllText(fullFilePath);

                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/>");
                WriteToFile(fullFilePath, sb.ToString());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }

        static string ReadAllText(string fullFilePath)
        {
            return System.Web.HttpUtility.HtmlEncode(File.ReadAllText(fullFilePath));
        }

        static void WriteToFile(string fullFilePath, string text)
        {
            var outputFilePath = Path.GetDirectoryName(fullFilePath) + Path.DirectorySeparatorChar +
                Path.GetFileNameWithoutExtension(fullFilePath) + ".html";

            using (StreamWriter file =
            new StreamWriter(outputFilePath))
            {
                file.Write(text);
            }
        }
    }
}

This code will work, but it will be difficult to modify.

Because everything is in once place, the code will take longer to read than it could have been. Of course, this is just a simple example, but what if you had a real-life console application with way more complicated functionality all in one place?

And it is crucially important that you understand the code before you make any changes to it. Otherwise, you will inadvertently introduce bugs. So, you will, pretty much, have to read the entire class, even if only a tiny subset of it is responsible for a particular functionality that you interested in. Otherwise, how would you know if there is nothing else in the class that will be affected by your changes?

Imagine another scenario. Two developers are working on the same file, but are making changes to completely different functionalities within it. Once they are ready to merge their changes, there is merge conflict. And it’s an absolute nightmare to resolve, because each of the developers is only familiar with his own set of changes and isn’t aware how to resolve the conflict with the changes made by another developer.

Single responsibility principle exists precisely to eliminate these kinds of problems. In our example, we can apply single responsibility principle by splitting our code into three separate classes, so as well as having Program class, we will also have FileProcessor and TextProcessor classes.

The content of our FileProcessor class will be as follows:

using System.IO;

namespace TextToHtmlConvertor
{
    public class FileProcessor
    {
        private readonly string fullFilePath;

        public FileProcessor(string fullFilePath)
        {
            this.fullFilePath = fullFilePath;
        }

        public string ReadAllText()
        {
            return System.Web.HttpUtility.HtmlEncode(File.ReadAllText(fullFilePath));
        }

        public void WriteToFile(string text)
        {
            var outputFilePath = Path.GetDirectoryName(fullFilePath) + Path.DirectorySeparatorChar +
                Path.GetFileNameWithoutExtension(fullFilePath) + ".html";

            using (StreamWriter file =
            new StreamWriter(outputFilePath))
            {
                file.Write(text);
            }
        }
    }
}

The content of the TextProcessor file will be as follows:

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>";

        private readonly FileProcessor fileProcessor;

        public TextProcessor(FileProcessor fileProcessor)
        {
            this.fileProcessor = fileProcessor;
        }

        public void ConvertText()
        {
            var inputText = fileProcessor.ReadAllText();

            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/>");
            fileProcessor.WriteToFile(sb.ToString());
        }
    }
}

And this is what remains of our original Program file:

using System;

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

        static void Main()
        {
            try
            {
                Console.WriteLine("Please specify the file to convert to HTML.");
                var fullFilePath = Console.ReadLine();

                var fileProcessor = new FileProcessor(fullFilePath);
                var textProcessor = new TextProcessor(fileProcessor);

                textProcessor.ConvertText();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}

Now, you have the entire text-processing logic is handled by TextProcessor class, while FileProcessor file is solely responsible for reading from files and writing into them.

This has made your code way more manageable. First of all, if it’s a specific functionality you would want to change, you will only need to modify the file that is responsible for that specific functionality and nothing else. You won’t even have to know how anything else works inside the app. Secondly, if one developer is changing how the text is converted by the app, while another developer is making changes to how files are processed, their changes will not clash.

While we’ve split text-processing and file-processing capabilities into their own single responsibility classes, we have left the minimal amount of code inside Program class, the application entry point. It is now solely responsible for launching the application, reading the user’s input and calling methods in other classes.

The concept of class cohesion

In our example, it was very clear where the responsibilities should be split. And, in most of the cases, the decision will be based on the same factor we have used – splitting responsibility based on atomic functional areas. In our case, the application was mainly responsible for two things – processing text and managing files; therefore we have two functional areas within it and ended up with a separate class responsible for each of these.

However, there will be situation where a clear-cut functional areas would be difficult to establish. Different functionalities sometimes have very fuzzy boundaries. This is where the concept of class cohesion comes into play to help you decide which classes to split and which ones to leave as their are.

Class cohesion is a measure of how different public components of a given class are inter-related. If all public members are inter-related, then the class has the maximal cohesion, while a class that doesn’t have any inter-related public members has no cohesion. And the best way to determine the degree of cohesion within a class is to check whether all private class-level variables are used by all public members.

If every private class-level variable is used by every public member, then the class is known to have maximal cohesion. This is a very clear indicator that the class is atomic and shouldn’t be split. The exception would be when the class can be refactored in an obvious way and the process of refactoring eliminates some or all of the cohesion within the class.

If every public member inside the class uses at least one of the private class-level variables, while the variables themselves are inter-dependent and are used in combination inside some of the public methods, the class has less cohesion, but would probably still not be in violation of single responsibility principle.

If, however, there are some private class-level variables that are only used by some of the public members, while other private class-level variables are only ever used by a different subset of public members, the class has low cohesion. This is a good indicator that the class should probably be split into two separate classes.

Finally, if every public member is completely independent from any other public member, the class has zero cohesion. This would probably mean that every public method should go into its own separate class.

Let’s have a look at the examples of cohesion above.

In our TextProcessor class, we only have one method, ConvertText(). So, we don’t even have to look at the cohesion. It has maximal cohesion already.

In our FileProcessor class, we have two methods, ReadAllText() and WriteToFile(). Both of these methods use the fullFilePath variable, which is initialized in the class constructor. So, the class also has the maximal cohesion and therefore is atomic.

God Object – the opposite of single responsibility

So, you now know what single responsibility principle is and how it benefits you as a developer. What you may be interested to know is that this principle has the opposite, which is known as God Object.

In software development, a method of doing things that is opposite to what best practices prescribe is known as anti-pattern; therefore God Object is a type of an anti-pattern. It is just as important to name bad practices as it is to name good practices. If something has a name, it becomes easy to conceptualize and remember, and it is crucially important for software developers to remember what not to do.

In this case, the name perfectly describes what this object is. As you may have guessed, a God Object is a type of class that is attempting to do everything. Just like God, it is omnipotent, omniscient and omnipresent.

In our example, the first iteration of our code had Program class containing the entire application logic, therefore it was a God Object in the context of our application. But this was just a simplistic example. In a real-life scenario, a God Object may span thousands of lines of code.

So, I don’t care whether you are religious or not. Everyone is entitled to worship any deity in the privacy of their own home. Just make sure you don’t put God into your code. And remember to always use single responsibility principle.

Conclusion

In this article, we have covered the first and arguably the most important SOLID principle of object-oriented software development. In the next article, I will explain how to use Open-closed principle in the context of C#.

Happy hacking!

Leave a Reply

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