É possível aplicar DRY sem aumentar o acoplamento?

14

Suponha que temos um módulo de software A que implementa uma função F. Outro módulo B implementa a mesma função que F '.

Existem várias maneiras de se livrar do código duplicado:

  1. Deixe A usar F 'de B.
  2. Deixe B usar F de A.
  3. Coloque F em seu próprio módulo C e deixe que A e B o usem.

Todas essas opções geram dependências adicionais entre os módulos. Eles aplicam o princípio DRY ao custo de aumentar o acoplamento.

Tanto quanto posso ver, o acoplamento é sempre aumentado ou, quando alugado, passa para um nível mais alto ao aplicar o DRY. Parece haver um conflito entre dois dos princípios mais básicos do design de software.

(Na verdade, não acho surpreendente a existência de conflitos como esse. Provavelmente é isso que torna o bom design de software tão difícil. Acho surpreendente que esses conflitos normalmente não sejam abordados em textos introdutórios.)

Editar (para esclarecimento): Presumo que a igualdade de F e F 'não seja apenas uma coincidência. Se F precisar ser modificado, F 'provavelmente precisará ser modificado da mesma maneira.

Frank Puffer
fonte
2
... Acho que o DRY pode ser uma estratégia muito útil, mas essa pergunta ilustra uma ineficiência com o DRY. Alguns (por exemplo, entusiastas de OOP) podem argumentar que você deve copiar / colar F em B apenas para manter a autonomia conceitual de A e B, mas ainda não encontrei um cenário em que faria isso. Eu acho que o código de copiar / colar é a pior opção, não suporto a sensação de "perda de memória de curto prazo" onde eu tinha tanta certeza de que já escrevi um método / função para fazer alguma coisa; corrigir um bug em uma função e esquecer de atualizar outra pode ser outra questão importante.
Jrh 15/07
3
Existem muitos princípios de OO que se contradizem. Na maioria dos casos, você precisa encontrar uma compensação razoável. Mas IMHO o princípio DRY é o mais valioso. Como o @jrh escreveu: ter o mesmo comportamento implementado em vários locais é um pesadelo de manutenção que deve ser evitado a qualquer custo. Descobrir que você esqueceu de atualizar uma das cópias redundantes em produção pode prejudicar seus negócios.
Timothy Truckle
2
@ TimothyTruckle: Não devemos chamá-los de princípios OO porque eles se aplicam a outros paradigmas de programação. E sim, o DRY é valioso, mas também perigoso se for exagerado. Não apenas porque tende a criar dependências e, portanto, complexidade. Também é frequentemente aplicado a duplicatas causadas por coincidência, com diferentes razões para mudar.
31718 Frank Puffer
1
... também às vezes, quando me deparo com esse cenário, sou capaz de dividir F em partes que podem ser usadas para o que A precisa e o que B precisa.
Jrh 15/07
1
O acoplamento não é inerentemente uma coisa ruim e geralmente é necessário para diminuir erros e aumentar a produtividade. Se você usasse uma função parseInt da biblioteca padrão do seu idioma dentro da sua função, você estaria acoplando sua função à biblioteca padrão. Não vejo um programa que não faça isso há muitos anos. A chave é não criar acoplamentos desnecessários. Na maioria das vezes, uma interface é usada para evitar / remover esse acoplamento. Por exemplo, minha função pode aceitar uma implementação de parseInt como argumento. No entanto, isso nem sempre é necessário, nem sempre é sábio.
Joshua Jones

Respostas:

14

Todas essas opções geram dependências adicionais entre os módulos. Eles aplicam o princípio DRY ao custo de aumentar o acoplamento.

Por que sim eles fazem. Mas eles diminuem o acoplamento entre as linhas. O que você obtém é o poder de alterar o acoplamento. O acoplamento vem de várias formas. A extração de código aumenta a indireção e a abstração. Aumentar isso pode ser bom ou ruim. A primeira coisa que decide qual você recebe é o nome que você usa para isso. Se olhar para o nome me surpreende quando olho para dentro, então você não fez nenhum favor a ninguém.

Além disso, não siga DRY no vácuo. Se você eliminar a duplicação, estará assumindo a responsabilidade de prever que esses dois usos desse código mudarão juntos. Se é provável que eles mudem independentemente, você causou confusão e trabalho extra com pouco benefício. Mas um nome realmente bom pode tornar isso mais agradável. Se tudo o que você pode pensar é um nome ruim, por favor, pare agora.

O acoplamento sempre existirá, a menos que seu sistema seja tão isolado que ninguém nunca saiba se funciona. Portanto, refatorar o acoplamento é um jogo de escolha do seu veneno. A seguir, o DRY pode render, minimizando o acoplamento criado, expressando a mesma decisão de projeto repetidamente em muitos lugares, até que seja muito difícil mudar. Mas o DRY pode impossibilitar a compreensão do seu código. A melhor maneira de salvar essa situação é encontrar um nome realmente bom. Se você não consegue pensar em um bom nome, espero que você seja capaz de evitar nomes sem sentido.

candied_orange
fonte
Apenas para ter certeza de que entendi seu ponto de vista corretamente, deixe-me colocar um pouco diferente: se você atribuir um bom nome ao código extraído, o código extraído não será mais relevante para a compreensão do software, porque o nome já diz tudo (idealmente). O acoplamento ainda existe no nível técnico, mas não no nível cognitivo. Portanto, é relativamente inofensivo, certo?
Frank Puffer
1
Deixa pra lá
Basilevs
@FrankPuffer melhor?
Candied_orange 16/07/19
3

Existem maneiras de quebrar dependências explícitas. Um dos mais populares é injetar dependências em tempo de execução. Dessa forma, você obtém DRY e remove o acoplamento a custo de segurança estática. Hoje em dia, é tão popular que as pessoas nem sequer entendem que isso é uma troca. Por exemplo, os contêineres de aplicativos rotineiramente fornecem gerenciamento de dependência que complica imensamente o software, ocultando a complexidade. Mesmo a injeção simples de construtor antigo falha em garantir alguns contratos devido à falta do sistema de tipos.

Para responder ao título - sim, é possível, mas esteja preparado para as consequências do envio em tempo de execução.

  • Defina a interface F A em A, fornecendo a funcionalidade de F
  • Definir interface F B em B
  • Coloque F em C
  • Crie o módulo D para gerenciar todas as dependências (depende de A, B e C)
  • Adapte F a F A e F B em D
  • Injete (passe) os invólucros em A e B

Dessa forma, o único tipo de dependência que você teria é D, dependendo do outro módulo.

Ou registre C no contêiner do aplicativo com injeção de dependência integrada e aproveite a sorte de cabeamento automático que cresce lentamente loops e bloqueios de carregamento de classe em tempo de execução.

Basilevs
fonte
1
+1, mas com a ressalva explícita de que isso geralmente é uma troca ruim. Essa segurança estática existe para sua proteção e contorná-la com várias ações assustadoras à distância está apenas pedindo erros difíceis de rastrear mais adiante quando a complexidade do projeto cresce um pouco ...
Mason Wheeler
1
Pode-se dizer realmente que a DI quebra a dependência? Mesmo formalmente, você precisa de uma interface com a assinatura de F para implementá-la. Mas, ainda assim, se o módulo A estiver usando rwally F de C, ele depwnds nele, independentemente de C ser injetado em tempo de execução ou diretamente vinculado.DI não quebra a dependência, o bug apenas adia a falha se a dependência não for fornecida
max630
@ max630 substitui a dependência de implementação pela contratual, que é mais fraca.
Basilevs
@ max630 Você está correto. DI não pode ser dito para quebrar uma dependência. A DI é, de fato, um método de introdução de dependências e é realmente ortogonal à pergunta feita em relação ao acoplamento. Uma alteração em F (ou na interface que o encapsula) ainda exigiria uma alteração em A e B.
rei-lado-corrediça
1

Não tenho certeza de que uma resposta sem outro contexto faça sentido.

Será que Ajá dependem Bou vice-versa? - nesse caso, poderíamos ter uma escolha óbvia de casa F.

Não Ae Bjá compartilham quaisquer dependências comuns que podem ser um bom lar para F?

Quão grande / complexo é F? Do que mais Fdepende?

São módulos Ae Busados no mesmo projeto?

Will Ae Bacabam compartilhando alguns de qualquer maneira dependência comum?

Que sistema de linguagem / módulo está sendo usado: Quão doloroso é um novo módulo, na dor do programador, na sobrecarga de desempenho? Por exemplo, se você estiver escrevendo em C / C ++ com o sistema de módulo COM, o que causa problemas no código fonte, requer ferramentas alternativas, tem implicações na depuração e tem implicações no desempenho (para invocações entre módulos), talvez faça uma pausa séria.

Por outro lado, se você está falando sobre DLLs Java ou C # que combinam perfeitamente em um único ambiente de execução, isso é outra questão.


Uma função é uma abstração e suporta DRY.

No entanto, boas abstrações precisam ser concluídas - abstrações incompletas podem muito bem fazer o cliente consumidor (programador) compensar o déficit usando o conhecimento da implementação subjacente: isso resulta em um acoplamento mais rígido do que se a abstração fosse oferecida como mais completa.

Então, eu argumentaria procurar criar uma abstração melhor Ae com Ba qual depender do que simplesmente mover uma única função para um novo móduloC

Eu estaria procurando por um conjunto de funções para provocar uma nova abstração, ou seja, eu poderia esperar até que a base de código fosse adiante para identificar uma refatoração de abstração mais completa / mais completa a ser executada em vez de uma baseada em um único código de função informe.

Erik Eidt
fonte
2
Não é perigoso acoplar abstrações e gráfico de dependência?
Basilevs
Does A already depend on B or vice versa? — in which case we might have an obvious choice of home for F.Isso pressupõe que A sempre confiará em B (ou vice-versa), que é uma suposição muito perigosa. O fato de OP ver F como inerentemente não faz parte de A (ou B) sugere que F existe sem ser inerente a nenhuma biblioteca. Se F pertence a uma biblioteca (por exemplo, um método de extensão DbContext (F) e uma biblioteca de wrapper do Entity Framework (A ou B)), a pergunta do OP seria discutível.
Flater
0

As respostas aqui, focadas em todas as maneiras pelas quais você pode "minimizar" esse problema, estão fazendo um desserviço. E “soluções” simplesmente oferecendo maneiras diferentes de criar acoplamentos não são realmente soluções.

A verdade é que não estão conseguindo entender o mesmo problema que você criou. O problema do seu exemplo não tem nada a ver com DRY, e sim (mais amplamente) com o design do aplicativo.

Pergunte-se por que os módulos A e B são separados se ambos dependem da mesma função F? É claro que você terá problemas com o gerenciamento de dependências / abstração / acoplamento / nome-do-it se você se comprometer com um design inadequado.

A modelagem de aplicativo adequada é feita de acordo com o comportamento. Como tal, as partes de A e B que são dependentes de F precisam ser extraídas em seu próprio módulo independente. Se isso não for possível, A e B precisam ser combinados. Nos dois casos, A e B não são mais úteis para o sistema e devem deixar de existir.

O DRY é um princípio que pode ser usado para expor um design inadequado, não para causar isso. Se você não conseguir obter DRY ( quando realmente se aplica - observando sua edição) devido à estrutura do seu aplicativo, é um sinal claro de que a estrutura se tornou um passivo. É por isso que a refatoração contínua também é um princípio a seguir.

Do de outros diretores de design (sólida, seca, etc) estão lá fora, ABC todos usados para fazer a mudança (incluindo refactoring) uma aplicação mais indolor. Concentre-se nisso e todos os outros problemas começam a desaparecer.

deslize do lado do rei
fonte
Você sugere ter exatamente um módulo em cada aplicativo?
Basilevs
@ Basilevs Absolutamente não (a menos que seja justificado). Sugiro ter o maior número possível de módulos completamente dissociados. Afinal, esse é o objetivo de um módulo.
king-side-slide
Bem, a pergunta implica que os módulos A e B contenham funções não relacionadas e que já sejam extraídos de acordo, por que e como eles devem deixar de existir? Qual é a sua solução para o problema, conforme indicado?
Basilevs
@Basilevs A questão implica que A e B não foram modelados corretamente. É essa deficiência inerente que está causando o problema em primeiro lugar. Certamente, o simples fato de que eles fazem existir não é evidência de que eles devem existir. Esse é o argumento que estou afirmando acima. Claramente, é necessário um design alternativo para evitar a quebra do DRY. É importante entender que o objetivo de todos esses "princípios de design" é facilitar a alteração de um aplicativo.
king-side-slide
Muitos outros métodos, com dependências completamente diferentes, foram mal modelados e precisam ser refeitos, apenas por causa desse único método não acidental? Ou você supõe que os módulos A e B contêm um único método cada?
Basilevs
0

Todas essas opções geram dependências adicionais entre os módulos. Eles aplicam o princípio DRY ao custo de aumentar o acoplamento.

Eu tenho uma opinião diferente, pelo menos para a terceira opção:

Da sua descrição:

  • A precisa de F
  • B precisa de F
  • Nem A nem B precisam um do outro.

Colocar F em um módulo C não aumenta o acoplamento, pois A e B já precisam do recurso de C.

mouviciel
fonte