Single Responsibility Principle in C#

The definitive guide with an example of how to break class and separate concerns using the Single Responsibility Principle.

Single Responsibility Principle in C#

Single Responsibility Principle or SRP stands for the idea that every class or module should have one and only one responsibility. In other words, each class or module should do one thing and do it well.

What Means Responsibility in Software Engineering

Robert C. Martin defined responsibility as a reason to change. This reason to change is more related to module or application features. In an ideal world, one feature’s behavior change means one class or module to modify.

SRP in Unix

SRP is a well-known principle in software design, and we’ve known for decades that this works very well.

Unix is based on this principle of doing one thing and doing it well. Unix is based on the command line, so each command is a small executable program. It has a way to compose those executables into larger-scale programs.

The success of Unix continues with the success of Linux, so we know that there are examples of this principle leading to systems that are very stable and very long-lived. I found a repository with historical commits since 1970 if you are more interested.

Why is SRP Hard to Implement

On my hunt for the answer, I came across a great Quora answer from user Pawan Bhadauria, and I want to quote the piece of it.

Consider the following sentence: “Tihs snetnece is hrad to raed”.

I bet, you will be able to read this sentence since you comprehend the word as a whole rather than linking each alphabet and making a sentence out of it. Though it directly doesn’t relate to the principle but gives a glimpse into how our brain processes things. Our minds are excellent at seeing a set of behavior and grouping those together, even when those constitute different responsibilities.

We tend to group behaviors by our nature. The human brain’s strength is also a developer’s kryptonite when he is trying to apply SRP.

Indicators of Violation SRP

The most significant indicator is the size of the class or function. Count of physical lines of code measures functions size. On the other hand, the number of responsibilities measures the class size.

“The first rule of classes is that they should be small. The second rule of the class is that they should be smaller than that.” — Robert C. Martin

Troubles to name the class correctly is one of the indicators as well. The name should be maximally 25 characters long without using words like “if”, “and”, “or” or “but”.

Sometimes, problems with naming the class end by raising a point of view and using the most common name that comes up to the programmer’s mind like `OrderService.

The order is processed from filling the basket to receiving a receipt, and it can’t be defined with such a simple name.

Tutorial

Prerequisites

The reader should have basic knowledge about Object-oriented Programming or OOP. A basic understanding of Unit Testing is also appropriate, but not necessary. I am programming in C# language, but Java or other OOP language developers should not have a problem understanding the concept.

Introduction of Project

The repository consists of two projects. The first one is the Class Library named BusinessLogic. It includes two folders; Before and After.

Folder Before contains the class FileStore, which is taking care of storing messages into text files.

Folder After contains a module consisting of three classes with the very same responsibilities, as a Before/FileStore class. The content of the directory is the resulting representation of applying the SRP process.

The second project Tests also holds two folders with the same names. Each folder has one class with the very same Unit Tests. The test class Before/FileStoreUnitTests is almost identical to the test class After/MessageStoreUnitTests, but its tests aim to the different system under test as its names suggest.

Identifying the Responsibilities

Gist bellow shows how Before/FileStore.cs the class looks like.

public class FileStore
    {
        public FileStore(string workingDirectory)
        {
            if (workingDirectory == null)
                throw new ArgumentNullException(nameof(workingDirectory));
            if (!Directory.Exists(workingDirectory))
                throw new ArgumentException("Boo", nameof(workingDirectory));

            this.WorkingDirectory = workingDirectory;
        }

        public string WorkingDirectory { get; }

        public void Save(int id, string message)
        {
            Log.Info($"Saving message {id}");
            var path = this.GetFileName(id);
            File.WriteAllText(path, message);
            Log.Info($"Saved message {id}");
        }

        public string Read(int id)
        {
            Log.Debug($"Reading message {id}.");
            var path = this.GetFileName(id);

            if (!File.Exists(path))
            {
                Log.Debug($"No message {id} found.");
                return string.Empty;
            }

            var message = File.ReadAllText(path);
            Log.Debug($"Returning message {id}.");
            return message;
        }

        public string GetFileName(int id)
        {
            return Path.Combine(this.WorkingDirectory, id + ".txt");
        }
    }

Most of you can identify what’s the FileStore class responsibilities. First, that should come at the top of your head is Storing Messages.

Another more obvious responsibility is Logging. Both command methods, Read and Save invoke the NLog functions to log what is currently happening.

The last is not that clear so I’ll tell you. It is what I call an Orchestration. The way of controlling how the previous two aspects play with each other is also the responsibility.

That’s it! We identified all three, and let’s repeat them.

  • Storing Messages
  • Logging Events
  • Orchestrating interactions

Break the Class

Breaking the class is practically an application of another well-known principle called the Separation of Concerns.

I like Quora's answer by architecture mentor Victor Kane to the question “What is Separation of Concerns in Software Engineering?”

Let me quote the beginning of the answer;

The philosophy of separation of concerns in IT is aimed at preventing “pathological” dependencies between code “concerned” with different domains while attempting to isolate all code concerned with a single “concern”.

“..isolate all code concerned with a single concern.” Sounds familiar right? Now, let’s break the FileStore.

I began with the extraction of Logging concerns. The new StoreLogger class encapsulates implementation details about logging events into its methods. The initialization of StoreLogger is in the FileStore constructor and methods are called from the same places as where NLog functions invocations were before.

public class FileStore
{
  private readonly StoreLogger log;

  public FileStore(string workingDirectory)
  {
      /*
      */
      this.log = new StoreLogger();
      this.WorkingDirectory = workingDirectory;
  }

  public void Save(int id, string message)
  {
      Log.Saving(id};
      var path = this.GetFileName(id);
      File.WriteAllText(path, message);
      Log.Saved(id);
  }

  /*
  */
}
public class StoreLogger
{
    public void Saving(int id)
    {
        Log.Info($"Saving message {id}.");
    }

    public void Saved(int id)
    {
        Log.Info($"Saved message {id}.");
    }

    public void Reading(int id)
    {
        Log.Debug($"Reading message {id}.");
    }

    public void DidNotFind(int id)
    {
        Log.Debug($"No message {id} found.");
    }

    public void Returning(int id)
    {
        Log.Debug($"Returning message {id}.");
    }
}

Now, we need to separate Storing Messages and Orchestration. Let’s logically in our minds separate two implementation details; Storage and Data. In our particular case, the storage is a file system, and the data are messages.

When we consider the dependencies, it appears that the messages should be dependent on their storage, but storage does not need to know anything about the form of data. We can achieve it by extracting the orchestration around the message and letting concerns about storage on FileStore.

I separated the original FileStore class into two new classes; FileStore and MessageStore.

public class FileStore
{
    private string workingDirectory;

    public FileStore(string workingDirectory)
    {
        if (workingDirectory == null)
            throw new ArgumentNullException(nameof(workingDirectory));
        if (!Directory.Exists(workingDirectory))
            throw new ArgumentException("Boo", nameof(workingDirectory));

        this.workingDirectory = workingDirectory;
    }

    public void WriteAllText(string path, string message)
    {
        File.WriteAllText(path, message);
    }

    public string ReadAllText(string path)
    {
        return File.ReadAllText(path);
    }

    public string GetFileInfo(int id)
    {
        return Path.Combine(workingDirectory, id + ".txt");
    }

    public bool Exists(string path)
    {
        return File.Exists(path);
    }
}

MessageStore orchestrates the messages, logging, and storage processes.

public class MessageStore
{
    private readonly StoreLogger log;
    private readonly FileStore fileStore;

    public MessageStore(string workingDirectory)
    {
        this.log = new StoreLogger();
        this.fileStore = new FileStore(workingDirectory);
    }

    public void Save(int id, string message)
    {
        log.Saving(id);
        fileStore.WriteAllText(GetFileName(id), message);
        log.Saved(id);
    }

    public string Read(int id)
    {
        log.Reading(id);
        var path = GetFileName(id);

        if (!fileStore.Exists(path))
        {
            log.DidNotFind(id);
            return string.Empty;
        }

        var message = fileStore.ReadAllText(path);
        log.Returning(id);
        return message;
    }

    public string GetFileName(int id)
    {
        return fileStore.GetFileInfo(id);
    }
}

This time I stuck with the word “store” in the name because it is more precise. Sometimes I struggle to pick the right name for the orchestration responsible classes. So usually I choose the word “Processor”, and I add a prefix which defines the responsibility-for example, a MessageStoringProcessor. The MessageStore name sounds more precise.

I would love to hear how you are dealing with this problem.

Summary

We looked at FileStore class and identify its three responsibilities. First, we extracted the easiest one, the Logging responsibility. Therefore, we separated remaining concerns, Storring Messages, and Orchestration, into the two new classes; MessageStore and FileStore.

The repository also contains the project with unit tests which you can try to run and see if any dropped behavior or function. The green test will prove that we did just right.

itixo-logo-blue.png