Ao usar o Princípio da Responsabilidade Única, o que constitui uma "responsabilidade"?


198

Parece bastante claro que "princípio de responsabilidade única" não significa "apenas uma coisa". É para isso que servem os métodos.

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

Bob Martin diz que "as aulas devem ter apenas um motivo para mudar". Mas isso é difícil de entender, se você é um programador novo no SOLID.

Escrevi uma resposta para outra pergunta , na qual sugeri que as responsabilidades são como cargos e dancei sobre o assunto usando uma metáfora de restaurante para ilustrar meu argumento. Mas isso ainda não articula um conjunto de princípios que alguém poderia usar para definir as responsabilidades de suas classes.

Então como você faz isso? Como você determina quais responsabilidades cada classe deve ter e como define uma responsabilidade no contexto do SRP?


28
Postar a Revisão do Código e ser rasgada :-D
Jörg W Mittag

8
@ JörgWMittag Ei, agora, não assuste as pessoas:)
Flambino

118
Perguntas como essa de membros veteranos demonstram que as regras e princípios que tentamos cumprir não são de maneira alguma direta ou simples . Eles são [meio que] auto-contraditórios e místicos ... como qualquer bom conjunto de regras deveria ser. E, gostaria de acreditar em perguntas como essa, humildes e sábios, e dar esperança àqueles que se sentem irremediavelmente estúpidos. Obrigado Robert!
svidgen

41
Gostaria de saber se esta questão teria sido downvoted + marcada duplicar imediatamente se foram publicados pelos um noob :)
Andrejs

9
@rmunn: ou em outras palavras - grande representante atrai ainda mais rep, porque ninguém cancelada prejuízo humano básico em Stackexchange
Andrejs

Respostas:


117

Uma maneira de resolver isso é imaginar possíveis mudanças nos requisitos de projetos futuros e se perguntar o que você precisará fazer para que eles aconteçam.

Por exemplo:

Novos requisitos de negócios: os usuários localizados na Califórnia recebem um desconto especial.

Exemplo de alteração "boa": preciso modificar o código em uma classe que calcula descontos.

Exemplo de alterações ruins: preciso modificar o código na classe User, e essa alteração terá um efeito cascata em outras classes que usam a classe User, incluindo classes que não têm nada a ver com descontos, por exemplo, inscrição, enumeração e gerenciamento.

Ou:

Novo requisito não funcional: começaremos a usar o Oracle em vez do SQL Server

Exemplo de boa alteração: basta modificar uma única classe na camada de acesso a dados que determine como persistir os dados nos DTOs.

Alteração ruim: preciso modificar todas as minhas classes da camada de negócios porque elas contêm lógica específica do SQL Server.

A idéia é minimizar a presença de possíveis alterações futuras, restringindo as modificações de código a uma área de código por área de mudança.

No mínimo, suas aulas devem separar preocupações lógicas e físicas. Um grande conjunto de exemplos podem ser encontrados no System.IOnamespace: não podemos encontrar um vários tipos de fluxos físicos (por exemplo FileStream, MemoryStreamou NetworkStream) e vários leitores e escritores ( BinaryWriter, TextWriter) que trabalham em um nível lógico. Ao separar-los desta forma, evitamos explosão combinatória: em vez de precisar FileStreamTextWriter, FileStreamBinaryWriter, NetworkStreamTextWriter, NetworkStreamBinaryWriter, MemoryStreamTextWriter, e MemoryStreamBinaryWriter, você simplesmente ligar o escritor e o fluxo e você pode ter o que quiser. Posteriormente, podemos adicionar, digamos, um XmlWriter, sem a necessidade de reimplementá-lo para memória, arquivo e rede separadamente.


34
Embora eu concorde com o pensamento futuro, existem princípios como o YAGNI e metodologias como o TDD que sugerem o contrário.
Robert Harvey

87
YAGNI nos diz para não construir coisas que não precisamos hoje. Não diz para não construir coisas de uma maneira que seja extensível. Consulte também o princípio aberto / fechado , que declara "entidades de software (classes, módulos, funções, etc.) devem estar abertas para extensão, mas fechadas para modificação".
John Wu

18
@ JohnW: +1 para o seu comentário YAGNI sozinho. Não acredito no quanto preciso explicar às pessoas que a YAGNI não é uma desculpa para construir um sistema rígido e inflexível que não pode reagir à mudança - ironicamente, o oposto do que o SRP e os diretores de abertura / fechamento estão buscando.
Greg Burghardt 27/03

36
@ JohnWu: Eu discordo, YAGNI nos diz exatamente para não construir coisas que não precisamos hoje. Legibilidade e testes, por exemplo, são algo que um programa sempre precisa "hoje", portanto o YAGNI nunca é uma desculpa para não adicionar estrutura e pontos de injeção. No entanto, assim que a "extensibilidade" agrega um custo significativo para o qual os benefícios não são óbvios "hoje", a YAGNI pretende evitar esse tipo de extensibilidade, uma vez que a última leva à superengenharia.
Doc Brown

9
@ JohnWu Mudamos do SQL 2008 para 2012. Havia um total de duas consultas que precisavam ser alteradas. E do SQL Auth para confiável? Por que isso seria uma alteração de código; alterar a connectionString no arquivo de configuração é suficiente. Mais uma vez, YAGNI. O YAGNI e o SRP às vezes são preocupações concorrentes, e você precisa julgar qual deles tem o melhor custo / benefício.
21417 Andy

76

Na prática, as responsabilidades são limitadas por coisas que provavelmente mudarão. Portanto, infelizmente, não existe uma maneira científica ou de fórmula para chegar ao que constitui uma responsabilidade. É uma decisão de julgamento.

É sobre o que, na sua experiência , provavelmente mudará.

Tendemos a aplicar a linguagem do princípio com uma raiva hiperbólica, literal e zelosa. Nós tendemos a dividir as classes porque elas podem mudar, ou ao longo de linhas que simplesmente nos ajudam a resolver problemas. (A última razão não é inerentemente ruim.) Mas, o SRP não existe por si só; está a serviço da criação de software sustentável.

Então, novamente, se as divisões não forem motivadas por mudanças prováveis , elas não estarão realmente em serviço no SRP 1 se o YAGNI for mais aplicável. Ambos servem ao mesmo objetivo final. E ambos são questões de julgamento - esperançosamente julgamento experiente .

Quando o tio Bob escreve sobre isso, ele sugere que pensemos em "responsabilidade" em termos de "quem está pedindo a mudança". Em outras palavras, não queremos que o Partido A perca o emprego porque o Partido B pediu uma mudança.

Quando você escreve um módulo de software, deseja garantir que, quando as alterações forem solicitadas, essas alterações possam se originar apenas de uma única pessoa, ou melhor, de um único grupo de pessoas fortemente associado que representa uma única função de negócios definida de maneira restrita. Você deseja isolar seus módulos das complexidades da organização como um todo e projetar seus sistemas de modo que cada módulo seja responsável (responda) às necessidades de apenas uma função comercial. ( Tio Bob - O princípio da responsabilidade única )

Desenvolvedores bons e experientes terão uma noção de quais mudanças são prováveis. E essa lista mental varia um pouco de acordo com o setor e a organização.

O que constitui uma responsabilidade em seu aplicativo específico, em sua organização específica, é, em última análise, uma questão de julgamento experiente . É sobre o que provavelmente mudará. E, de certa forma, é sobre quem é o dono da lógica interna do módulo.


1. Para ser claro, isso não significa que sejam divisões ruins . Eles podem ser grandes divisões que melhoram drasticamente a legibilidade do código. Significa apenas que eles não são conduzidos pelo SRP.


11
Melhor resposta e, na verdade, cita os pensamentos do tio Bob. Quanto ao que provavelmente mudará, todo mundo faz uma grande diferença em relação à E / S: "e se mudarmos o banco de dados?" ou "e se mudarmos de XML para JSON?" Eu acho que isso geralmente é errado. A verdadeira questão deve ser "e se precisarmos alterar esse int para um float, adicionar um campo e alterar esse String para uma Lista de Strings?"
user949300

2
Isso é trapaça. A responsabilidade única por si só é apenas uma maneira proposta de "isolamento da mudança". Explicar que você precisa isolar as alterações para manter a responsabilidade "única" não sugere como fazer isso, apenas explica a origem do requisito.
Basilevs 28/03

6
@Basilevs Estou tentando aprimorar a deficiência que você está vendo nesta resposta - para não mencionar a resposta do tio Bob! Mas, talvez eu precise esclarecer que o SRP não visa garantir que "uma mudança" tenha impacto apenas em 1 classe. Trata-se de garantir que cada classe responda a apenas "uma mudança". ... Trata-se de tentar desenhar suas flechas de cada classe para um único proprietário. Não de cada proprietário para uma única classe.
svidgen

2
Obrigado por fornecer uma resposta pragmática! Até o tio Bob alerta contra a adesão zelosa aos princípios do SOLID na arquitetura ágil . Não tenho a citação à mão, mas ele basicamente diz que dividir responsabilidades aumenta inerentemente o nível de abstração em seu código e que toda abstração tem um custo, portanto, verifique se o benefício de seguir o SRP (ou outros princípios) supera o custo de adicionar mais abstração. (continuação próximo comentário)
Michael L.

4
É por isso que devemos colocar o produto na frente do cliente o mais cedo e com a frequência razoável, para que eles forcem alterações em nosso design e possamos ver quais áreas provavelmente mudarão nesse produto. Além disso, ele adverte que não podemos nos proteger de todo tipo de mudança. Para qualquer aplicativo, será difícil fazer certos tipos de alterações. Precisamos garantir que essas são as mudanças com menor probabilidade de acontecer.
Michael L.

29

Sigo "as aulas devem ter apenas um motivo para mudar".

Para mim, isso significa pensar em esquemas flexíveis que o proprietário do meu produto pode inventar ("Precisamos oferecer suporte a dispositivos móveis!", "Precisamos ir para a nuvem!", "Precisamos oferecer suporte a chinês!"). Bons projetos limitarão o impacto desses esquemas a áreas menores e os tornarão relativamente fáceis de realizar. Projetos ruins significam usar muito código e fazer várias alterações arriscadas.

A experiência é a única coisa que encontrei para avaliar adequadamente a probabilidade desses esquemas malucos - porque facilitar um pode dificultar outros dois - e avaliar a bondade de um design. Programadores experientes podem imaginar o que precisariam fazer para alterar o código, o que está por aí para mordê-los na bunda e quais truques facilitam as coisas. Programadores experientes têm uma boa sensação de como estão ferrados quando o proprietário do produto pede coisas malucas.

Na prática, acho que os testes de unidade ajudam aqui. Se seu código for inflexível, será difícil testar. Se você não pode injetar zombarias ou outros dados de teste, provavelmente não poderá injetar esse SupportChinesecódigo.

Outra métrica aproximada é o passo do elevador. Lotes de elevadores tradicionais são "se você estivesse em um elevador com um investidor, poderia vender a ele uma ideia?". As startups precisam ter descrições curtas e simples do que estão fazendo - qual é o foco delas. Da mesma forma, classes (e funções) devem ter uma descrição simples do que fazem . Não "essa classe implementa algum fubar, de forma que você possa usá-lo nesses cenários específicos". Algo que você pode dizer a outro desenvolvedor: "Esta classe cria usuários". Se você não pode se comunicar isso para outros desenvolvedores, você está indo para obter bugs.


Às vezes, você implementa o que você pensou que seria uma mudança confusa, e ela é simples ou um pequeno refator simplifica e adiciona funcionalidades úteis ao mesmo tempo. Mas sim, geralmente você pode ver problemas chegando.

16
Sou um grande defensor da ideia de "argumento do elevador". Se é difícil explicar o que uma classe faz em uma ou duas frases, você está em um território arriscado.
Ivan

11
Você toca em um ponto importante: a probabilidade desses esquemas malucos varia dramaticamente de um proprietário para outro. Você precisa confiar não apenas em sua experiência geral, mas em quão bem conhece o proprietário do projeto. Eu trabalhei para pessoas que queriam reduzir nossos sprints para uma semana e ainda não conseguiam evitar mudar de direção no meio do sprint.
Kevin Krumwiede 28/03

11
Além dos benefícios óbvios, documentar seu código usando "arremessos de elevador" também serve para ajudá-lo a pensar no que seu código está fazendo usando linguagem natural que acho útil para descobrir várias responsabilidades.
288 Alexander Alexander

11
@KevinKrumwiede É para isso que servem as metodologias "Frango correndo com a cabeça cortada" e "Wild Goose Chase"!

26

Ninguém sabe. Ou pelo menos, não podemos concordar com uma definição. É isso que torna a SPR (e outros princípios do SOLID) bastante controversa.

Eu argumentaria que ser capaz de descobrir o que é ou não uma responsabilidade é uma das habilidades que o desenvolvedor de software precisa aprender ao longo de sua carreira. Quanto mais código você escrever e revisar, mais experiência terá para determinar se algo é de responsabilidade única ou múltipla. Ou se a responsabilidade única for dividida em partes separadas do código.

Eu argumentaria que o objetivo principal do SRP não é ser uma regra difícil. É para nos lembrar de estarmos atentos à coesão no código e sempre colocar algum esforço consciente para determinar qual código é coeso e o que não é.


20
Novos programadores tendem a tratar o SOLID como se fosse um conjunto de leis, o que não é. É apenas um agrupamento de boas idéias para ajudar as pessoas a melhorar o design da classe. Infelizmente, as pessoas tendem a levar esses princípios muito a sério; Recentemente, vi um anúncio de emprego que citava o SOLID como um dos requisitos do trabalho.
Robert Harvey

9
+42 para o último parágrafo. Como o @RobertHarvey diz, coisas como SPR, SOLID e YAGNI não devem ser tomadas como " regras absolutas ", mas como princípios gerais de "bons conselhos". Entre eles (e outros), o conselho às vezes é contraditório, mas equilibrar esse conselho (em vez de seguir um conjunto rígido de regras) irá (com o tempo, à medida que sua experiência cresce) o guiar a produzir um software melhor. Deve haver apenas uma "regra absoluta" no desenvolvimento de software: " Não há regras absolutas ".
TripeHound 28/03

Este é um esclarecimento muito bom sobre um aspecto do SRP. Mas, mesmo que os princípios do SOLID não sejam regras rígidas, eles não serão terrivelmente valiosos se ninguém entender o que eles significam - menos ainda se a sua afirmação de que "ninguém sabe" for realmente verdadeira! ... faz sentido que eles sejam difíceis de entender. Como em qualquer habilidade, há algo que distingue o bom do menos bom! Mas ... "ninguém sabe" torna mais um ritual de trote. (E eu não acredito que essa é a intenção do SOLID!)
svidgen

3
Com "Ninguém sabe", espero que o @Euphoric simplesmente signifique que não há uma definição precisa que funcione para todos os casos de uso. É algo que requer um certo grau de julgamento. Acho que uma das melhores maneiras de determinar onde estão suas responsabilidades é iterar rapidamente e deixar sua base de código lhe dizer . Procure por "cheiros", pois seu código não é fácil de manter. Por exemplo, quando uma alteração em uma única regra de negócios começa a ter efeitos em cascata por meio de classes aparentemente não relacionadas, você provavelmente tem uma violação do SRP.
Michael L.

11
Em segundo lugar, sinceramente, o @TripeHound e outros que apontaram que todas essas "regras" não existem para definir a única religião verdadeira do desenvolvimento, mas para aumentar a probabilidade de desenvolvimento de software sustentável. Tenha muito cuidado de seguir uma "melhor prática" se você não pode explicar como ele promove o software de fácil manutenção, melhora a qualidade ou aumenta a eficiência do desenvolvimento ..
Michael L.

5

Eu acho que o termo "responsabilidade" é útil como uma metáfora porque nos permite usar o software para investigar quão bem o software está organizado. Em particular, eu me concentraria em dois princípios:

  • A responsabilidade é proporcional à autoridade.
  • Não existem duas entidades responsáveis ​​pela mesma coisa.

Esses dois princípios nos permitem distribuir responsabilidades de maneira significativa porque elas se desempenham. Se você está autorizando um pedaço de código a fazer algo por você, ele precisa ter uma responsabilidade pelo que faz. Isso causa a responsabilidade de uma classe crescer, expandindo sua "única razão para mudar" para escopos cada vez mais amplos. No entanto, à medida que você amplia as coisas, você naturalmente começa a se deparar com situações em que várias entidades são responsáveis ​​pela mesma coisa. Isso está cheio de problemas na responsabilidade da vida real, portanto, certamente, também é um problema na codificação. Como resultado, esse princípio faz com que os escopos se estreitem, à medida que você subdivide a responsabilidade em parcelas não duplicadas.

Além desses dois, um terceiro princípio parece razoável:

  • A responsabilidade pode ser delegada

Considere um programa recém-inventado ... uma lousa em branco. No início, você tem apenas uma entidade, que é o programa como um todo. É responsável por ... tudo. Naturalmente, em algum momento, você começará a delegar responsabilidades em funções ou classes. Nesse ponto, as duas primeiras regras entram em jogo forçando você a equilibrar essa responsabilidade. O programa de nível superior ainda é responsável pela produção geral, assim como um gerente é responsável pela produtividade de sua equipe, mas a cada subentidade foi delegada responsabilidade e, com ela, a autoridade para executá-la.

Como um bônus adicional, isso torna o SOLID particularmente compatível com qualquer desenvolvimento de software corporativo que possa ser necessário. Toda empresa no planeta tem algum conceito de como delegar responsabilidades e nem todas concordam. Se você delegar a responsabilidade no seu software de uma forma que lembra a delegação da sua empresa, será muito mais fácil para futuros desenvolvedores se atualizarem sobre como você faz as coisas nessa empresa.


Não tenho 100% de certeza de que isso explica tudo. Mas acho que explicar "responsabilidade" com relação à "autoridade" é uma maneira perspicaz de expressá-la! (+1)
svidgen 28/03

Pirsig disse: "Você tende a criar seus problemas na máquina", o que me dá uma pausa.

@nocomprende Você também tende a fortalecer seus pontos fortes na máquina. Eu diria que, quando seus pontos fortes e fracos são as mesmas coisas, é aí que fica interessante.
Cort Ammon 28/03

5

Em desta conferência na Universidade de Yale, Tio Bob dá a este engraçado exemplo:

Digite a descrição da imagem aqui

Ele diz que Employeetem três razões para mudar, três fontes de requisitos de mudança e fornece essa explicação bem - humorada e explícita , mas ilustrativa, mas ilustrativa:

  • Se o CalcPay()método apresentar um erro e custar à empresa milhões de US $, o CFO o despedirá .

  • Se o ReportHours()método apresentar um erro e custar à empresa milhões de US $, o COO o despedirá .

  • Se o WriteEmmployee(método) apresentar um erro que cause o apagamento de muitos dados e custe milhões de dólares à empresa, o CTO o acionará .

Portanto, ter três executivos diferentes de nível C potencialmente demitindo você por erros dispendiosos na mesma classe significa que a classe tem muitas responsabilidades.

Ele fornece essa solução que resolve a violação do SRP, mas ainda precisa resolver a violação do DIP, que não é mostrada no vídeo.

Digite a descrição da imagem aqui


Este exemplo se parece mais com uma classe que tem responsabilidades erradas .
Robert Harvey

4
@RobertHarvey Quando uma classe tem muitas responsabilidades, significa que as responsabilidades extras são as incorretas .
Tulains Córdova 28/03

5
Eu ouço o que você está dizendo, mas não acho convincente. Há uma diferença entre uma classe com muitas responsabilidades e uma classe fazendo algo que não é da sua conta. Pode parecer o mesmo, mas não é; contar amendoins não é o mesmo que chamá-los de nozes. É o princípio do tio Bob e o exemplo do tio Bob, mas se fosse suficientemente descritivo, não precisaríamos dessa pergunta.
Robert Harvey

@RobertHarvey, qual é a diferença? Essas situações me parecem isomórficas.
21320 Paul

3

Eu acho que uma maneira melhor de subdividir as coisas do que "razões para mudar" é começar pensando se faria sentido exigir que o código que precisa executar duas (ou mais) ações precise manter uma referência a objeto separada para cada ação e se seria útil ter um objeto público que pudesse executar uma ação, mas não a outra.

Se as respostas para ambas as perguntas forem afirmativas, isso sugere que as ações devem ser realizadas por classes separadas. Se as respostas para as duas perguntas não forem, isso sugere que, do ponto de vista público, deve haver uma classe; se o código para isso for pesado, ele pode ser subdividido internamente em classes privadas. Se a resposta para a primeira pergunta for não, mas a segunda for sim, deve haver uma classe separada para cada ação mais uma classe composta que inclua referências a instâncias das outras.

Se houver classes separadas para o teclado de uma caixa registradora, sinal sonoro, leitura numérica, impressora de recibos e gaveta de dinheiro e nenhuma classe composta para uma caixa registradora completa, o código que deve processar uma transação pode acabar sendo invocado acidentalmente em um Uma maneira que recebe a entrada do teclado de uma máquina, produz ruído do sinal sonoro de uma segunda máquina, mostra números no visor de uma terceira máquina, imprime um recibo na impressora da quarta máquina e abre a quinta gaveta da máquina. Cada uma dessas subfunções pode ser útil para uma classe separada, mas também deve haver uma classe composta que as junte. A classe composta deve delegar o máximo de lógica possível para as classes constituintes,

Pode-se dizer que a "responsabilidade" de cada classe é incorporar alguma lógica real ou fornecer um ponto de conexão comum para várias outras classes que o fazem, mas o importante é se concentrar, em primeiro lugar, em como o código do cliente deve exibir uma classe. Se faz sentido para o código do cliente ver algo como um único objeto, o código do cliente deve vê-lo como um único objeto.


Este é um bom conselho. Vale a pena ressaltar que você divide as responsabilidades de acordo com mais critérios do que apenas o SRP.
Jørgen Fogh 28/03

11
Analogia do carro: não preciso saber quanto gás há no tanque de outra pessoa ou quero ligar os limpadores de outra pessoa. (mas essa é a definição da Internet) (Shh você vai arruinar a história!)

11
@nocomprende - "Não preciso saber quanto gás há no tanque de outra pessoa" -, a menos que você seja um adolescente tentando decidir qual dos carros da família "emprestará" para sua próxima viagem ...;)
alephzero

3

É difícil acertar o SRP. É principalmente uma questão de atribuir 'trabalhos' ao seu código e garantir que cada parte tenha responsabilidades claras. Como na vida real, em alguns casos, dividir o trabalho entre as pessoas pode ser bastante natural, mas em outros casos pode ser realmente complicado, especialmente se você não os conhece (ou o trabalho).

Eu sempre recomendo que você escreva um código simples que funcione primeiro e depois refatorar um pouco: você tenderá a ver como o código se agrupa naturalmente depois de um tempo. Eu acho que é um erro forçar responsabilidades antes que você saiba o código (ou pessoas) e o trabalho a ser feito.

Uma coisa que você notará é quando o módulo começa a fazer muito e é difícil depurar / manter. Este é o momento de refatorar; qual deve ser o trabalho principal e que tarefas poderiam ser dadas a outro módulo? Por exemplo, ele deve lidar com as verificações de segurança e os outros trabalhos, ou você deve fazer as verificações de segurança em outro local primeiro ou isso tornará o código mais complexo?

Use muitos indiretos e isso se tornará uma bagunça novamente ... quanto a outros princípios, este estará em conflito com outros, como KISS, YAGNI, etc. Tudo é uma questão de equilíbrio.


O SRP não é apenas a Coesão de Constantino em grande escala?
precisa

Você vai naturalmente encontrar esses padrões se você codificar o tempo suficiente, mas você pode acelerar a aprendizagem, nomeando-os e ajuda na comunicação ...
Christophe Roussy

@NickKeighley Eu acho que é coesão, não muito extensa, mas vista de outra perspectiva.
sdenham

3

"Princípio da responsabilidade única" é talvez um nome confuso. "Apenas um motivo para mudar" é uma descrição melhor do princípio, mas ainda é fácil entender mal. Não estamos falando de dizer o que faz com que os objetos mudem de estado em tempo de execução. Estamos discutindo o que pode levar os desenvolvedores a alterar o código no futuro.

A menos que esteja corrigindo um bug, a alteração ocorrerá devido a um requisito comercial novo ou alterado. Você terá que pensar fora do próprio código e imaginar quais fatores externos podem fazer com que os requisitos sejam alterados independentemente . Dizer:

  • As taxas de imposto são alteradas devido a uma decisão política.
  • O marketing decide alterar os nomes de todos os produtos
  • A interface do usuário deve ser redesenhada para estar acessível
  • O banco de dados está congestionado, então você precisa fazer algumas otimizações
  • Você precisa acomodar um aplicativo móvel
  • e assim por diante...

Idealmente, você deseja que fatores independentes afetem diferentes classes. Por exemplo, como as taxas de imposto mudam independentemente dos nomes dos produtos, as alterações não devem afetar as mesmas classes. Caso contrário, você corre o risco de uma alteração tributária introduzir um erro na nomeação de produtos, que é o tipo de acoplamento rígido que você deseja evitar com um sistema modular.

Portanto, não se concentre apenas no que pode mudar - qualquer coisa pode mudar no futuro. Concentre-se no que pode mudar de forma independente . As mudanças normalmente são independentes se forem causadas por diferentes atores.

Seu exemplo com cargos está no caminho certo, mas você deve interpretá-lo de maneira mais literal! Se o marketing pode causar alterações no código e o financiamento pode causar outras alterações, essas alterações não devem afetar o mesmo código, pois são literalmente diferentes cargos e, portanto, as alterações ocorrerão independentemente.

Para citar o tio Bob, que inventou o termo:

Quando você escreve um módulo de software, deseja garantir que, quando as alterações forem solicitadas, essas alterações possam se originar apenas de uma única pessoa, ou melhor, de um único grupo de pessoas fortemente associado que representa uma única função de negócios definida de maneira restrita. Você deseja isolar seus módulos das complexidades da organização como um todo e projetar seus sistemas de modo que cada módulo seja responsável (responda) às necessidades de apenas uma função comercial.

Para resumir: Uma "responsabilidade" está atendendo a uma única função comercial. Se mais de um ator puder fazer com que você mude de classe, provavelmente a classe quebra esse princípio.


De acordo com seu livro "Arquitetura Limpa", isso é exatamente correto. As regras de negócios devem vir de uma fonte e apenas uma vez. Isso significa que todos os setores de RH, operações e TI precisam cooperar na formulação de requisitos em uma "responsabilidade única". E esse é o princípio. +1
Benny Skogberg

2

Um bom artigo que explica os princípios de programação do SOLID e fornece exemplos de código que seguem e não seguem esses princípios é https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- design .

No exemplo referente ao SRP, ele fornece um exemplo de algumas classes de formas (círculo e quadrado) e uma classe projetada para calcular a área total de várias formas.

Em seu primeiro exemplo, ele cria a classe de cálculo de área e retorna sua saída como HTML. Mais tarde, ele decide que deseja exibi-lo como JSON e precisa alterar sua classe de cálculo de área.

O problema com este exemplo é que sua classe de cálculo de área é responsável por calcular a área de formas E exibir essa área. Ele então segue uma maneira melhor de fazer isso usando outra classe projetada especificamente para exibir áreas.

Este é um exemplo simples (e mais fácil de entender a leitura do artigo, pois possui trechos de código), mas demonstra a ideia central do SRP.


0

Antes de tudo, o que você tem são dois problemas separados : o problema de quais métodos colocar em suas classes e o problema de inchaço da interface.

Interfaces

Você tem esta interface:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Presumivelmente, você tem várias classes que estão em conformidade com a CustomerCRUDinterface (caso contrário, uma interface é desnecessária) e algumas funções do_crud(customer: CustomerCRUD)que recebem um objeto em conformidade. Mas você já quebrou o SRP: uniu essas quatro operações distintas.

Digamos que, posteriormente, você operaria em visualizações de banco de dados. Uma visualização de banco de dados possui apenas o Readmétodo disponível para ela. Mas você deseja escrever uma função do_query_stuff(customer: ???)que opere de forma transparente em tabelas ou visualizações completas; Readafinal, ele usa apenas o método.

Então crie uma interface

interface pública CustomerReader {leitura pública do cliente (customerID: int)}

e fatorar sua CustomerCrudinterface como:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Mas não há fim à vista. Pode haver objetos que podemos criar, mas não atualizar, etc. Essa toca de coelho é muito profunda. A única maneira sensata de aderir ao princípio da responsabilidade única é fazer com que todas as suas interfaces contenham exatamente um método . O Go realmente segue essa metodologia pelo que eu vi, com a grande maioria das interfaces contendo uma única função; se você deseja especificar uma interface que contém duas funções, é necessário criar desajeitadamente uma nova interface que combine as duas. Você logo recebe uma explosão combinatória de interfaces.

A saída dessa bagunça é usar a subtipagem estrutural (implementada no OCaml) em vez das interfaces (que são uma forma de subtipagem nominal). Nós não definimos interfaces; em vez disso, podemos simplesmente escrever uma função

let do_customer_stuff customer = customer.read ... customer.update ...

que chama os métodos que gostamos. O OCaml usará inferência de tipo para determinar que podemos transmitir qualquer objeto que implemente esses métodos. Neste exemplo, seria determinar que customertem tipo <read: int -> unit, update: int -> unit, ...>.

Aulas

Isso resolve a bagunça da interface ; mas ainda temos que implementar classes que contêm vários métodos. Por exemplo, devemos criar duas classes diferentes CustomerReadere CustomerWriter? E se quisermos mudar a forma como as tabelas são lidas (por exemplo, agora armazenamos nossas respostas em redis antes de buscar os dados), mas agora como elas são gravadas? Se você seguir essa cadeia de raciocínio até sua conclusão lógica, estará indissociavelmente à programação funcional :)


4
"Sem sentido" é um pouco forte. Eu poderia ficar atrás de "místico" ou "zen". Mas, não totalmente sem sentido!
svidgen

Você pode explicar um pouco mais por que a subtipagem estrutural é uma solução?
Robert Harvey

@RobertHarvey Reestruturado minha resposta significativamente
gardenhead

4
Eu uso interfaces mesmo quando tenho apenas uma classe implementando-a. Por quê? Zombando de testes unitários.
Eternal21

0

Na minha opinião, a coisa mais próxima de um SRP que vem à minha mente é um fluxo de uso. Se você não possui um fluxo de uso claro para qualquer classe, provavelmente a sua classe tem um cheiro de design.

Um fluxo de uso seria uma sucessão de chamada de método específica que forneceria um resultado esperado (portanto testável). Você basicamente define uma classe com os casos de uso que obteve no IMHO, é por isso que toda a metodologia do programa se concentra nas interfaces e na implementação.


0

É para conseguir que várias alterações de requisitos não exijam a alteração do seu componente .

Mas boa sorte ao entender isso à primeira vista, quando você ouvir falar sobre o SOLID.


Eu vejo muitos comentários dizendo que o SRP e o YAGNI podem se contradizer, mas o YAGN aplicado pela TDD (GOOS, London School) me ensinou a pensar e projetar meus componentes da perspectiva do cliente. Comecei a projetar minhas interfaces com o mínimo que um cliente gostaria que ele fizesse, é o quão pouco deve fazer . E esse exercício pode ser feito sem qualquer conhecimento de TDD.

Gosto da técnica descrita pelo tio Bob (infelizmente não me lembro de onde), que é algo como:

Pergunte a si mesmo, o que essa classe faz?

Sua resposta continha E ou Ou

Nesse caso, extraia essa parte da resposta, que é de sua própria responsabilidade

Essa técnica é absoluta e, como a @svidgen disse, o SRP é uma decisão judicial , mas, ao aprender algo novo, o absoluto é o melhor, é mais fácil sempre fazer alguma coisa. Verifique se o motivo para você não se separar é; uma estimativa educada, e não porque você não sabe. Esta é a arte, e é preciso experiência.


Eu acho que muitas das respostas parecem argumentar para se dissociar quando se fala em SRP .

SRP é não ter certeza de uma mudança não se propaga para baixo o gráfico de dependência.

Teoricamente, sem SRP , você não teria nenhuma dependência ...

Uma mudança não deve causar muitas mudanças no aplicativo, mas temos outros princípios para isso. No entanto, o SRP aprimora o Princípio Aberto Fechado . Esse princípio é mais sobre abstração, no entanto, abstrações menores são mais fáceis de reimplementar .

Portanto, ao ensinar o SOLID como um todo, tenha cuidado ao ensinar que o SRP permite alterar menos código quando os requisitos mudam; quando, na verdade, permite escrever menos código novo .


3
When learning something new, absolutes are the best, it is easier to just always do something.- Na minha experiência, novos programadores são dogmáticos demais. O absolutismo leva a desenvolvedores que não pensam e programação de cultos de carga. Dizer "apenas faça isso" é bom, desde que você entenda que a pessoa com quem você está falando terá que desaprender mais tarde o que você ensinou a ela.
Robert Harvey

@RobertHarvey, completamente verdade que cria um comportamento dogmático, e você precisa desaprender / reaprender à medida que ganha experiência. Este é o meu ponto, porém. Se um novo programador tenta fazer julgamentos sem nenhuma maneira de fundamentar sua decisão, parece ser inútil, porque eles não sabem por que funcionou, quando funcionou. Ao fazer as pessoas exagerarem , ensina-as a procurar as exceções em vez de fazer suposições não qualificadas. Tudo o que você disse sobre absolutismo está correto, e é por isso que deve ser apenas um ponto de partida.
precisa

@RobertHarvey, Um exemplo rápido da vida real : você pode ensinar seus filhos a serem sempre honestos, mas à medida que envelhecem, eles provavelmente perceberão algumas exceções nas quais as pessoas não querem ouvir seus pensamentos mais honestos. Esperar que uma criança de 5 anos faça um julgamento correto sobre ser honesto é otimista na melhor das hipóteses. :)
Chris Wohlert

0

Não há uma resposta clara para isso. Embora a pergunta seja estreita, as explicações não são.

Para mim, é algo como o Navalha de Occam, se você quiser. É um ideal onde eu tento medir meu código atual. É difícil identificá-lo com palavras claras e simples. Outra metáfora seria "um tópico" que é tão abstrato, isto é, difícil de entender, como "responsabilidade única". Um terceiro descritor seria "lidar com um nível de abstração".

O que isso significa praticamente?

Ultimamente eu uso um estilo de codificação que consiste principalmente de duas fases:

A fase I é melhor descrita como caos criativo. Nesta fase, escrevo o código à medida que os pensamentos fluem - ou seja, cru e feio.

A fase II é o oposto completo. É como limpar depois de um furacão. Isso exige mais trabalho e disciplina. E então eu olho para o código da perspectiva de um designer.

Atualmente, estou trabalhando principalmente em Python, o que me permite pensar em objetos e classes posteriormente. Primeira Fase I - Escrevo apenas funções e as espalho quase aleatoriamente em diferentes módulos. Na Fase II , depois que eu comecei as coisas, tenho uma visão mais detalhada de qual módulo lida com qual parte da solução. E, enquanto percorre os módulos, os tópicos são emergentes para mim. Algumas funções estão relacionadas tematicamente. Estes são bons candidatos para as aulas . E depois que transformei funções em classes - o que é quase feito com recuo e adição selfà lista de parâmetros em python;) - uso SRPcomo o Razcam da Occam para remover a funcionalidade de outros módulos e classes.

Um exemplo atual pode estar escrevendo uma pequena funcionalidade de exportação no outro dia.

Havia a necessidade de csv , excel e folhas de excel combinadas em um zip.

A funcionalidade simples foi realizada em três visualizações (= funções). Cada função usava um método comum para determinar filtros e um segundo método para recuperar os dados. Em cada função, a preparação da exportação ocorreu e foi entregue como uma resposta do servidor.

Havia muitos níveis de abstração misturados:

I) lidar com solicitação / resposta de entrada / saída

II) determinação de filtros

III) recuperando dados

IV) transformação de dados

O passo fácil foi usar uma abstração ( exporter) para lidar com as camadas II-IV em um primeiro passo.

O único restante foi o tópico que lida com solicitações / respostas . No mesmo nível de abstração está extraindo parâmetros de solicitação, o que é bom. Então, eu tinha para essa visão uma "responsabilidade".

Segundo, tive que dividir o exportador, que, como vimos, consistia em pelo menos três outras camadas de abstração.

A determinação dos critérios de filtro e a recuperação real estão quase no mesmo nível de abstração (os filtros são necessários para obter o subconjunto correto dos dados). Esses níveis foram colocados em algo como uma camada de acesso a dados .

Na próxima etapa, dividi os mecanismos de exportação reais: onde era necessário gravar em um arquivo temporal, dividi-o em duas "responsabilidades": uma para a gravação real dos dados no disco e outra parte que tratava do formato real.

Ao longo da formação das classes e módulos, as coisas ficaram mais claras, o que pertencia a onde. E sempre a pergunta latente, se a classe faz demais .

Como você determina quais responsabilidades cada classe deve ter e como define uma responsabilidade no contexto do SRP?

É difícil dar uma receita a seguir. É claro que eu poderia repetir a regra enigmática "um nível de abstração" - se isso ajudar.

Principalmente para mim, é um tipo de "intuição artística" que leva ao design atual; Eu modelo código como um artista pode esculpir argila ou pintar.

Imagine-me como um codificador Bob Ross ;)


0

O que eu tento fazer para escrever o código que segue o SRP:

  • Escolha um problema específico que você precisa resolver;
  • Escreva o código que resolve, escreva tudo em um método (por exemplo: main);
  • Analise cuidadosamente o código e, com base nos negócios, tente definir as responsabilidades que são visíveis em todas as operações que estão sendo executadas (esta é a parte subjetiva que também depende do negócio / projeto / cliente);
  • Por favor, note que toda a funcionalidade já está implementada; o que vem a seguir é apenas a organização do código (nenhum recurso ou mecanismo adicional será implementado a partir de agora nesta abordagem);
  • Com base nas responsabilidades que você definiu nas etapas anteriores (definidas com base nos negócios e na idéia "um motivo para mudar")), extraia uma classe ou método separado para cada um;
  • Observe que essa abordagem se preocupa apenas com o SPR; idealmente, deve haver etapas adicionais aqui tentando aderir a outros princípios também.

Exemplo:

Problema: obtenha dois números do usuário, calcule sua soma e envie o resultado para o usuário:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

Em seguida, tente definir responsabilidades com base nas tarefas que precisam ser executadas. A partir disso, extraia as classes apropriadas:

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

Em seguida, o programa refatorado se torna:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

Nota: este exemplo muito simples leva em consideração apenas o princípio SRP. O uso de outros princípios (por exemplo: o código "L" deve depender de abstrações em vez de concreções) traria mais benefícios ao código e o tornaria mais sustentável para mudanças nos negócios.


11
Seu exemplo é muito simples para ilustrar adequadamente o SRP. Ninguém faria isso na vida real.
Robert Harvey

Sim, em projetos reais, escrevo algum pseudocódigo em vez de escrever o código exato, como no meu exemplo. Após o pseudocódigo, tento dividir as responsabilidades, como fiz no exemplo. Enfim, é assim que eu faço.
Emerson Cardoso

0

Do livro de Robert C. Martins Arquitetura Limpa: Guia do Artesão para Estrutura e Design de Software , publicado em 10 de setembro de 2017, Robert escreve na página 62 o seguinte:

Historicamente, o SRP foi descrito desta maneira:

Um módulo deve ter um e apenas um motivo para mudar

Os sistemas de software são alterados para satisfazer usuários e partes interessadas; esses usuários e partes interessadas são a "razão para mudar". que o princípio está falando. De fato, podemos reformular o princípio para dizer o seguinte:

Um módulo deve ser responsável por um e apenas um usuário ou parte interessada

Infelizmente, a palavra "usuário" e "parte interessada" não são realmente a palavra certa a ser usada aqui. Provavelmente, haverá mais de um usuário ou parte interessada que deseja que o sistema seja alterado da maneira sã. Em vez disso, estamos realmente nos referindo a um grupo - uma ou mais pessoas que exigem essa mudança. Vamos nos referir a esse grupo como ator .

Portanto, a versão final do SRP é:

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

Portanto, não se trata de código. O SRP trata de controlar o fluxo de requisitos e necessidades de negócios, que só podem vir de uma empresa.


Não sei por que você está fazendo a distinção de que "não se trata de código". Claro que é sobre código; isso é desenvolvimento de software.
Robert Harvey

@RobertHarvey Meu argumento é que o fluxo de requisitos vem de uma fonte, o ator. Usuários e partes interessadas não estão no código, eles estão nas regras de negócios que nos chegam como requisitos. Portanto, o SRP é um processo para controlar esses requisitos, que para mim não é código. É Desenvolvimento de Software (!), Mas não código.
Benny Skogberg 23/07
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.