O princípio de responsabilidade única pode / deve ser aplicado ao novo código?

20

O princípio é definido como módulos com um motivo para mudar . Minha pergunta é: certamente essas razões para mudar não são conhecidas até que o código realmente comece a mudar? Praticamente todos os trechos de código têm várias razões pelas quais ele pode mudar, mas certamente tentar antecipar tudo isso e projetar seu código com isso em mente acabaria com um código muito ruim. Não é uma idéia melhor realmente começar a aplicar o SRP apenas quando as solicitações para alterar o código começarem a chegar? Mais especificamente, quando um trecho de código foi alterado mais de uma vez por mais de um motivo, provando que ele tem mais de um motivo para alterar. Parece muito anti-ágil tentar adivinhar os motivos da mudança.

Um exemplo seria um pedaço de código que imprime um documento. É solicitado um pedido para alterá-lo para imprimir em PDF e, em seguida, um segundo pedido é feito para alterá-lo para aplicar alguma formatação diferente ao documento. Nesse ponto, você tem provas de mais de um motivo para alterar (e violação do SRP) e deve fazer a refatoração apropriada.

SeeNoWeevil
fonte
6
@Frank - Na verdade, é comumente definida como aquele - ver, por exemplo en.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans
1
A maneira como você está expressando não é a maneira como entendo a definição de SRP.
Pieter B
2
Cada linha de código tem (pelo menos) duas razões para ser alterada: contribui para um bug ou interfere com um novo requisito.
Bart van Ingen Schenau
1
@BartvanIngenSchenau: LOL ;-) Se você vê dessa maneira, o SRP não pode ser aplicado em nenhum lugar.
Doc Brown
1
@ DocBrown: Você pode se não associar o SRP à alteração do código-fonte. Por exemplo, se você interpreta o SRP como capaz de fornecer um relato completo do que uma classe / função faz em uma frase sem usar a palavra e (e nenhuma expressão de doninha para contornar essa restrição).
Bart van Ingen Schenau

Respostas:

27

Obviamente, o princípio YAGNI lhe dirá para aplicar o SRP antes que você realmente precise. Mas a pergunta que você deve se fazer é: preciso aplicar o SRP primeiro e somente quando tiver que alterar meu código?

De acordo com a minha experiência, o aplicativo do SRP oferece um benefício muito antes: quando você precisa descobrir onde e como aplicar uma alteração específica no seu código. Para esta tarefa, você deve ler e entender suas funções e classes existentes. Isso fica muito mais fácil quando todas as suas funções e classes têm uma responsabilidade específica. Portanto, IMHO, você deve aplicar o SRP sempre que facilitar a leitura do código, sempre que tornar suas funções menores e mais auto-descritivas. Portanto, a resposta é sim , faz sentido aplicar o SRP mesmo para o novo código.

Por exemplo, quando seu código de impressão lê um documento, formata o documento e imprime o resultado em um dispositivo específico, essas são 3 responsabilidades claras e separáveis. Portanto, faça pelo menos três funções com elas, atribua-lhes os nomes. Por exemplo:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

Agora, quando você obtém um novo requisito para alterar a formatação do documento ou outro para imprimir em PDF, você sabe exatamente em qual dessas funções ou locais no código você deve aplicar as alterações e, mais importante, onde não.

Assim, sempre que você chegar a uma função que não entendem porque a função faz "muito", e você não tem certeza se e onde aplicar uma alteração, em seguida, considerar a possibilidade de refazer a função em funções separadas, menores. Não espere até ter que mudar alguma coisa. O código é 10 vezes mais lido do que alterado, e funções menores são muito mais fáceis de ler. Pela minha experiência, quando uma função tem uma certa complexidade, você sempre pode dividi-la em responsabilidades diferentes, independentemente de saber quais mudanças virão no futuro. Bob Martin normalmente vai um passo além, veja o link que eu dei nos meus comentários abaixo.

EDIT: ao seu comentário: A principal responsabilidade da função externa no exemplo acima não é imprimir em um dispositivo específico ou formatar o documento - é integrar o fluxo de trabalho de impressão . Portanto, no nível de abstração da função externa, um novo requisito como "documentos não deve mais ser formatado" ou "documento deve ser enviado por correio em vez de impresso" é apenas "o mesmo motivo" - ou seja, "o fluxo de trabalho de impressão mudou". Se falamos sobre coisas assim, é importante manter o nível certo de abstração .

Doc Brown
fonte
Geralmente, sempre desenvolvo com o TDD, portanto, no meu exemplo, não seria capaz de manter toda essa lógica em um módulo porque seria impossível testar. Este é apenas um subproduto do TDD e não porque estou deliberadamente aplicando o SRP. Meu exemplo tinha responsabilidades claras e separadas, talvez não seja um bom exemplo. Acho que o que estou perguntando é: você pode escrever algum novo código e dizer inequivocamente: Sim, isso não viola o SRP? As 'razões para mudar' não são definidas essencialmente pela empresa?
SeeWeWeil
3
@ thecapsaicinkid: sim, você pode (pelo menos por refatoração imediata). Mas você terá funções muito, muito pequenas - e nem todo programador gosta disso. Veja este exemplo: sites.google.com/site/unclebobconsultingllc/…
Doc Brown
Se você estava aplicando o SRP antecipando razões para mudar, no seu exemplo eu ainda poderia argumentar que há mais do que uma única mudança de razão. A empresa poderia decidir que não queria mais formatar um documento e depois decidir que queria que fosse enviado por email em vez de impresso. EDIT: Basta ler o link e, embora eu não goste do resultado final, 'Extrair até que você não consiga extrair mais' faz muito mais sentido e é menos ambíguo do que 'apenas um motivo para mudar'. Não é muito pragmático.
SeeNoWeevil
1
@thecapsaicinkid: veja minha edição. A principal responsabilidade da função externa não é imprimir em um dispositivo específico ou formatar o documento - é integrar o fluxo de trabalho de impressão. E quando este fluxo de trabalho muda, que é a one-and-única razão pela qual a função vai mudar
Doc Brown
Seu comentário sobre manter o nível certo de abstração parece ser o que estava faltando. Um exemplo, eu tenho uma classe que eu descreveria como 'Cria estruturas de dados a partir de uma matriz JSON'. Parece uma responsabilidade única para mim. Faz um loop pelos objetos em uma matriz JSON e os mapeia em POJOs. Se eu permanecer no mesmo nível de abstração da minha descrição, é difícil argumentar que há mais de um motivo para mudar, ou seja, 'Como o JSON mapeia o objeto'. Sendo menos abstrata eu poderia argumentar que tem mais de uma razão, por exemplo como eu mapear os campos de data alterações, como os valores numéricos são mapeados para dias etc
SeeNoWeevil
7

Acho que você está entendendo mal o SRP.

O único motivo para a mudança NÃO é alterar o código, mas o que o seu código faz.

Pieter B
fonte
3

Penso que a definição de SRP como "ter um motivo para mudar" é enganosa exatamente por esse motivo. Considere exatamente o valor nominal: o Princípio da Responsabilidade Única diz que uma classe ou função deve ter exatamente uma responsabilidade. Ter apenas um motivo para mudar é um efeito colateral de fazer apenas uma coisa para começar. Não há razão para que você não possa pelo menos fazer um esforço em direção à responsabilidade única em seu código sem saber nada sobre como isso pode mudar no futuro.

Uma das melhores pistas para esse tipo de coisa é quando você escolhe nomes de classe ou função. Se não for imediatamente aparente como a classe deve ser nomeada, ou o nome é particularmente longo / complexo, ou se o nome usar termos genéricos como "gerente" ou "utilitário", provavelmente está violando o SRP. Da mesma forma, ao documentar a API, ela deve se tornar rapidamente aparente se você estiver violando o SRP com base na funcionalidade que está descrevendo.

Obviamente, existem nuances no SRP que você não pode conhecer até mais tarde no projeto - o que parecia ser uma única responsabilidade, eram duas ou três. Esses são os casos em que você precisará refatorar para implementar o SRP. Mas isso não significa que o SRP deve ser desconsiderado até você receber uma solicitação de alteração; isso derrota o propósito do SRP!

Para falar diretamente com seu exemplo, considere documentar seu método de impressão. Se você diria "este método formata os dados para imprimir e envia para a impressora", que e é o que você recebe: isso não é uma única responsabilidade, que é duas responsabilidades: formatação e envio para a impressora. Se você reconhecer isso e dividi-los em duas funções / classes, quando suas solicitações de mudança aparecerem, você já terá apenas um motivo para mudar cada seção.

Adrian
fonte
3

Um exemplo seria um pedaço de código que imprime um documento. É solicitado um pedido para alterá-lo para imprimir em PDF e, em seguida, um segundo pedido é feito para alterá-lo para aplicar alguma formatação diferente ao documento. Nesse ponto, você tem provas de mais de um motivo para alterar (e violação do SRP) e deve fazer a refatoração apropriada.

Eu muitas vezes me levantei gastando muito tempo adaptando o código para acomodar essas alterações. Em vez de apenas imprimir o PDF estúpido.

Refatorar para reduzir o código

O padrão de uso único pode criar inchaço no código. Onde os pacotes são poluídos com pequenas classes específicas que criam uma pilha de código inútil que não faz sentido individualmente. Você precisa abrir dezenas de arquivos de origem apenas para entender como chega à parte de impressão. Além disso, pode haver centenas, senão milhares de linhas de código em funcionamento, apenas para executar 10 linhas de código que fazem a impressão real.

Criar um Bullseye

O padrão de uso único visava reduzir o código fonte e melhorar a reutilização do código. Foi criado para criar especialização e implementações específicas. Uma espécie de bullseyecódigo fonte para você go to specific tasks. Quando havia um problema com a impressão, você sabia exatamente para onde ir para corrigi-lo.

O uso único não significa fraturamento ambíguo

Sim, você tem um código que já imprime um documento. Sim, agora você deve alterar o código para também imprimir PDFs. Sim, agora você deve alterar a formatação do documento.

Você tem certeza de usageque mudou significativamente?

Se a refatoração fizer com que seções do código-fonte se tornem excessivamente generalizadas. Ao ponto em que a intenção original printing stuffnão é mais explícita, você criou uma fratura ambígua no código-fonte.

O novo cara será capaz de descobrir isso rapidamente?

Sempre mantenha seu código-fonte da organização mais fácil de entender.

Não seja relojoeiro

Muitas vezes eu vi desenvolvedores colocar uma ocular e focar nos pequenos detalhes, a ponto de ninguém mais poder juntar as peças novamente, caso desmoronasse.

insira a descrição da imagem aqui

Reactgular
fonte
2

Um motivo para a mudança é, em última análise, uma alteração na especificação ou nas informações sobre o ambiente em que o aplicativo é executado. Portanto, um único princípio de responsabilidade está solicitando que você escreva cada componente (classe, função, módulo, serviço ...) para que ele considere o mínimo possível da especificação e do ambiente de execução.

Como você conhece a especificação e o ambiente ao escrever o componente, pode aplicar o princípio.

Se você considerar o exemplo de código que imprime um documento. Você deve considerar se pode definir o modelo de layout sem considerar que o documento terminará em PDF. Você pode, então o SRP está lhe dizendo que deveria.

Claro que YAGNI está dizendo que você não deveria. Você precisa encontrar um equilíbrio entre os princípios de design.

Jan Hudec
fonte
2

Flup está indo na direção certa. O "princípio de responsabilidade única" se aplicava originalmente aos procedimentos. Por exemplo, Dennis Ritchie diria que uma função deve fazer uma coisa e fazê-la bem. Então, em C ++, Bjarne Stroustrup diria que uma classe deve fazer uma coisa e fazê-la bem.

Observe que, exceto como regra geral, esses dois têm pouco ou nada a ver um com o outro. Eles atendem apenas ao que é conveniente para expressar na linguagem de programação. Bem, isso é alguma coisa. Mas é uma história bem diferente do que o flup está dirigindo.

As implementações modernas (por exemplo, ágil e DDD) se concentram mais no que é importante para os negócios do que no que a linguagem de programação pode expressar. A parte surpreendente é que as linguagens de programação ainda não o alcançaram. As linguagens antigas do tipo FORTRAN capturam responsabilidades que se encaixam nos principais modelos conceituais da época: os processos aplicados a cada cartão enquanto passava pelo leitor de cartões ou (como em C) o processamento que acompanhava cada interrupção. Depois vieram as linguagens ADT, que amadureceram a ponto de capturar o que o pessoal do DDD mais tarde reinventaria como importante (embora Jim Neighbours tivesse a maior parte disso descoberto, publicado e usado em 1968): o que hoje chamamos de classes . (Eles NÃO são módulos.)

Este passo foi menos uma evolução do que um balanço do pêndulo. À medida que o pêndulo foi alterado para dados, perdemos a modelagem de casos de uso inerente ao FORTRAN. Tudo bem quando seu foco principal envolve os dados ou as formas em uma tela. É um ótimo modelo para programas como o PowerPoint, ou pelo menos para suas operações simples.

O que se perdeu são as responsabilidades do sistema . Nós não vendemos os elementos do DDD. E não sabemos bem os métodos de classe. Nós vendemos responsabilidades do sistema. Em algum nível, você precisa projetar seu sistema de acordo com o princípio de responsabilidade única.

Então, se você olhar para pessoas como Rebecca Wirfs-Brock, ou eu, que costumavam falar sobre métodos de classe, agora estamos falando em termos de casos de uso. É o que vendemos. Essas são as operações do sistema. Um caso de uso deve ter uma única responsabilidade. Um caso de uso raramente é uma unidade de arquitetura. Mas todo mundo estava tentando fingir que era. Testemunhe o pessoal da SOA, por exemplo.

É por isso que estou animado com a arquitetura DCI da Trygve Reenskaug - que é descrita no livro Lean Architecture acima. Finalmente, dá uma estatura real ao que costumava ser uma obediência arbitrária e mística à "responsabilidade única" - como se encontra na maioria das argumentações acima. Essa estatura está relacionada aos modelos mentais humanos: usuários finais primeiro e programadores depois. Relaciona-se a preocupações comerciais. E, quase por acaso, encapsula a mudança à medida que o fracasso nos desafia.

O princípio de responsabilidade única como o conhecemos é um dinossauro que sobrou de seus dias de origem ou um cavalo de passeio que usamos como substituto da compreensão. Você precisa deixar alguns desses cavalos de hobby para criar um ótimo software. E isso requer pensar fora da caixa. Manter as coisas simples e fáceis de entender funciona apenas quando o problema é simples e fácil de entender. Não estou muito interessado nessas soluções: elas não são típicas e não é aí que reside o desafio.

Lidar
fonte
2
Ao ler o que você escreveu, em algum lugar ao longo do caminho, perdi totalmente de vista o que você estava falando. Boas respostas não tratam a pergunta como o ponto de partida de uma caminhada pela floresta, mas como um tema definido ao qual vincular toda a escrita.
Donal Fellows
1
Ah, você é um desses, como um dos meus antigos gerentes. "Não queremos entender: queremos melhorá-lo!" A questão temática chave aqui é de princípio: esse é o "P" em "SRP". Talvez eu tivesse respondido diretamente à pergunta se fosse a pergunta certa: não era. Você pode aceitar isso com quem já fez a pergunta.
Copie
Há uma boa resposta enterrada aqui em algum lugar. Eu acho ...
RubberDuck
0

Sim, o princípio de responsabilidade única deve ser aplicado ao novo código.

Mas! O que é uma responsabilidade?

"Imprime um relatório como uma responsabilidade"? A resposta, acredito, é "Talvez".

Vamos tentar usar a definição de SRP como "tendo apenas um único motivo para mudar".

Suponha que você tenha uma função que imprima relatórios. Se você tiver duas alterações:

  1. altere essa função porque seu relatório precisa ter um fundo preto
  2. altere essa função porque você precisa imprimir em pdf

A primeira alteração é "alterar o estilo do relatório", a outra é "alterar o formato de saída do relatório" e agora você deve colocá-las em duas funções diferentes, porque essas são coisas diferentes.

Mas se sua segunda alteração tivesse sido:

2b. altere essa função porque seu relatório precisa de uma fonte diferente

Eu diria que as duas alterações são "alterar o estilo do relatório" e podem permanecer em uma função.

Então, onde isso nos deixa? Como sempre, você deve tentar manter as coisas simples e fáceis de entender. Se alterar a cor do plano de fundo significa 20 linhas de código e alterar a fonte significa 20 linhas de código, faça duas funções novamente. Se for uma linha cada, mantenha-a em uma.

Sarien
fonte
0

Ao projetar um novo sistema, é aconselhável considerar o tipo de mudanças que você poderá fazer durante a sua vida útil e quão caras essas serão dadas à arquitetura que você está implementando. Dividir seu sistema em módulos é uma decisão cara para cometer erros.

Uma boa fonte de informação é o modelo mental dos especialistas em domínio da empresa. Veja o exemplo do documento, a formatação e o pdf. Os especialistas em domínio provavelmente dirão que eles formatam suas cartas usando modelos de documentos. No estacionário ou no Word ou o que for. Você pode recuperar essas informações antes de começar a codificar e usá-las em seu design.

Uma ótima leitura sobre essas coisas: Lean Architecture de Coplien

flup
fonte
0

"Print" é muito parecido com "view" no MVC. Qualquer um que entenda o básico dos objetos entenderia isso.

É uma responsabilidade do sistema . É implementado como um mecanismo - MVC - que envolve uma impressora (a Vista), a coisa que está sendo impressa (o Módulo) e a solicitação e opções da impressora (do Controlador).

Tentar localizá-lo como uma responsabilidade de classe ou módulo é inoportuno e reflete o pensamento de 30 anos de idade. Aprendemos muito desde então, e isso é amplamente evidenciado na literatura e no código de programadores maduros.

Lidar
fonte
0

Não é uma idéia melhor realmente começar a aplicar o SRP apenas quando solicitações para alterar o código começarem a chegar?

Idealmente, você já terá uma boa idéia de quais são as responsabilidades das várias partes do código. Divida as responsabilidades de acordo com seus primeiros instintos, possivelmente levando em consideração o que as bibliotecas que você está usando desejam fazer (delegar uma tarefa, uma responsabilidade a uma biblioteca geralmente é uma ótima coisa a se fazer, desde que a biblioteca possa realmente executar a tarefa ) Em seguida, refine sua compreensão das responsabilidades de acordo com os requisitos variáveis. Quanto melhor você entender o sistema inicialmente, menos precisará alterar fundamentalmente as atribuições de responsabilidade (embora às vezes descubra que uma responsabilidade é melhor dividida em sub-responsabilidades).

Não que você deva gastar muito tempo se preocupando com isso. Um recurso importante do código é que ele pode ser alterado posteriormente, não sendo necessário corrigi-lo completamente da primeira vez. Apenas tente melhorar ao longo do tempo aprendendo que tipo de responsabilidades de forma têm para que você possa cometer menos erros no futuro.

Um exemplo seria um pedaço de código que imprime um documento. É solicitado um pedido para alterá-lo para imprimir em PDF e, em seguida, um segundo pedido é feito para alterá-lo para aplicar alguma formatação diferente ao documento. Nesse ponto, você tem provas de mais de um motivo para alterar (e violação do SRP) e deve fazer a refatoração apropriada.

Isso é estritamente uma indicação de que a responsabilidade geral - “imprimir” o código - tem sub-responsabilidades e deve ser dividida em partes. Isso não é uma violação do SRP por si só, mas uma indicação de que o particionamento (talvez em subtarefas de “formatação” e “renderização”) é provavelmente necessário. Você pode descrever claramente essas responsabilidades para entender o que está acontecendo nas subtarefas sem observar sua implementação? Se você puder, é provável que sejam divisões razoáveis.

Também pode ser mais claro se observarmos um exemplo real simples. Vamos considerar o sort()método utilitário em java.util.Arrays. O que isso faz? Ele classifica uma matriz e é tudo o que faz. Não imprime os elementos, não encontra o membro mais apto para a moral, não apita Dixie . Apenas classifica uma matriz. Você também não precisa saber como. A classificação é a única responsabilidade desse método. (De fato, existem muitos métodos de classificação em Java por razões técnicas bastante feias relacionadas aos tipos primitivos; você não precisa prestar atenção nisso, pois todos têm responsabilidades equivalentes.)

Faça seus métodos, suas aulas, seus módulos, faça com que eles tenham um papel tão claramente designado na vida. Ele mantém a quantidade que você precisa entender de uma vez para baixo e, por sua vez, é o que permite lidar com o design e a manutenção de um sistema grande.

Donal Fellows
fonte