SRP - The Single Responsibility Principle

5 min read
By Thiago Souza
SOLIDSRPSoftware DesignC#

According to Robert C. Martin himself (Uncle Bob), the SRP (Single Responsibility Principle) is certainly, of all 5 SOLID Principles, the least understood. I'm sorry to have to say this, but he's right.

What I'm going to present here is not just an opinion, but rather my understanding based on the definition that the creator of this set of principles wanted to convey as a message when he published his article.

Definition of SRP

Without further ado, I will now present you with the phrase that best defines the Single Responsibility Principle:

"A module must be responsible for one, and only one, actor."

Felca - Tá certo isso?

That's right. He defines a module as a source file and, depending on the programming language, a module can be defined as a cohesive set of functions and data structures. In C#, for example, we can understand this definition of module as a class.

Violation of SRP

Code duplication is something that every good developer tries to avoid and that's why we create methods with code snippets that can be reused.

When we write a method with code snippets containing part of a business rule that can serve more than one actor, we should be a bit more cautious and careful to understand if this cannot cause us a problem.

Let's imagine a (hypothetical) scenario where:

  • The actor from the Sales department calculates the discount value of an order;
  • The actor from the Finance department calculates the commission value to be paid on an order;
public class Pedido 
{
    public List<ItemPedido> Itens { get; set; }

    // Method used by the actor from the Sales department
    public decimal CalcularDesconto(decimal percentualDeconto)
    {
        decimal valorTotal = CalcularValorTotal();

        decimal desconto = valorTotal * percentualDeconto;

        return desconto;
    }

    // Method used by the actor from the Finance department
    public decimal CalcularComissao(decimal percentualComissao)
    {
        decimal valorTotal = CalcularValorTotal();

        decimal comissao = valorTotal * percentualComissao;

        return comissao;
    }
    
    // Private method shared between the methods 
    // CalcularImposto and CalcularComissao
    private decimal CalcularValorTotal()
    {
        decimal valorTotal = Itens.Sum(item => item.Valor);

        return valorTotal;
    }
}

Regardless of whether these methods are in a single class or allocated each in a specific class, this attempt to reuse code causes the actors responsible for Finance and Sales to become coupled to each other, because any change in the CalcularValorTotal function directly affects the methods used by both actors.

Let's say that at some point in the future the actor from the Sales department decides that they no longer want to consider items of type Premium in the discount calculation. The developer promptly decides to include a filter before summing the items in the CalcularValorTotal method, uploads the changes to the staging environment and asks the actor from the Sales department to validate the change before going to production.

The actor from the Sales department performs all tests meticulously and indicates that everything is OK. Finally, the developer deploys to the production environment and closes the Change Request.

Days (or weeks) later, someone discovers that this change broke the commission calculation, generating a huge problem for the actor from the Finance department.

Michael Scott frustrado

This is because we haven't even gone very far talking, for example, about possible problems in the merge process of branches from other developers on the team.

I'm sure you can already imagine the criticality and the reason why we should avoid actors being coupled to each other.

Depending on your time in the development area, I venture to say that, very probably, you have already experienced some kind of situation like this.

But destiny wanted you to reach this point in the reading and now you have the power in your hands to simplify the software development process for you and all your colleagues, avoiding the famous "fix one side and break the other".

Applying the SRP

There are several ways to avoid (and solve) this problem. Thinking long-term and about software health, we could organize the code as follows:

// Order Entity
public class Pedido 
{
    public List<ItemPedido> Itens { get; set; }
}

// Specific class for calculating the discount of an order
// exclusive to the actor from the Sales department
public class ProvedorCalculoDescontoPedido(Pedido pedido)
{
    public decimal Calcular(decimal percentualDesconto)
    {
        decimal total = pedido.Itens.Sum(item => item.Valor);

        decimal desconto = total * percentualDeconto;

        return desconto;
    }
}

// Specific class for calculating the commission of an order
// exclusive to the actor from the Finance department
public class ProvedorCalculoComissaoPedido(Pedido pedido)
{
    public decimal Calcular(decimal percentualComissao)
    {
        decimal total = pedido.Itens
            .Where(item => item.Tipo != TipoItemPedido.Premium)
            .Sum(item => item.Valor);

        decimal comissao = total * percentualComissao;

        return comissao ;
    }
}

From now on, any new change request made by any of the actors should no longer generate the problems reported earlier. Furthermore, the classes become more cohesive and become responsible for one, and only one, actor — reducing coupling between actors and facilitating maintenance.


Thanks for reading. To deepen: Clean Architecture — A Craftsman's Guide to Software Structure and Design (Robert C. Martin, 2019).