SOLID de verdade – Single Responsibility Principle (SRP)

responsibility

Nas palavras de Robert Martin, o Single Responsibility Principle (SRP) é o princípio SOLID mais mal compreendido. Ainda segundo ele, talvez por conta do seu enunciado. Será isso mesmo? Qual problema esse princípio busca resolver? É o que vamos descobrir a seguir.

O problema do Single Responsibility Principle (SRP)

Antes de qualquer coisa, conto com a sua “suspensão de descrença” ao ler os códigos de exemplo e a arquitetura proposta aqui. São apenas “toy codes”. Tenho certeza absoluta de que, na sua base de código, há exemplos muito melhores para ilustrar o Single Responsibility Principle. Assim, peço que foque na essência de cada uma das proposições. Mesmo sabendo que você poderia fazer melhor (e com certeza faria).

Nós estamos codificando um ERP bem simples. Ele abrange três áreas da empresa: Financeiro, Departamento Pessoal e Projetos. Essas três áreas compartilham uma entidade em comum: Colaborador, que está em Cadastro. As três áreas também possuem um código em comum: cálculo das horas trabalhadas. O Financeiro utiliza essas informações para saber quanto pagar no final do mês. Já o Departamento Pessoal deseja saber se o colaborador não está sobrecarregado. E por último, o Projeto quer saber quantas horas o Colaborador prestou ao projeto, para fins de faturamento. Eis parte do código:

src/Cadastro/Models/Colaborador.cs
public decimal ConsultarHorasTrabalhadas()
{
    return _horasDecimal.Sum(x => x);
}

src/DepartamentoPessoal/Service/MedidorBemEstar.cs
public class MedidorBemEstar
{
    public const int MAXIMO_HORAS_IDEAL = 160;
    public bool EhCargaIdealDeTrabalho(Colaborador colaborador)
    {
        return colaborador.ConsultarHorasTrabalhadas() <= MAXIMO_HORAS_IDEAL;
    }
}

src/Financeiro/Services/CalculadoraDeSalario.cs
public class CalculadoraDeSalario
{
    public decimal CalcularSalarioPorHora(Colaborador colaborador, decimal valorHora)
    {
        var horasTrabalhadas = colaborador.ConsultarHorasTrabalhadas();
        return horasTrabalhadas * valorHora;
    }
}

src/Projeto/Services/CalculadoraFaturamento.cs
public class CalculadoraFaturamento
{
    public decimal FaturadoPor(Colaborador colaborador, decimal valorHora)
    {
        var horasTrabalhadas = colaborador.ConsultarHorasTrabalhadas();
        return horasTrabalhadas * valorHora;
    }
}

Como você pode perceber, há um forte acoplamento entre os três diferentes setores da empresa com o Colaborador. De certa forma, é até compreensível esse acoplamento, já que até o nome utilizado pelos setores é o mesmo. A grande questão é que o comportamento do módulo Colaborador pode variar dependendo do contexto onde ele estiver.

Para termos visão disso, suponhamos que os requisitos mudaram. O Departamento Pessoal chegou à conclusão de que para garantir que o colaborador tenha saúde mental, ele precisa reservar 20 horas por mês de atividades lúdicas durante o expediente. Já o setor de Projeto determinou que o colaborador precisa ter até 10 horas mensais dedicadas a estudos. Mas o cliente irá pagar apenas aquilo que será efetivamente gasto no projeto.

Onde nós teremos que alterar o código? O sistema inteiro deverá ser alterado! Até mesmo o setor Financeiro, que nada tem a ver com as alterações do projeto, deverá ser modificado. Se as nossas alterações sequer modificassem a interface da classe colaborador, poderíamos ter um erro no cálculo da folha de pagamento. O acoplamento deixou o código muito difícil de alterar e muito suscetível a erros. Como alterar isso?

O que diz o princípio?

Como foi dito no início, o enunciado do Single Responsibility Principle pode induzir a erros, levando para extremos que, ao invés de ajudar, podem atrapalhar. Vejamos o que ele diz:

Um módulo deve ter um, e apenas um, motivo para ser alterado

Dissecando as informações do princípio, começamos por módulo. O que é um módulo? Para quem trabalha a mais tempo com tecnologia no Brasil, o uso da palavra módulo pode confundir um pouco. Já que “módulo” era a designação utilizada para recortes de interesse dentro de grandes sistemas. Você tinha o módulo financeiro, tinha o módulo de notas, tinha o módulo de vendas e assim por diante. Um conceito bem próximo do que hoje chamamos de domínio.

Mas não é desse módulo que o Single Responsibility Principle está falando. Para Tom DeMarco e Meilir Page-Jones, escritores do princípio, módulo poderia ser um arquivo de código ou uma classe. Você deve se lembrar que em linguagens não orientadas a objeto, as pessoas desenvolvedoras faziam um grande esforço para separar o código em blocos coesos. Já em linguagens orientadas a objeto, temos a figura da classe substituindo este módulo. Ok. Estamos falando de classes e isso você já sabia.

Até aqui, tudo bem. Onde está a confusão?

A confusão, no entanto, começa quando vamos pensar em motivo. Parece ser algo tão subjetivo, não é mesmo? Que motivo poderia ser esse? Olhando para o nome do princípio, o que vem à mente é: o motivo para alterar uma classe seria, portanto, uma alteração na sua responsabilidade. E daqui é um pulo para escrevermos classes anêmicas, que mais se parecem com uma função refatorada do que realmente com uma classe projetada.

O próprio Tio Bob comenta a possiblidade dessa confusão acontecer:

Não se confunda, há um princípio parecido com este. Uma função deve fazer uma, e somente uma, coisa. Nós usamos este princípio quando estamos refatorando funções grandes em menores; nós usamos isto no nível mais baixo. Mas este não é um dos princípios SOLID – isto não é SRP.

Clean Architecture – Traduzido livremente

O erro certamente está na compreensão de motivo e responsabilidade. Olhemos para o nosso exemplo. Como esse projeto poderia estar mais desacoplado? O que causa o acoplamento é o fato da classe Colaborador estar sendo compartilhada por todos os contextos. Quem causou o problema na arquitetura? As necessidades divergentes de diferentes stakeholders. A resposta parece óbvia, então: Cada contexto deveria ter a sua própria classe Colaborador. Em Clean Architecture, Uncle Bob revisita este princípio e o atualiza da seguinte forma:

Um módulo deve ser responsável por um, e apenas um, ator.

Esta nova assertiva parece estar alinhada com a solução óbvia que encontramos antes. Sim, o problema do Colaborador é que ele tem muitos atores mandando nele. E quando entram em desacordo, causam a quebra da classe. A ideia de reparti-lo entre os domínios está, portanto, de acordo com o Single Responsibility Principle.

Então só vale para Domínios?

Enquanto revisava o artigo, me dei conta que bati tanto na tecla de que as classes não devem ter sua complexidade compartilhada entre domínios, que me esqueci dos casos mais corriqueiros. Então, respondendo diretamente: Não.

Existe uma ideia por trás do Single Responsibility Principle e que deve servir de guia para o bom senso na hora de escrever e refatorar código: Coesão. Em uma lista, você espera que exista um método Add. Não importa a linguagem. Já em objetos que não representam uma lista (ou não encapsulam uma) você não espera por esse tipo de método. Isso é coesão.

Outro exemplo de coesão é quando você busca o algoritmo para salvar arquivos em disco na classe Arquivo e não na classe Relatório. Se não há coesão no seu código, além de estar violando o SRP, você também está dificultando a sua vida. Código espalhado é bug disseminado. Utilize SRP nas classes internas do seu domínio. Você vai perceber que, de repente, elas podem servir com helpers para outros domínios, reaproveitando código.

Polêmica!

Quero aproveitar o ensejo para deixar algo polêmico necessário e que ninguém fala porque é polêmico: As boas práticas, assim como os padrões de projeto, devem te ajudar a escrever código. Nunca o contrário. Coesão? O que parece fazer sentido para você, pode não fazer para mim. Quem estaria certo afinal? Por mais que pareça estranho alguns princípios serem subjetivos, eu lembro você que programar é uma arte subjetiva. Seu código fala sobre como você percebe o mundo a sua volta. Por isso alguns princípios e o uso de padrões são balizados pelo bom senso. Nada mais subjetivo.

Minha dica, portanto, é não encare padrões, boas práticas e etc como regras escritas em pedra. Martin Fowler, que é o cara dos padrões de projetos enterprise, foi quem disse que os padrões devem se adequar ao software. Então se você perceber que uma classe precisa realmente ter mais responsabilidade do que devia, confie no seu tino de arquiteto. Deixe a dívida técnica para ser resolvida quando for a hora. Agora, se o seu tino sempre manda fazer o errado… Bom, está na hora de chamar a assistência técnica dele.

Como [não] refatorar seguindo o Single Responsibility Principle?

Ao final da leitura, você pode ir correndo para o seu código e tentar refatorar todas as suas classes, de modo que elas possuam apenas uma responsabilidade. Com certeza, fazer com que elas pertençam a apenas um domínio da aplicação é urgente. Mas ainda assim eu recomendo cuidado.

Você já ouviu falar de overengineering? É quando você projeta o software para situações que nunca irão acontecer. O problema do overengineering (além de ter de escrever essa palavra difícil) é que pode aumentar desnecessariamente a complexidade do seu código e tornar as coisas mais lentas do que o necessário.

No livro Agile Principles Patterns and Practices in C#, Robert Martin novamente chama a atenção para o tema SOLID. E falando sobre SRP, ele lembra que se uma classe possui duas responsabilidades e elas não tendem a mudar por diferentes motivos, não há razão para separá-las. O Code Smell gerado é justamente o de complexidade desnecessária – ou overengineering.

Dito isto, particularmente não sou fã de refatorações drásticas e instantâneas. Se tem um momento em que os baby steps devem ser seguidos, é esse. Existem vários livros falando sobre mecânicas de refatoração. E eu recomendo que você os leia. Não como manuais, mas como fonte de insights para o seu dia-a-dia.

Se você tem testes unitários, refatorar é menos perigoso. Apenas gostaria de ressaltar que refatorações para o SRP podem ser uma ótima oportunidade para segregar os comportamentos divergentes em novas Interfaces (e até classes), tornando a classe original em um facade.

E agora?

Agora, olhando para o nosso exemplo, como poderíamos refatorá-lo? As abordagens seriam muitas. Com certeza, precisaríamos modificar a forma como as horas são adicionadas ao colaborador. Adicionar um domínio de Controle de Ponto pode ser legal? Acho que sim. Poderíamos também ter Modelos de Domínio recebendo os dados do controle de ponto e executando a regra de negócio? Também me parece uma boa solução.

Qual eu vou adotar? Nenhuma. Eu gostaria muito de ver as soluções que você daria para esse nosso simples caso de violação do Single Responsibility Principle. O código deste e dos demais exemplos estão disponíveis em: https://github.com/ftathiago/solid_ebook abra um pull request e vamos espalhar conhecimento!

Obrigado e até o próximo artigo! Advinha sobre o que será?

Lista de artigos da série

11 thoughts on “SOLID de verdade – Single Responsibility Principle (SRP)

  1. Olá! Obrigado pelo texto. Você escreve muito bem. Até me deu a sensação de tudo fazer sentido… pelo menos por um momento. Como sempre, eu consegui ficar mais confuso pensando sobre essa interpretação correta acerca do princípio. Novas dúvidas surgiram.

    Abaixo eu deixo algumas delas. Caso algum leitor ou o autor do texto puder me ajudar a responder essas perguntas, eu agradeço de coração!

    “Domínio” é um sinônimo de departamento?

    Como os termos “motivo” e “responsabilidade” se encaixa nessa interpretação? Quero dizer, como “ter uma única responsabilidade” ou “apenas um motivo para mudar” pode significar algo como “estar sob um único domínio”.

    Não é ruim ter múltiplas classes representando uma mesma entidade, já que alguma parte do código pode ser compartilhado entre elas, de forma que uma alteração nessa parte compartilhada terá de ser repetida em todas essas classes?

    Seria esse um princípio para ser aplicado de forma retroativa? Identificar que uma classe irá sofrer múltiplas mudanças, que causaram problemas em diferentes partes do programa que a utilizam, antes que esses problemas ocorram não me parece factível. Mas pode ser só a minha falta de experiência em uma empresa de fato.

    1. Bom dia, Fernando. Ótima pergunta! A primeira coisa que eu preciso lembrar é que não existe bala de prata. Sei que é clichê, mas é que realmente toda decisão arquitetural/modelagem depende muito do contexto. Dito isto:

      > Domínio é um sinônimo de departamento?

      Dependendo do tamanho do negócio que você deseja modelar, pode até ser que, coincidentemente, os domínios e departamentos tenham uma relação 1×1. Mas não são sinônimos. Pense, por exemplo, numa empresa de contabilidade. Ela própria é um “departamento financeiro” de alguém, e mesmo assim pode ter vários domínios/sub-domínios.

      A melhor forma de pensa em domínios, subdomínios e contextos é entendê-los como “ilhas de complexidade de negócio”. Por exemplo: Ao desenvolver um PCP, para um mesmo setor de uma linha de produção, você pode olhar para ela considerando várias frentes de complexidade. Entre elas: Performance, Qualidade, Manutenção, Segurança do trabalho e etc. Pensando no Operador, ele tem papéis distintos e funções distintas em cada um desses contextos (ou sub-domínios, como queira chamar). No contexto de performance, um operador não deveria conseguir gerar um pedido de manutenção – veja, estamos falando da complexidade e não da jornada do usuário em um sistema – bem como no contexto de Segurança do trabalho, não seria possível anotar o encerramento de um lote. Captou?

    2. > Como os termos “motivo” e “responsabilidade” se encaixa nessa interpretação? Quero dizer, como “ter uma única responsabilidade” ou “apenas um motivo para mudar” pode significar algo como “estar sob um único domínio”.

      Motivo é motivo mesmo. Ainda no nosso exemplo, se eu quero alterar a forma como eu calculo a performance de um lote, o módulo que calcula a qualidade não deveria sofrer alterações. Os atores envolvidos, muito provavelmente, não são os mesmos. E aqui não estou falando de usuários. Estamos mais próximos dos stakeholders.

      A Classe lote, no domínio de performance, teve o motivo “alterar cálculo de performance” e no contexto de performance ela tem a responsabilidade de “calcular a performance deste lote”. Se for a mesma classe em qualidade, o domínio teria que ser recompilado e redeployado MESMO que nenhuma alteração para ele tenha sido solicitada.

    3. > Não é ruim ter múltiplas classes representando uma mesma entidade, já que alguma parte do código pode ser compartilhado entre elas, de forma que uma alteração nessa parte compartilhada terá de ser repetida em todas essas classes?

      Vai depender do modelo arquitetural que você está seguindo. Se você está implementando Micro serviços, não vejo problema. Mesmo porque, cada micro serviço saberia lidar apenas com a complexidade que lhe cabe, não reproduzindo uma função de outro serviço. O lote, no MS de performance, sequer teria os métodos relativos a qualidade.

      Se você está implementando um monolito, SOA ou qualquer outro modelo, existem formas de reaproveitar código sim, mas em tese você deveria separar em namespaces diferentes e controlar a visibilidade dos métodos. Para um domínio não deveria acessar diretamente a interface do outro.

    4. > Seria esse um princípio para ser aplicado de forma retroativa? Identificar que uma classe irá sofrer múltiplas mudanças, que causaram problemas em diferentes partes do programa que a utilizam, antes que esses problemas ocorram não me parece factível. Mas pode ser só a minha falta de experiência em uma empresa de fato.

      Aí nós podemos falar de arquitetura evolutiva. Basicamente, você precisa aumentar a complexidade do seu software de acordo com a demanda. De igual forma, você precisa cuidar para que o software esteja flexível o suficiente para suportar modificações. Isso quer dizer que você não precisa partir de uma solução completamente desacoplada. Principalmente se você não tem visão de pra onde o sistema vai evoluir. Você caminha com o que conhece e refatora durante o caminho.

      DETALHE: refatorar código SEMPRE será preciso; especialmente para diminuir a entropia do design da sua aplicação.

      Espero ter ajudado. E desculpe pela demora 🙂

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.