SOLID - S: Princípio da Responsabilidade Única (Single Responsibility Principle)
Eae pessoal, tudo bem?
Vou começar a postar os principios S.O.L.I.D. por ser algo que me ajudou MUITO na qualidade de codigo (sei que o tema está meio batido com o tanto de videos, artigos e etc). Mas o real motivo de começar é de aprender e me acostumar a estruturar textos de maneira legivel e que faça sentido (o que nunca fui muito bom), então se quiserem dar dicas ou fazer criticas sobre a estrutura, gramática e/ou conhecimento técnico sintam-se a vontade.
Single Responsability Principle (SRP)
O que isso significa?
Históricamente ele é descrito com essa "simples" e pequena fraze:
Um módulo deve ter um, e apenas um, motivo para mudar.
Porém os sistemas de softwares são mudados para satisfazer um "user" ou um "stakeholder". Fazendo com que a fraze fique um pouco diferente:
Um módulo deve ter um, e apenas um, user ou stakeholder.
Mas como é muito provavel que a tenha mais de um user ou stakeholder querendo essa mudança, então devemos nos referir ao grupo de users e ou stakeholders podendo ser um ator (sim aquele mesmo do UML). Sendo assim:
Um módulo deve ter um, e apenas um, ator.
Legal entendi que um "módulo" só pode ter um motivo para ser alterado, porem o que/ quem é esse cara?
"A maneira mais simples de descrevelo é um arquivo de código-fonte." ~ Robert C. Martin
em "Clean Architecture: A Craftsman's Guide to Software Structure and Design"
Ainda assim acredito estar meio confuso, por exemplo:
-
Cada arquivo deve ser responsável por fazer apenas uma coisa.
-
Cada função/classe desse arquivo deve também ter apenas uma responsabilidade, tornando-a um expecialista no que ela foi prosta a fazer.
Tem como identificar?
Os dois principais sintomas que Robert C. Martin fala são:
Duplicação acidental
class Employee {
calculatePay() {
//calcula o pagamento
};
reportHours() {
//gera relatorio das horas
}
save() {
// salva as alterações na base de dados
}
}
Essa classe é um bom exemplo para visualizar a duplicação do SRP por conta de:
calculatePay()
é um método específico de um ator da área de finançasreportHours()
é um método específico de um ator da área de recursos humanossave()
é um método específico de um ator da área de tecnologia
O acoplamento destes três métodos faz com que os 3 atores diferentes acabem se tornando apenas "um employee" fazendo com que todos tenham as peculiaridades de todos, ocorrendo uma duplicação acidental.
Um outro exemplo de como isso é maléfico
Imagine que tem um método regularHours()
que é utilizado no calculatePay()
e no reportHours()
De maneira um pouco mais visual
calculatePay()
-> regularHours()
<- reportHours()
Porem o ator de finanças quer que regularHours()
tenha algumas alterações para atender as necessidades dele
Mas isso afetaria o reportHours()
, onde o ator de recursos humanos que já avisou que não pode ocorrer essas mudanças, pois irá gerar o relatório com informações inválidas.
Obs: nesse caso ambos os atores estão cientes das alterações e de seus impactos quanto maior e mais complexo é o sistema, mais dificil de ter noção e prever o que pode acontecer, podendo gerar bugs, informações invalidas, atrapalhar nas tomadas de decisões, falhas de segurança e por ai vai.
Merges
Não é muito dificil de imaginar que merges vão ser comum de ocorrer em sistemas onde arquivos possuem multiplas funcionalidades.
Voltando ao exemplo do Employee
Imagine que o ator da área de tecnologia ele precise fazer uma alteração no schema de employee na base de dados e por coincidencia o ator de recursos humanos também decide alterar o employee para mudar o formato de data/hora.
Quando eles tentam subir suas as alterações causa um conflito, obviamente isso não é o fim do mundo, porem tras um risco para o sistema, já que irá ter conflitos mais frequentemente, aumenta a possibilidade de quem estiver resolvendo o conflito errar em algo.
O que eu ganho resolvendo isso?
- Facilita a implementação de uma classe, bem como o seu entendimento e manutenção.
- Facilita a alocação de um único responsável por manter uma classe.
- Facilita o reúso e teste de uma classe, pois é mais simples reusar e testar uma classe coesa do que uma classe com várias responsabilidades.
Como resolvo isso?
Uma das maneiras de solucionar é separando as responsabilidades em cada arquivo
Continuando o problema do employee
Separando tudo podemos concluir que temos duas camadas
- Dados (os dados do employee)
- Serviço (as funcionalidades dele que cada ator precisa)
Poderiamos ter:
EmployeeData.ts
Para a representação dos empregados
interface EmployeeData {
// ... estrutura de dados do employee
}
PayCalculator.ts
class PayCalculator {
execute(employee:EmployeeData): Promise<number>{
// ... implementação do calculo de pagamento
}
}
HourReporter.ts
class HourReporter {
execute(employee:EmployeeData): Promise<HourReport>{
// ... implementação do relatório de horas
}
}
EmployeeSaver.ts
class HourReporter {
execute(employee:EmployeeData): Promise<void>{
// ... Salva as alterações do employee
}
}
e para representar cada ator poderiamos usar o Facade pattern
EmployeeFacade.ts
class EmployeeFacade {
private employee: EmployeeData
}
e herdando do EmployeeFacade poderiamos adicionar os metodos de cada ator para dentro dele, deixando o "módulo" de employee com maior facilidade de manutenção, leitura.
Conclusão
Basicamente o Princípio de responsábilidade única é sobre funções e classes, limitando seu escopo de uma unica funcionalidade por arquivo a nivel de componentes.
Mas pelo lado arquitetural ele define os limites do contexto.