TheDebugosaurus

This is an old article

My opinions on this have likely changed since this was first written

Taking responsibility

SOLID seems to be the hardest word

As I mentioned in the previous post the Single Responsibility Principle (SRP) becomes problematic when you try and define a responsibility within your code. The issue is that there’s no specific measure of a responsibility and so it becomes a matter of opinion / experience as to how you decide to enforce the SRP within code.

Responsibilities are implementation specific

I think the big challenge with implementing the SRP is that it’s natural to talk about code responsibilities at a higher level than is applicable to the principle. We tend to think of responsibility in terms of what the class does rather than how it does it; however within SOLID it is the implementation that matters rather than the intent.

Take for example a class that is to be used for reading SMTP configuration settings from a config file. If I were to use one of the many JSON libraries out there this would be possibly a one-liner implementation:

public class JsonSmtpConfigReader : ISmtpConfigReader
{
    public SmtpConfig Read(string path)
    {
        return JSON.Parse<SmtpConfig>(path)
    }
}

It would be perfectly reasonable for me to describe the responsibilities of this class as “JsonSmtpConfigReader is responsible for reading SMTP configuration from a config file”. This aptly describes the intent of the class but not the implementation responsibilities as applicable to the SRP.

Taking Uncle Bob’s definition of a responsibility as a ‘reason to change’ how might I define the responsibilities of this class? What reasons could I have to revisit and change this class?

Removing responsibilities

I can refactor this one liner to remove additional responsibilities in order to achieve a class with a single responsibility:

public class JsonSmtpConfigReader : ISmtpConfigReader
{
    private readonly IJsonParser jsonParser;
    private readonly ISmtpConfigFactory[] smtpConfigFactories;

    public JsonSmtpConfigReader(
        IJsonParser jsonParser,
        ISmtpConfigFactory[] smtpConfigFactories)
    {
        this.jsonParser = jsonParser;
        this.smtpConfigFactories = smtpConfigFactories;
    }

    public SmtpConfig Read(string path)
    {
        var jsonConfiguration = jsonParser.Parse(path);
        return smtpConfigFactories.First(x => x.CreateFrom(jsonConfiguration));  
    }
}

The JSON library changes

I introduce a bespoke IJsonParser which removes the direct dependency on the JSON library, now our config reader is no longer directly responsible for changes in the JSON library.

The format of the JSON file changes

We pass the results of parsing - a representation of JSON data - to the first factory we find that can deal with the JSON data; since each factory is responsible only for a specific configuration format we sidestep the need to change if the JSON data changes. New formats can be added by creating new implementations of this factory.

The structure of SmtpConfig changes

Since the factory deals with the details of the SmtpConfig creation we have eliminated another reason to change from our reader.

Single responsibility achieved

With these revisions we have reduced the responsibilities of this simple class down to a single reason to change - the ISmtpConfigReader interface changes; or to put it another way the JsonSmtpConfigReader is responsible for implementing the ISmtpConfigReader interface, it achieves this by controlling the orchestration of data between dependencies.

I can now redefine the implementation level responsibility of the class as it being responsible for the orchestration necessary to read SMTP configuration from JSON.

You decide your level of responsibility

I hope the above illustrates some of the problems in implementing the SRP. I’ll talk more later on about using judgment when following SOLID however you normally need to decide on how far you want to pursue the principles. Fine grained responsibilities may lead to technically more robust code however there might be a trade off in terms of verbosity. Ultimately you need to decide how far you want to take the SRP which will inform how big or small your responsibilities are.

A good approach is to look at the reasons your class has to change and decide on the likelihood of that change. With the above I’d say there’s a high likelihood of the JSON library being maintained and upgraded however the chance of the library interface changing is much smaller - so small that it’s probably not worth guarding against it in this instance. Likewise if this is the only place SmtpConfig is being created then again it might be better not to separate out the responsibility of instantiating the config instance; if however it is a shared responsibility then you absolutely should consider factoring it out into a shared dependency.