Esclarecer o princípio da responsabilidade única

64

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"?

dsimcha
fonte
6
+1 para o "código ravioli" superior. No início da minha carreira, fui uma daquelas pessoas que levaram isso longe demais. Não apenas com classes, mas também com modularização de métodos. Meu código estava repleto de toneladas de pequenos métodos que faziam algo simples, apenas para quebrar um problema em pequenos pedaços que poderiam caber na tela sem rolar. Obviamente, isso muitas vezes estava indo longe demais.
Bobby Tables

Respostas:

50

Gosto muito da maneira como Robert C. Martin (tio Bob) reafirma o princípio de responsabilidade única (vinculado ao PDF) :

Nunca deve haver mais de um motivo para uma classe mudar

É 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:

O SRP é um dos princípios mais simples e um dos mais difíceis de acertar. Reunir responsabilidades é algo que fazemos naturalmente. Encontrar e separar essas responsabilidades é muito do que realmente interessa o design de software.

Paddyslacker
fonte
2
Também gosto da maneira como ele afirma, parece mais fácil verificar quando relacionado à mudança do que à "responsabilidade" abstrata.
Matthieu M.
Essa é realmente uma excelente maneira de colocá-lo. Eu amo isso. Como uma observação, normalmente costumo pensar no SRP como se aplica muito mais fortemente aos métodos. Às vezes, uma classe precisa fazer duas coisas (talvez seja a classe que faz a ponte entre dois domínios), mas um método quase nunca deve estar fazendo mais do que aquilo que você pode descrever sucintamente por sua assinatura de tipo.
CodexArcanum
11
Apenas mostrei isso ao meu Grad - ótima leitura e um ótimo lembrete para mim mesmo.
Martijn Verburg
11
Ok, isso faz muito sentido quando você o combina com a idéia de que o código não-superengenharia deve planejar apenas as mudanças que provavelmente ocorrerão no futuro próximo, e não todas as mudanças possíveis. Eu repetiria isso, então, um pouco, pois "deve haver apenas uma razão para uma classe mudar que provavelmente ocorrerá no futuro próximo". Isso incentiva o favorecimento da simplicidade em partes do projeto que provavelmente não serão alteradas e o desacoplamento em partes que provavelmente serão alteradas.
dsimcha
18

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?

Brandon
fonte
Embora essas diretrizes possam ou não ser úteis, ela realmente não tem nada a ver com o SRP, conforme definido no SOLID, não é?
Sara
Obrigado. Não posso acreditar em parte da insanidade envolvida nos chamados princípios SOLID, onde eles tornam o código muito simples cem vezes mais complexo sem uma boa razão . Os pontos que você descreve descrevem as razões do mundo real para implementar o SRP. Eu acho que os princípios que você deu acima devem se tornar seu próprio acrônimo, e jogar "SOLID" fora, faz mais mal do que bem. "Astronautas de arquitetura" , de fato, como você apontou no tópico que me levou até aqui.
Nicholas Petersen
4

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.

Jeremy
fonte
4

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.

Noite escura
fonte
Eu acho que o principal problema aqui é a palavra "manipulação". É genérico demais para definir uma única responsabilidade. Eu acho que seria melhor ter um componente responsável pela impressão da fatura e outro pela atualização da fatura, em vez de um único identificador de componente {imprimir, atualizar e - por que não? - exibição, qualquer que seja} fatura .
Machado
11
O OP está essencialmente perguntando "Como você define uma responsabilidade?" então, quando você diz que a responsabilidade é o que você define, parece que está apenas repetindo a pergunta.
Despertar
2

Eu sempre o vejo em dois níveis:

  • Garanto que meus métodos façam apenas uma coisa e façam bem
  • Eu vejo uma classe como um agrupamento lógico (OO) desses métodos que representa uma coisa bem

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 como walk(), run()e bite(DotNetDeveloper spawnOfBill)(desculpe, não pude resistir; p).

Se Dogficar difícil de manejar, eu pensaria em como grupos desses métodos podem ser modelados juntos em outra classe, como uma Movementclasse, que poderia conter meus métodos walk()e run().

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.

Martijn Verburg
fonte
Mordida deve realmente ter uma instância de objeto, e uma DotNetDeveloper deve ser uma subclasse de Person (geralmente, de qualquer maneira!)
Alan Pearce
@Alan - Não - fixa que para você :-)
Martijn Verburg
1

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 Dogclasse, que possui métodos para walk(), run()e bark(). Eu não estou indo para adicionar métodos para meaow(), squeak(), slither()ou fly()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).

JohnL
fonte
+1 em "se o seu cão voar, provavelmente você deve parar de jogá-lo pela janela". :)
Bobby Tables
Além da questão do que a classe deve representar, o que uma instância representa? Se alguém considera SeesFooduma característica de DogEyes, Barkcomo algo feito por a DogVoicee Eatcomo feito por a DogMouth, então a lógica if (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.
Supercat 25/03
@ supercat é um ponto justo, embora o contexto seja importante. Se o código que você menciona estiver dentro da Dogclasse, provavelmente está Dogrelacionado. Caso contrário, você provavelmente acabaria com algo como myDog.Eyes.SeesFoode não apenas eyes.SeesFood. Outra possibilidade é que Dogexpõe a ISeeinterface que exige a Dog.Eyespropriedade e o SeesFoodmétodo.
johnl
@ JohnL: Se a mecânica real da visão é tratada pelos olhos de um cão, essencialmente da mesma maneira que a de um gato ou zebra, pode fazer sentido ter a mecânica tratada por uma Eyeclasse, 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.
Supercat #
Então você usaria a interface ISee como eu descrevo no meu comentário.
johnl
1

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.

David Thornley
fonte
0

É 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.

Klaim
fonte
-1

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.

user1488779
fonte
-1

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.

AKS
fonte
-1

"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.

kiwicomb123
fonte
11
Uma razão para mudar não tem nada a ver com o número de casos de uso, atores ou com que probabilidade uma mudança é. Você não deve fazer uma lista de alterações prováveis. É correto que uma alteração deva impactar apenas uma classe. Ser capaz de estender uma classe para acomodar essa mudança é bom, mas esse é o princípio aberto e fechado, não o SRP.
Candied_orange
Por que não devemos fazer uma lista das mudanças mais prováveis? Design especulativo?
precisa saber é o seguinte
porque não ajudará, não estará completo e existem maneiras mais eficazes de lidar com as mudanças do que tentar prever. Apenas espere. Isole as decisões e o impacto será mínimo.
Candied_orange
Ok, eu entendo, isso viola o princípio "você não precisa disso".
precisa saber é o seguinte
Na verdade, o yagni também pode ser empurrado para longe. Ele realmente pretende impedir você de implementar casos de uso especulativos. Para não impedir que você isole um caso de uso implementado.
Candied_orange