Nas palavras de Robert Martin, o Single Responsability 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 Responsability 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 Responsability 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.
Em qual parte 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 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 Responsability 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 há 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 Responsability 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.
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 modificaçã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.
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. Porém este não é um dos princípios SOLID – isto não é SRP.
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 Responsability 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 Responsability 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.
Quero aproveitar o ensejo para deixar algo necessário e que ninguém fala porque tende a ser 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.
Como [não] refatorar seguindo o Single Responsability 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.