ti-enxame.com

O que devo considerar quando os princípios DRY e KISS são incompatíveis?

O princípio DRY às vezes força os programadores a escrever funções/classes complexas e difíceis de manter. Um código como esse tem uma tendência a se tornar mais complexo e mais difícil de manter ao longo do tempo. Violando o princípio KISS .

Por exemplo, quando várias funções precisam fazer algo semelhante. A solução usual DRY é escrever uma função que usa parâmetros diferentes para permitir pequenas variações no uso.

A vantagem é óbvia, DRY = um lugar para fazer alterações, etc.

A desvantagem e a razão pela qual ela está violando KISS é porque funções como essas tendem a se tornar cada vez mais complexas, com mais e mais parâmetros ao longo do tempo. No final, os programadores terão muito medo de fazer alterações nessas funções ou causarão erros em outros casos de uso da função.

Pessoalmente, acho que faz sentido violar DRY princípio para fazê-lo seguir KISS princípio.

Eu preferiria ter 10 funções super simples que são semelhantes a ter uma função super complexa.

Prefiro fazer algo tedioso, mas fácil (fazer a mesma alteração ou alteração semelhante em 10 lugares), do que fazer uma mudança muito assustadora/difícil em um só lugar.

Obviamente, a maneira ideal é fazê-lo como KISS = possível sem violar o DRY. Mas às vezes parece impossível.

Uma pergunta que surge é "com que frequência esse código muda?" o que implica que, se mudar com frequência, é mais relevante torná-lo SECO. Eu discordo, porque alterar essa função complexa DRY frequentemente fará com que ela cresça em complexidade e se torne ainda pior com o tempo.

Então, basicamente, acho que, em geral, KISS> DRY.

O que você acha? Em quais casos você acha que DRY deve sempre conquistar o KISS e vice-versa? Que coisas você considera ao tomar a decisão? Como você evita a situação?

71
user158443

O KISS é subjetivo. DRY é fácil de aplicar em excesso. Ambos têm boas idéias por trás deles, mas ambos são fáceis de abusar. A chave é o equilíbrio.

O KISS está realmente nos olhos de sua equipe. Você não sabe o que KISS é. Sua equipe sabe. Mostre seu trabalho a eles e veja se eles acham que é simples. Você é um péssimo juiz disso, porque já sabe como funciona. Descubra o quão difícil é o seu código para outras pessoas lerem.

DRY não é sobre a aparência do seu código. Você não pode detectar problemas reais DRY procurando por código idêntico. Um problema real DRY pode ser que você esteja resolvendo o mesmo problema com uma aparência completamente diferente Você não viola DRY quando usa código idêntico para resolver um problema diferente em um lugar diferente. Por quê? Porque problemas diferentes podem mudar de forma independente. Agora é preciso mudar e o outro não.

Tome decisões de design em um só lugar. Não espalhe uma decisão por aí. Mas não desista de todas as decisões que parecem iguais agora e no mesmo lugar. Não há problema em ter xey mesmo quando ambos estão definidos como 1.

Com essa perspectiva, nunca coloco KISS ou DRY sobre a outra. Não vejo quase a tensão entre elas. Esses dois princípios são importantes, mas também não são uma bala de prata.

144
candied_orange

Eu escrevi sobre isso já em m comentário a outra resposta por candied_orange a uma pergunta semelhante e também toquei um pouco nisso em um resposta diferente , mas vale a pena repetir:

DRY é um acrônimo bonito de três letras para o mnemônico "Não se repita", que foi cunhado no livro The Pragmatic Programmer , onde é um - seção inteira de 8,5 páginas . Também possui explicação e discussão em várias páginas no wiki .

A definição no livro é a seguinte:

Todo conhecimento deve ter uma representação única, inequívoca e autoritativa dentro de um sistema.

Observe que é enfaticamente não sobre a remoção da duplicação. Trata-se de identificação qual das duplicatas é a canônica. Por exemplo, se você tiver um cache, ele conterá valores duplicados de outra coisa. No entanto, deve ficar bem claro que o cache é não a fonte canônica.

O princípio é não as três letras SECA. São essas 20 páginas no livro e no wiki.

O princípio também está intimamente relacionado ao OAOO, que é um acrônimo de quatro letras não tão fofo para "Once And Only Once", que por sua vez é um princípio no eXtreme Programming que possui explicação e discussão em várias páginas) no wiki .

A página wiki do OAOO tem uma citação muito interessante de Ron Jeffries:

Uma vez vi Beck declarar dois patches de código quase completamente diferente como "duplicação", alterá-los para que fossem duplicação e, em seguida, remover a duplicação recém-inserida para criar algo obviamente melhor.

Sobre o qual ele elabora:

Lembro-me de ver uma vez Beck ver dois loops que eram bastante diferentes: eles tinham estruturas diferentes e conteúdos diferentes, o que praticamente não é duplicado, exceto a Palavra "for", e o fato de estarem repetindo - de maneira diferente - a mesma coisa. coleção.

Ele mudou o segundo loop para loop da mesma maneira que o primeiro. Isso exigia a alteração do corpo do loop para pular os itens no final da coleção, pois a versão anterior fazia apenas a frente da coleção. Agora, as declarações for eram as mesmas. "Bem, é preciso eliminar essa duplicação, ele disse, e moveu o segundo corpo para o primeiro loop e excluiu o segundo loop completamente.

Agora ele tinha dois tipos de processamento semelhante acontecendo no mesmo loop. Ele encontrou algum tipo de duplicação lá, extraiu um método, fez algumas outras coisas e pronto! o código estava muito melhor.

O primeiro passo - criar duplicação - foi surpreendente.

Isso mostra: você pode ter duplicação sem código duplicado!

E o livro mostra o outro lado da moeda:

Como parte do seu aplicativo de pedido de vinho on-line, você está capturando e validando a idade do usuário, juntamente com a quantidade que ele está solicitando. De acordo com o proprietário do site, ambos devem ser números e maiores que zero. Então você codifica as validações:

def validate_age(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

def validate_quantity(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

Durante a revisão do código, o know-how residente rejeita esse código, alegando que é uma violação DRY: ambos os corpos de função são os mesmos.

Eles estão errados. O código é o mesmo, mas o conhecimento que eles representam é diferente. As duas funções validam duas coisas separadas que simplesmente têm as mesmas regras. Isso é uma coincidência, não uma duplicação.

Este é um código duplicado que não é duplicação de conhecimento.

Há uma grande anedota sobre duplicação que leva a uma profunda compreensão da natureza das linguagens de programação: muitos programadores conhecem a linguagem de programação Scheme e que ela é uma linguagem processual na família LISP com primeiro procedimentos de classe e ordem superior, escopo lexical, fechamentos lexicais e foco em estruturas de dados e códigos puramente funcionais e referencialmente transparentes. Entretanto, o que poucas pessoas sabem é que ele foi criado para estudar a Programação Orientada a Objetos e os Sistemas de Atores (que os autores do Scheme consideravam estar intimamente relacionados, se não a mesma coisa).

Dois dos procedimentos fundamentais no Esquema são lambda, que cria um procedimento, e apply, que executa um procedimento. Os criadores do Scheme adicionaram mais dois: alpha, que cria um a controlador (ou objeto) e send , que envia uma mensagem para um ator (ou objeto).

Uma conseqüência irritante de ter ambos apply e send foi que a sintaxe elegante das chamadas de procedimento não funcionou mais. No esquema como o conhecemos hoje (e em praticamente qualquer LISP), uma lista simples é geralmente interpretada como "interprete o primeiro elemento da lista como um procedimento e apply para o restante da lista, interpretado" como argumentos ". Então você pode escrever

(+ 2 3)

e isso é equivalente a

(apply '+ '(2 3))

(Ou algo próximo, meu esquema está bastante enferrujado.)

No entanto, isso não funciona mais, pois você não sabe se apply ou send (assumindo que não deseja priorizar um dos dois que os criadores do Scheme não fizeram) eles queriam que os dois paradigmas fossem iguais). … Ou você? Os criadores do Scheme perceberam que, na verdade, eles simplesmente precisam verificar o tipo de objeto referenciado pelo símbolo: se + é um procedimento, você apply, se + é um ator, você send uma mensagem para ele. Na verdade, você não precisa separar apply e send, pode ter algo como apply-or-send.

E foi o que eles fizeram: eles pegaram o código dos dois procedimentos apply e send e os colocaram no mesmo procedimento, como dois ramos de uma condicional.

Logo depois, eles também reescreveram o intérprete do Scheme, que até aquele momento foi escrito em uma linguagem Assembly de transferência de registro de nível muito baixo para uma máquina de registro, no Scheme de alto nível. E eles notaram algo surpreendente: o código nas duas ramificações do condicional tornou-se idêntico. Eles não haviam notado isso antes: os dois procedimentos foram escritos em momentos diferentes (eles começaram com um "LISP mínimo" e depois adicionaram OO)), além da verbosidade e do baixo nível da Assembléia significava que eles foram realmente escritos de maneira bastante diferente, mas depois de reescrevê-los em um idioma de alto nível, ficou claro que eles fizeram a mesma coisa.

Isso levou a um profundo entendimento de Atores e OO: executando um programa orientado a objetos e executando um programa em uma linguagem procedural com fechamentos lexicais e chamadas de cauda apropriadas, são a mesma coisa. A única diferença é se as primitivas da sua linguagem são objetos/atores ou procedimentos. Mas operacionalmente, é o mesmo.

Isso também leva a outra realização importante que, infelizmente, ainda não é bem compreendida até hoje: você não pode manter a abstração orientada a objetos sem chamadas de cauda apropriadas ou colocar de forma mais agressiva: uma linguagem que afirma ser orientada a objetos, mas não possui chamadas de cauda apropriadas , não é orientado a objetos. (Infelizmente, isso se aplica a todos os meus idiomas favoritos e não é acadêmico: eu tenho me deparei com esse problema, que eu tive que quebrar o encapsulamento para evitar um estouro de pilha.)

Este é um exemplo em que a duplicação muito bem escondida realmente obscurecida um conhecimento importante e descobrindo essa duplicação também revelou conhecimento.

39
Jörg W Mittag

Em caso de dúvida, sempre escolha a solução mais simples possível que resolva o problema.

Se a solução simples for muito simples, ela poderá ser facilmente alterada. Uma solução excessivamente complexa, por outro lado, também é mais difícil e arriscada de mudar.

O KISS é realmente o mais importante de todos os princípios de design, mas geralmente é esquecido, porque nossa cultura de desenvolvedor valoriza muito a inteligência e o uso de técnicas sofisticadas. Mas às vezes um if é realmente melhor que um padrão de estratégia .

O princípio DRY às vezes força os programadores a escrever funções/classes complexas e difíceis de manter.

Pare aí mesmo! O objetivo do princípio DRY é obter um código mais sustentável). Se a aplicação do princípio em um caso específico levar a para menos código sustentável, o princípio não deve ser aplicado.

Lembre-se de que nenhum desses princípios é objetivo em si. O objetivo é criar um software que atenda a sua finalidade e que possa ser modificado adaptado e ampliado quando necessário. KISS, DRY, SOLID e todos os outros princípios são meios para atingir esse objetivo. Mas todos têm suas limitações e podem ser aplicados de maneira que trabalhem em oposição ao objetivo final, que é escrever software funcional e de manutenção.

8
JacquesB

IMHO: se você parar de se concentrar no código que é KISS/DRY e começar a se concentrar nos requisitos de condução do código, encontrará a melhor resposta que está procurando.

Acredito:

  1. Precisamos incentivar um ao outro a permanecer pragmático (como você está fazendo)

  2. Nunca devemos parar de promover a importância dos testes

  3. Focar mais nos requisitos resolverá suas perguntas.

TLDR

Se o seu requisito é que as peças sejam alteradas de forma independente, mantenha as funções independentes por não ter funções auxiliares. Se seus requisitos (e quaisquer alterações futuras) forem os mesmos para todas as funções, mova essa lógica para uma função auxiliar.

Acho que todas as nossas respostas até agora formam um diagrama de Venn: todos dizemos a mesma coisa, mas damos detalhes a diferentes partes.

Além disso, ninguém mais mencionou testes, razão pela qual parcialmente escrevi esta resposta. Eu acho que se alguém menciona que os programadores têm medo de fazer alterações, é muito imprudente não falar sobre testes! Mesmo se "pensarmos" que o problema é sobre o código, pode ser que o problema real seja a falta de teste. Decisões objetivamente superiores se tornam mais realistas quando as pessoas investem primeiro em testes automatizados.

Primeiro, evitar o medo é sabedoria - Bom trabalho!

Aqui está uma frase que você disse: os programadores terão muito medo de fazer alterações nessas funções [auxiliares] ou causarão erros em outros casos de uso da função

Concordo que esse medo é o inimigo, e você nunca se apega a princípios se eles estão apenas causando medo de erros/trabalho/alterações em cascata. Se copiar/colar entre várias funções é a única maneira de remover esse medo (que eu não acredito que seja - veja abaixo), então é o que você deve fazer.

O fato de você sentir esse medo de fazer alterações e de tentar fazer algo a respeito faz dele um profissional melhor do que muitos outros que não se importam o suficiente com a melhoria do código - eles apenas fazem o que lhes dizem e faça as alterações mínimas necessárias para fechar o ticket.

Também (e posso dizer que estou repetindo o que você já sabe): habilidades de pessoas superam habilidades de design. Se as pessoas da vida real da sua empresa são totalmente ruins, não importa se a sua "teoria" é melhor. Você pode ter que tomar decisões objetivamente piores, mas sabe que as pessoas que a manterão são capazes de entender e trabalhar. Além disso, muitos de nós também entendem a gerência que (OMI) nos administra de maneira minuciosa e encontra maneiras de sempre negar a refatoração necessária.

Como alguém que é um fornecedor que escreve código para clientes, tenho que pensar nisso o tempo todo. Talvez eu queira usar currying e metaprogramação porque há um argumento de que é objetivamente melhor, mas na vida real, vejo pessoas sendo confundidas com esse código porque não é visualmente óbvio o que está acontecendo.

Segundo, melhores testes solucionam vários problemas ao mesmo tempo

Se (e somente se) você tiver testes automatizados eficazes, estáveis ​​e comprovados pelo tempo (unidade e/ou integração), aposto que você verá o medo desaparecer. Para iniciantes em testes automatizados, pode parecer muito assustador confiar nos testes automatizados; os recém-chegados podem ver todos esses pontos verdes e ter muito pouca confiança de que esses pontos verdes refletem o funcionamento da produção na vida real. No entanto, se você, pessoalmente, confia nos testes automatizados, pode começar a encorajar emocionalmente/relacionalmente os outros a confiarem nele também.

Para você, (se você ainda não o fez), o primeiro passo é pesquisar práticas de teste, se você não tiver. Sinceramente, suponho que você já saiba essas coisas, mas como não vi isso mencionado no seu post original, tenho que falar sobre isso. Como os testes automatizados são tão importantes e relevantes para a sua situação que você colocou.

Não vou tentar resumir sozinho todas as práticas de teste em um único post aqui, mas gostaria de desafiá-lo a se concentrar na idéia de testes "à prova de refatoração". Antes de enviar um teste de unidade/integração ao código, pergunte-se se há alguma maneira válida de refatorar o CUT (código sob teste) que interromperia o teste que você acabou de escrever. Se isso for verdade, então (IMO) exclua esse teste. É melhor ter menos testes automatizados que não quebram desnecessariamente quando você refatorar, do que ter uma coisa a dizer que você tem alta cobertura de teste (qualidade sobre quantidade). Afinal, tornar a refatoração mais fácil é (IMO) o principal objetivo dos testes automatizados.

Como adotei essa filosofia de "prova de refatoração" ao longo do tempo, cheguei às seguintes conclusões:

  1. Testes de integração automatizados são melhores que testes de unidade
  2. Para testes de integração, se necessário, escreva "simuladores/falsificações" com "testes de contrato"
  3. Nunca teste uma API privada - sejam métodos de classe privada ou funções não exportadas de um arquivo.

Referências:

Enquanto você estiver pesquisando práticas de teste, talvez seja necessário dedicar mais tempo para escrever esses testes. Às vezes, a única melhor abordagem é não contar a ninguém que você está fazendo isso, porque eles o microgerenciarão. Obviamente isso nem sempre é possível porque a quantidade de necessidade de teste pode ser maior do que a necessidade de um bom equilíbrio entre trabalho e vida pessoal. Mas, às vezes, existem coisas pequenas o suficiente para que você possa adiar secretamente uma tarefa por um dia ou dois para escrever apenas os testes/códigos necessários. Sei que isso pode ser uma afirmação controversa, mas acho que é realidade.

Além disso, você obviamente pode ser o mais politicamente prudente possível para ajudar a encorajar outras pessoas a tomar medidas para entender/escrever os próprios testes. Ou talvez você seja o líder técnico que pode impor uma nova regra para revisões de código.

Ao conversar sobre o teste com seus colegas, espero que o ponto 1 acima (seja pragmático) nos lembre de que devemos continuar ouvindo primeiro e não ser agressivos.

Terceiro, concentre-se nos requisitos, não no código

Muitas vezes nos concentramos em nosso código, e não entendemos profundamente a imagem maior que nosso código deveria resolver! Às vezes, é necessário parar de discutir se o código está limpo e começar a garantir que você tenha um bom entendimento dos requisitos que deveriam estar dirigindo o código.

É mais importante que você faça a coisa certa do que sinta que seu código é "bonito" de acordo com idéias como KISS/DRY. É por isso que hesito em me preocupar com essas frases, porque (na prática) acidentalmente fazem você se concentrar no seu código sem pensar no fato de que os requisitos é o que fornece um bom julgamento da boa qualidade do código.


Se os requisitos de duas funções forem interdependentes/iguais, coloque a lógica de implementação desse requisito em uma função auxiliar. As entradas para essa função auxiliar serão as entradas para a lógica de negócios desse requisito.

Se os requisitos das funções forem diferentes, copie/cole entre eles. Se os dois tiverem o mesmo código neste momento, mas puder mudar corretamente de forma independente, então uma função auxiliar será ruim porque está afetando outra função cujo requisito é alterar independentemente.

Exemplo 1: você tem uma função chamada "getReportForCustomerX" e "getReportForCustomerY" e os dois consultam o banco de dados da mesma maneira. Vamos também fingir que há um requisito comercial em que cada cliente pode personalizar seu relatório literalmente da maneira que desejar. Nesse caso, por design , os clientes desejam números diferentes em seus relatórios. Portanto, se você tiver um novo cliente Z que precise de um relatório, talvez seja melhor copiar/colar a consulta de outro cliente e, em seguida, confirmar o código e mover um. Mesmo que as consultas sejam exatamente iguais, o ponto de definição dessas funções é separar alterações de um cliente afetando outro. Nos casos em que você fornece um novo recurso que todos os clientes desejam em seus relatórios, sim: você possivelmente estará digitando as mesmas alterações entre todas as funções.

No entanto, digamos que decidimos seguir em frente e criar uma função auxiliar chamada queryData. O motivo é ruim, porque haverá mais alterações em cascata ao introduzir uma função auxiliar. Se houver uma cláusula "where" na sua consulta que seja igual para todos os clientes, assim que um cliente desejar que um campo seja diferente para eles, em vez de 1) alterar a consulta na função X, você deverá: 1 ) altere a consulta para fazer o que o cliente X deseja 2) adicione condicionais à consulta para não fazer isso para os outros. Adicionar mais condicionais a uma consulta é logicamente diferente. Talvez eu saiba como adicionar uma sub-cláusula a uma consulta, mas isso não significa que sei como condicionar essa sub-cláusula sem afetar o desempenho daqueles que não a usam.

Então, você percebe que o uso de uma função auxiliar requer duas alterações em vez de uma. Eu sei que este é um exemplo artificial, mas a complexidade booleana de manter cresce mais do que linearmente, na minha experiência. Portanto, o ato de adicionar condicionais conta como "mais uma coisa" com a qual as pessoas precisam se preocupar e "mais uma coisa" para atualizar a cada vez.

Parece-me que este exemplo pode ser como a situação em que você está se deparando. Algumas pessoas se encolhem emocionalmente com a idéia de copiar/colar entre essas funções, e essa reação emocional é boa. Mas o princípio de "minimizar alterações em cascata" discernirá objetivamente as exceções para quando copiar/colar está OK.

Exemplo 2: você tem três clientes diferentes, mas a única coisa que permite diferenciar entre os relatórios deles são os títulos das colunas. Observe que essa situação é muito diferente. Nosso requisito comercial não é mais "agregar valor ao cliente, permitindo flexibilidade no relatório". Em vez disso, o requisito comercial é "evitar excesso de trabalho, não permitindo que os clientes personalizem muito o relatório". Nessa situação, a única vez em que você alteraria a lógica da consulta é quando também precisará garantir que todos os outros clientes recebam a mesma alteração. Nesse caso, você definitivamente deseja criar uma função auxiliar com uma matriz como entrada - quais são os "títulos" para as colunas.

No futuro, se os proprietários do produto decidirem que desejam permitir que os clientes personalizem algo sobre a consulta, você adicionará mais sinalizadores à função auxiliar.

Conclusão

Quanto mais você se concentrar nos requisitos em vez do código, mais o código será isomórfico aos requisitos literais. Você naturalmente escreve um código melhor.

4
Alexander Bird

Tente encontrar um meio termo razoável. Em vez de uma função com muitos parâmetros e condicionais complexos espalhados por ela, divida-a em algumas funções mais simples. Haverá alguma repetição nos chamadores, mas não tanto quanto se você não tivesse movido o código comum para funções em primeiro lugar.

Recentemente, me deparei com isso com algum código no qual estou trabalhando para fazer interface com as lojas de aplicativos do Google e do iTunes. Grande parte do fluxo geral é a mesma, mas há diferenças suficientes que eu não poderia escrever facilmente uma função para encapsular tudo.

Portanto, o código está estruturado da seguinte forma:

Google::validate_receipt(...)
    f1(...)
    f2(...)
    some google-specific code
    f3(...)

iTunes::validate_receipt(...)
    some iTunes-specific code
    f1(...)
    f2(...)
    more iTunes-specific code
    f3(...)

Não estou preocupado com o fato de que chamar f1 () e f2 () em ambas as funções de validação viole o princípio DRY, porque combiná-los tornaria mais complicado e não executaria um único e bem definido tarefa.

3
Barmar

Kent Beck adotou 4 regras de design simples, relacionadas a essa questão. Conforme expresso por Martin Fowler, eles são:

  • Passa nos testes
  • Revela a intenção
  • Sem duplicação
  • Menos elementos

Há muita discussão sobre a ordem dos dois do meio, por isso pode valer a pena pensar neles como igualmente importante.

DRY é o terceiro elemento da lista e KISS pode ser considerado uma combinação do 2º e 4º, ou mesmo da lista inteira).

Esta lista fornece uma visão alternativa à dicotomia DRY/KISS. Seu código DRY revela a intenção? Seu código KISS?? Você pode tornar a versão ether mais reveladora ou menos duplicada?

O objetivo não é DRY ou KISS, é um bom código. DRY, KISS, e essas regras são meras ferramentas para chegar lá).

3
Blaise Pascal