ti-enxame.com

O acoplamento fraco sem casos de uso é um antipadrão?

O acoplamento fraco é, para alguns desenvolvedores, o Santo Graal de um software bem projetado. Certamente é uma coisa boa quando torna o código mais flexível em face de mudanças que provavelmente ocorrerão em um futuro próximo ou evita a duplicação de código.

Por outro lado, os esforços para acoplar livremente os componentes aumentam a quantidade de indireção em um programa, aumentando assim sua complexidade, muitas vezes tornando-o mais difícil de entender e muitas vezes tornando-o menos eficiente.

Você considera um foco no acoplamento flexível, sem nenhum caso de uso para o acoplamento flexível (como evitar a duplicação de código ou planejar mudanças que provavelmente ocorrerão em um futuro próximo) como um antipadrão? O acoplamento solto pode cair sob a égide da YAGNI?

22
dsimcha

m pouco.

Às vezes, o acoplamento fraco que ocorre sem muito esforço é bom, mesmo se você não tiver requisitos específicos que exijam que algum módulo seja desacoplado. A fruta mais próxima, por assim dizer.

Por outro lado, a superengenharia para quantidades ridículas de mudança exigirá muita complexidade e esforço desnecessários. YAGNI, como você diz, acerta na cabeça.

13
Fishtoaster

É prática de programaçãoX bom ou ruim? Claramente, a resposta é sempre "depende."

Se você está olhando para o seu código, querendo saber quais "padrões" você pode injetar, então você está fazendo isso errado.

Se você está construindo seu software de forma que objetos não relacionados não mexam uns com os outros, então você está fazendo certo.

Se você está "projetando" sua solução para que possa ser infinitamente ampliada e alterada, na verdade a está tornando mais complicada.

Eu acho que no final do dia, você fica com uma única verdade: é mais ou menos complicado ter os objetos desacoplados? Se for menos complicado acoplá-los, essa é a solução correta. Se for menos complicado separá-los, essa é a solução certa.

(No momento, estou trabalhando em uma base de código bastante pequena que faz um trabalho simples de uma forma muito complicada, e parte do que o torna tão complicado é a falta de compreensão dos termos "acoplamento" e "coesão" por parte do original desenvolvedores.)

22
dash-tom-bang

Acho que você está chegando aqui ao conceito de coesão. Este código tem um bom propósito? Posso internalizar esse propósito e entender o "quadro geral" do que está acontecendo?

Isso pode levar a um código difícil de seguir, não apenas porque há muito mais arquivos de origem (assumindo que sejam classes separadas), mas porque nenhuma classe parece ter um propósito.

De uma perspectiva ágil, posso sugerir que esse acoplamento fraco seria um antipadrão. Sem a coesão, ou mesmo casos de uso para ela, você não pode escrever testes de unidade sensatos e não pode verificar o propósito do código. Agora, o código ágil pode levar a um acoplamento fraco, por exemplo, quando o desenvolvimento orientado a teste é usado. Mas se os testes certos foram criados, na ordem certa, então é provável que haja boa coesão e acoplamento fraco. E você está falando apenas sobre os casos em que claramente não existe coesão.

Novamente, da perspectiva ágil, você não quer esse nível artificial de indireção porque é um esforço desperdiçado em algo que provavelmente não será necessário de qualquer maneira. É muito mais fácil refatorar quando a necessidade é real.

No geral, você deseja alto acoplamento dentro de seus módulos e um acoplamento fraco entre eles. Sem o acoplamento alto, você provavelmente não tem coesão.

3
Macneil

A resposta simples é que o acoplamento fraco é bom quando feito corretamente.

Se o princípio de uma função, um propósito for seguido, então deve ser fácil seguir o que está acontecendo. Além disso, o código de pares soltos segue como algo natural, sem nenhum esforço.

Regras de design simples: 1. não construa conhecimento de vários itens em um único ponto (conforme apontado em todos os lugares, depende), a menos que você esteja construindo uma interface de fachada. 2. uma função - um propósito (esse propósito pode ser multifacetado, como em uma fachada) 3. um módulo - um conjunto claro de funções inter-relacionadas - um propósito claro 4. se você não pode simplesmente fazer um teste de unidade, então ele não tem um propósito simples

Todos esses comentários sobre como ser mais fácil refatorar depois são um monte de códigos. Uma vez que o conhecimento é construído em muitos lugares, particularmente em sistemas distribuídos, o custo de refatoração, a sincronização de rollout e quase todos os outros custos o deixam tão fora de consideração que, na maioria dos casos, o sistema acaba sendo destruído por causa disso.

O triste sobre o desenvolvimento de software hoje em dia é que 90% das pessoas desenvolvem novos sistemas e não têm capacidade de compreender sistemas antigos e nunca estão por perto quando o sistema atingiu um estado de saúde tão ruim devido à refatoração contínua de bits e peças.

1
sweetfa

Para a maioria das perguntas como essa, a resposta é "depende". No geral, se eu puder fazer um projeto logicamente fracamente acoplado de maneiras que façam sentido, sem uma grande sobrecarga para fazer isso, eu o farei. Evitar acoplamentos desnecessários no código é, na minha opinião, um objetivo de design totalmente válido.

Quando chegar a uma situação em que parece que os componentes devem logicamente ser fortemente acoplados, procurarei um argumento convincente antes de começar a separá-los.

Acho que o princípio pelo qual trabalho com a maioria desses tipos de prática é o da inércia. Tenho uma ideia de como gostaria que meu código funcionasse e se posso fazer dessa forma sem tornar a vida mais difícil, então farei. Se fazer isso tornará o desenvolvimento mais difícil, mas a manutenção e o trabalho futuro mais fáceis, tentarei adivinhar se será mais trabalhoso durante a vida útil do código e usarei isso como meu guia. Caso contrário, precisaria ser um ponto de design deliberado para valer a pena prosseguir.

1
glenatron

Não importa o quanto uma coisa esteja fortemente ligada a outra, se essa outra coisa nunca muda. Descobri que geralmente é mais produtivo com o passar dos anos focar em buscar menos razões para as coisas mudarem, em buscar estabilidade, do que torná-las mais fáceis de mudar tentando alcançar a forma mais frouxa de acoplamento possível. Decoupling Descobri ser muito útil, a ponto de às vezes ser a favor da duplicação de código modesta para separar pacotes. Como exemplo básico, tive a opção de usar minha biblioteca de matemática para implementar uma biblioteca de imagens. Não o fiz e apenas dupliquei algumas funções matemáticas básicas que eram triviais de copiar.

Agora minha biblioteca de imagens é completamente independente da biblioteca de matemática de uma forma que não importa que tipo de alterações eu faça em minha biblioteca de matemática, ela não afetará a biblioteca de imagens. Isso é colocar a estabilidade em primeiro lugar. A biblioteca de imagens é mais estável agora, por ter drasticamente menos razões para mudar, uma vez que é desacoplada de qualquer outra biblioteca que poderia mudar (além da biblioteca padrão C que esperançosamente nunca deveria mudar). Como um bônus, também é fácil de implantar quando é apenas uma biblioteca autônoma que não requer puxar um monte de outras bibliotecas para construí-la e usá-la.

A estabilidade é muito útil para mim. Gosto de construir uma coleção de códigos bem testados que têm cada vez menos motivos para mudar no futuro. Isso não é uma quimera; Eu tenho código C que tenho usado e usado novamente desde o final dos anos 80, que não mudou nada desde então. É reconhecidamente coisas de baixo nível, como código orientado a pixels e relacionado à geometria, enquanto muitas das minhas coisas de nível superior se tornaram obsoletas, mas é algo que ainda ajuda muito a ter por perto. Isso quase sempre significa uma biblioteca que depende cada vez menos de coisas, senão de algo externo. A confiabilidade aumenta cada vez mais se o seu software depende cada vez mais de bases estáveis ​​que encontram poucas ou nenhuma razão para mudar. Menos peças móveis é realmente bom, mesmo se na prática as peças móveis forem muito maiores em número do que as peças estáveis. Ainda ajuda minha sanidade mental saber que a totalidade de uma base de código não consiste em partes móveis.

O acoplamento frouxo segue a mesma linha, mas acho frequentemente que o acoplamento frouxo é muito menos estável do que nenhum acoplamento. A menos que você esteja trabalhando em uma equipe com designers de interface muito superiores e clientes que não mudam de ideia do que eu já trabalhei, até mesmo interfaces puras frequentemente encontram motivos para mudar de maneiras que ainda causam quebras em cascata em todo o código. Essa ideia de que a estabilidade pode ser alcançada direcionando dependências para o abstrato ao invés do concreto só é útil se o design da interface for mais fácil de acertar na primeira vez do que na implementação. Freqüentemente, acho que é invertido, quando um desenvolvedor pode ter criado uma implementação muito boa, se não maravilhosa, dados os requisitos de design que eles achavam que deveriam atender, apenas para descobrir no futuro que os requisitos de design mudariam completamente. O design é muito mais difícil de acertar do que a implementação se você me perguntar com bases de código em grande escala tentando cumprir os requisitos em constante mudança e, nesse caso, separar designs abstratos de implementações concretas não ajuda muito se o design abstrato é a coisa mais importante propenso a ser alterado.

Então, eu gosto de favorecer a estabilidade e o desacoplamento completo para que eu possa pelo menos dizer com segurança: "Esta pequena biblioteca isolada que foi usada por anos e protegida por testes completos quase não tem probabilidade de exigir mudanças, não importa o que aconteça no caótico mundo exterior . " Isso me dá um pouco de sanidade, não importa que tipo de mudanças de design sejam exigidas externamente.

Acoplamento e estabilidade, exemplo ECS

Eu também adoro sistemas de componente de entidade e eles introduzem um acoplamento forte porque o sistema para todas as dependências de componentes acessam e manipulam dados brutos diretamente, assim:

enter image description here

Todas as dependências aqui são bastante restritas, já que os componentes apenas expõem dados brutos. As dependências não estão fluindo para abstrações, estão fluindo para dados brutos o que significa que cada sistema tem o máximo de conhecimento possível sobre cada tipo de componente que eles solicitam para acessar. Os componentes não têm funcionalidade com todos os sistemas acessando e alterando os dados brutos. No entanto, é muito fácil raciocinar sobre um sistema como esse, pois é muito plano. Se uma textura ficar maluca, então você sabe com este sistema imediatamente que apenas o sistema de renderização e pintura acessa os componentes de textura, e você provavelmente pode descartar rapidamente o sistema de renderização, já que ele só lê texturas conceitualmente.

Enquanto isso, uma alternativa fracamente acoplada pode ser esta:

enter image description here

... com todas as dependências fluindo para funções abstratas, não dados, e cada coisa naquele diagrama expondo uma interface pública e funcionalidade própria. Aqui, todas as dependências podem ser muito soltas. Os objetos podem nem mesmo depender diretamente uns dos outros e interagir uns com os outros por meio de interfaces puras. Ainda assim, é muito difícil raciocinar sobre esse sistema, especialmente se algo der errado, devido ao complexo emaranhado de interações. Haverá também mais interações (mais acoplamento, embora mais solto) do que o ECS, porque as entidades precisam saber sobre os componentes que agregam, mesmo que apenas saibam sobre a interface pública abstrata uma da outra.

Além disso, se houver alterações de design em qualquer coisa, você obterá mais interrupções em cascata do que o ECS e, normalmente, haverá mais motivos e tentações para alterações de design, uma vez que cada coisa está tentando fornecer uma interface e abstração agradável orientada a objetos. Isso imediatamente vem com a ideia de que cada pequena coisa tentará impor restrições e limitações ao design, e essas restrições geralmente são o que justifica mudanças no design. A funcionalidade é muito mais restrita e tem que fazer muito mais suposições de design do que dados brutos.

Na prática, descobri que o tipo de sistema ECS "plano" acima é muito mais fácil de raciocinar do que até mesmo os sistemas mais fracamente acoplados com uma complexa teia de aranha de dependências soltas e, o mais importante para mim, encontro tão poucos motivos para que a versão do ECS precise alterar qualquer componente existente, uma vez que os componentes dos quais depende não têm responsabilidade, exceto fornecer os dados apropriados necessários para o funcionamento dos sistemas. Compare a dificuldade de projetar uma interface IMotion pura e um objeto de movimento concreto implementando essa interface que fornece funcionalidade sofisticada enquanto tenta manter invariáveis ​​sobre dados privados vs. um componente de movimento que só precisa fornecer dados brutos relevantes para resolver o problema e não se preocupa com a funcionalidade.

A funcionalidade é muito mais difícil de acertar do que os dados, por isso acho que geralmente é preferível direcionar o fluxo de dependências para os dados. Afinal, quantas bibliotecas vetoriais/matriciais existem? Quantos deles usam exatamente a mesma representação de dados e apenas diferem sutilmente na funcionalidade? Incontáveis, mas ainda temos muitos, apesar de representações de dados idênticas, porque queremos diferenças sutis na funcionalidade. Quantas bibliotecas de imagens existem? Quantos deles representam pixels de uma maneira diferente e única? Quase nenhum, e novamente mostrando que a funcionalidade é muito mais instável e propensa a alterações de design do que dados em muitos cenários. Claro que em algum ponto precisamos de funcionalidade, mas você pode projetar sistemas onde a maior parte das dependências flua para os dados, e não para as abstrações ou funcionalidades em geral. Isso seria priorizar a estabilidade acima do acoplamento.

As funções mais estáveis ​​que já escrevi (o tipo que tenho usado e reutilizado desde o final dos anos 80 sem ter que alterá-las) eram todas aquelas que dependiam de dados brutos, como uma função de geometria que apenas aceitava um array de floats e inteiros, não aqueles que dependem de um objeto Mesh complexo ou interface iMesh, ou multiplicação de vetor/matriz que dependia apenas de float[] ou double[], não aquele que dependia de FancyMatrixObjectWhichWillRequireDesignChangesNextYearAndDeprecateWhatWeUse.

0
user204677