O Princípio da Responsabilidade Única afirma que uma classe deve fazer uma e apenas uma coisa. Alguns casos são bem claros. Outros, porém, são difíceis porque o que parece "uma coisa" quando visualizado em um determinado nível de abstração pode ser múltiplo quando visualizado em um nível inferior. Eu também temo que, se o Princípio da Responsabilidade Única for respeitado nos níveis mais baixos, código ravioli excessivamente dissociado e detalhado, onde mais linhas são gastas criando classes minúsculas para tudo e informações sobre o encanamento do que realmente resolver o problema em questão, pode resultar.
Como você descreveria o que "uma coisa" significa? Quais são alguns sinais concretos de que uma classe realmente faz mais do que "uma coisa"?
fonte
Respostas:
Gosto muito da maneira como Robert C. Martin (tio Bob) reafirma o princípio de responsabilidade única (vinculado ao PDF) :
É sutilmente diferente da definição tradicional de "deve fazer apenas uma coisa", e eu gosto disso porque o força a mudar a maneira como você pensa sobre sua classe. Em vez de pensar em "isso está fazendo uma coisa?", Você pensa sobre o que pode mudar e como essas mudanças afetam sua classe. Por exemplo, se o banco de dados mudar, sua classe precisa mudar? E se o dispositivo de saída mudar (por exemplo, uma tela, um dispositivo móvel ou uma impressora)? Se a sua turma precisa mudar devido a mudanças de muitas outras direções, isso é um sinal de que sua turma tem muitas responsabilidades.
No artigo vinculado, o tio Bob conclui:
fonte
Fico me perguntando que problema o SRP está tentando resolver? Quando o SRP me ajuda? Aqui está o que eu vim com:
Você deve refatorar a responsabilidade / funcionalidade de uma classe quando:
1) Você duplicou a funcionalidade (DRY)
2) Você acha que seu código precisa de outro nível de abstração para ajudá-lo a entendê-lo (KISS)
3) Você acha que partes da funcionalidade são entendidas pelos especialistas em domínio como parte de um componente diferente (Linguagem Ubíqua)
Você NÃO deve refatorar a responsabilidade de uma classe quando:
1) Não há funcionalidade duplicada.
2) A funcionalidade não faz sentido fora do contexto da sua classe. Em outras palavras, sua classe fornece um contexto em que é mais fácil entender a funcionalidade.
3) Os especialistas em seu domínio não têm um conceito dessa responsabilidade.
Ocorre-me que, se o SRP for aplicado amplamente, trocamos um tipo de complexidade (tentando fazer cara ou coroa de uma classe com muita coisa acontecendo por dentro) com outro tipo (tentando manter todos os colaboradores / níveis de abstração direta, a fim de descobrir o que essas classes realmente fazem).
Em caso de dúvida, deixe-a de fora! Você sempre pode refatorar mais tarde, quando houver um caso claro para isso.
O que você acha?
fonte
Não sei se existe uma escala objetiva, mas o que daria isso seriam os métodos - não tanto o número deles, mas a variedade de suas funções. Concordo que você pode levar a decomposição longe demais, mas eu não seguiria nenhuma regra rígida sobre isso.
fonte
A resposta está na definição
O que você define como responsabilidade, em última análise, fornece os limites.
Exemplo:
Eu tenho um componente que tem a responsabilidade de exibir faturas -> nesse caso, se eu começar a adicionar mais alguma coisa, estou quebrando o princípio.
Se, por outro lado, se eu disser a responsabilidade de lidar com as faturas -> adicionar várias funções menores (por exemplo, imprimir fatura , atualizar fatura ), todas estão dentro desse limite.
No entanto, se esse módulo começasse a manipular qualquer funcionalidade fora das faturas , estaria fora desse limite.
fonte
Eu sempre o vejo em dois níveis:
Então, algo como um objeto de domínio chamado Dog:
Dog
é a minha classe, mas os cães podem fazer muitas coisas! Eu poderia ter métodos comowalk()
,run()
ebite(DotNetDeveloper spawnOfBill)
(desculpe, não pude resistir; p).Se
Dog
ficar difícil de manejar, eu pensaria em como grupos desses métodos podem ser modelados juntos em outra classe, como umaMovement
classe, que poderia conter meus métodoswalk()
erun()
.Não existe uma regra rígida e rápida, seu design OO evoluirá com o tempo. Eu tento ir para uma interface clara / API pública, bem como métodos simples que fazem uma coisa e uma coisa bem.
fonte
Eu olho para ele mais ao longo das linhas de uma classe deve representar apenas uma coisa. Para se apropriar do exemplo de @ Karianna, tenho minha
Dog
classe, que possui métodos parawalk()
,run()
ebark()
. Eu não estou indo para adicionar métodos parameaow()
,squeak()
,slither()
oufly()
porque essas não são coisas que os cães fazem. São coisas que outros animais fazem, e esses outros animais teriam suas próprias classes para representá-los.(BTW, se o seu cão não voar, então você provavelmente deve parar de jogá-lo para fora da janela).
fonte
SeesFood
uma característica deDogEyes
,Bark
como algo feito por aDogVoice
eEat
como feito por aDogMouth
, então a lógicaif (dog.SeesFood) dog.Eat(); else dog.Bark();
se tornaráif (eyes.SeesFood) mouth.Eat(); else voice.Bark();
, perdendo qualquer senso de identidade de que olhos, boca e voz estão todos conectados a uma única entidade.Dog
classe, provavelmente estáDog
relacionado. Caso contrário, você provavelmente acabaria com algo comomyDog.Eyes.SeesFood
e não apenaseyes.SeesFood
. Outra possibilidade é queDog
expõe aISee
interface que exige aDog.Eyes
propriedade e oSeesFood
método.Eye
classe, mas um cão deve "ver" usando seus olhos, em vez de apenas ter olhos que possam ver. Um cão não é um olho, mas também não é apenas um observador. É uma "coisa que pode [pelo menos tentar] ver" e deve ser descrita via interface como tal. Até um cão cego pode ser perguntado se vê comida; não será muito útil, pois o cão sempre diz "não", mas não há mal em perguntar.Uma classe deve fazer uma coisa quando visualizada em seu próprio nível de abstração. Sem dúvida, fará muitas coisas em um nível menos abstrato. É assim que as classes trabalham para tornar os programas mais fáceis de manter: eles ocultam os detalhes da implementação se você não precisar examiná-los de perto.
Eu uso nomes de classe como um teste para isso. Se não posso atribuir a uma classe um nome descritivo bastante curto, ou se esse nome tiver uma palavra como "E", provavelmente estou violando o Princípio de Responsabilidade Única.
Na minha experiência, é mais fácil manter esse princípio nos níveis mais baixos, onde as coisas são mais concretas.
fonte
É sobre ter um papel único .
Cada classe deve ser reiniciada com um nome de função. Um papel é de fato um (conjunto de) verbo (s) associado (s) a um contexto.
Por exemplo :
O arquivo fornece o acesso de um arquivo. O FileManager gerencia objetos de arquivo.
Dados de retenção de recurso para um recurso de um Arquivo. O ResourceManager retém e fornece todos os recursos.
Aqui você pode ver que alguns verbos como "gerenciar" implicam um conjunto de outros verbos. Os verbos sozinhos são mais bem pensados como funções do que as classes, na maioria das vezes. Se o verbo implica muitas ações que têm seu próprio contexto comum, deve ser uma classe em si.
Portanto, a idéia é apenas permitir que você tenha uma idéia simples do que a classe define, definindo uma função única, que pode ser o agregado de várias sub-funções (executadas por objetos membros ou outros objetos).
Costumo criar classes Manager que possuem várias outras classes diferentes. Como uma fábrica, um registro, etc. Veja uma classe de gerente como algum tipo de chefe de grupo, um chefe de orquestra que orienta outras pessoas a trabalharem juntas para alcançar uma ideia de alto nível. Ele tem um papel, mas implica trabalhar com outros papéis únicos. Você também pode ver como a empresa está organizada: um CEO não é produtivo no nível de produtividade pura, mas se ele não estiver lá, nada poderá funcionar corretamente juntos. Esse é o papel dele.
Ao projetar, identifique funções exclusivas. E para cada papel, verifique novamente se ele não pode ser cortado em vários outros papéis. Dessa forma, se você precisar simplesmente alterar a maneira como seu gerente cria objetos, basta alterar a Fábrica e ter a mente em paz.
fonte
O SRP não se refere apenas à divisão de classes, mas também à delegação de funcionalidades.
No exemplo Dog usado acima, não use SRP como justificativa para ter três classes separadas, como DogBarker, DogWalker, etc. (baixa coesão). Em vez disso, observe a implementação dos métodos de uma classe e decida se eles "sabem demais". Você ainda pode ter dog.walk (), mas provavelmente o método walk () deve delegar para outra classe os detalhes de como a caminhada é realizada.
De fato, estamos permitindo que a classe Dog tenha um motivo para mudar: porque os cães mudam. Obviamente, combinando isso com outros princípios do SOLID, você estenderia o Dog para obter novas funcionalidades, em vez de mudar o Dog (aberto / fechado). E você injetaria suas dependências, como IMove e IEat. E é claro que você faria essas interfaces separadas (segregação de interface). O cão só mudaria se encontrássemos um bug ou se o cão mudasse fundamentalmente (Liskov Sub, não estenda e remova o comportamento).
O efeito líquido do SOLID é que conseguimos escrever um novo código com mais frequência do que o necessário para modificar o código existente, e isso é uma grande vitória.
fonte
Tudo depende da definição de responsabilidade e de como essa definição afetará a manutenção do seu código. Tudo se resume a uma coisa e é assim que seu design o ajudará a manter seu código.
E como alguém disse "Andar na água e projetar software a partir de determinadas especificações é fácil, desde que ambos estejam congelados".
Portanto, se estamos definindo responsabilidades de maneira mais concreta, não precisamos alterá-las.
Às vezes, as responsabilidades serão claramente óbvias, mas às vezes podem ser sutis e precisamos decidir criteriosamente.
Suponha que adicionemos outra responsabilidade à classe Dog denominada catchThief (). Agora, isso pode estar levando a uma responsabilidade diferente adicional. Amanhã, se o Departamento de Polícia alterar a maneira como o cão está pegando ladrão, a classe do cão terá que ser alterada. Seria melhor criar outra subclasse nesse caso e nomeá-la ThiefCathcerDog. Mas, sob uma visão diferente, se tivermos certeza de que isso não será alterado em nenhuma circunstância, ou se a maneira como o catchThief foi implementado depende de algum parâmetro externo, é perfeitamente aceitável ter essa responsabilidade. Se as responsabilidades não são surpreendentemente estranhas, temos que decidir criteriosamente com base no caso de uso.
fonte
"Um motivo para mudar" depende de quem está usando o sistema. Certifique-se de ter casos de uso para cada ator e faça uma lista das mudanças mais prováveis; para cada possível mudança de caso de uso, verifique se apenas uma classe seria afetada por essa mudança. Se você estiver adicionando um caso de uso totalmente novo, verifique se você precisaria apenas estender uma classe para fazer isso.
fonte