Não importa o quão fortemente acoplado uma coisa seja à outra, se ela nunca mudar. Ao longo dos anos, achei geralmente mais produtivo concentrar-me em buscar menos razões para as coisas mudarem, buscar estabilidade do que torná-las mais fáceis de mudar, tentando obter a forma mais flexível possível de acoplamento.
Desacoplamento que achei muito útil, a ponto de às vezes favorecer duplicação de código modesta para desacoplar pacotes. Como exemplo básico, eu tive a opção de usar minha biblioteca de matemática para implementar uma biblioteca de imagens. Eu não fiz e apenas duplicei algumas funções matemáticas básicas que eram triviais para copiar.
Agora, minha biblioteca de imagens é completamente independente da biblioteca de matemática de uma maneira que, independentemente do tipo de alteração que eu faça na minha biblioteca de matemática, não afetará a biblioteca de imagens. Isso coloca a estabilidade em primeiro lugar. A biblioteca de imagens está mais estável agora, por ter drasticamente menos razões para mudar, uma vez que é dissociada de qualquer outra biblioteca que possa mudar (além da biblioteca padrão C, que, espero, nunca deve mudar). Como bônus, também é fácil de implantar quando é apenas uma biblioteca autônoma que não exige a coleta de várias outras bibliotecas para compilá-lo e usá-lo.
A estabilidade é muito útil para mim. Gosto de criar uma coleção de códigos bem testados, com cada vez menos razões para mudar no futuro. Isso não é um sonho; Eu tenho código C que tenho usado e usado novamente desde o final dos anos 80, que não mudou desde então. É admitidamente 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 de cada vez menos coisas, se não de nada externas. A confiabilidade aumenta e aumenta se o seu software depender cada vez mais de fundações estáveis que encontram poucas ou nenhuma razão para mudar. Menos partes móveis são realmente boas, mesmo que na prática as partes móveis sejam muito maiores em número do que as partes estáveis.
O acoplamento frouxo está na 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 e clientes de interface muito superiores que nunca mudam de idéia do que eu já trabalhei, mesmo as interfaces puras geralmente encontram razões para mudar de maneiras que ainda causam falhas em cascata no código. Essa idéia de que a estabilidade pode ser alcançada direcionando dependências para o abstrato e não para o concreto é útil apenas se o design da interface for mais fácil de acertar na primeira vez que a implementação. Costumo achar que é invertida onde um desenvolvedor pode ter criado uma implementação muito boa, se não maravilhosa, considerando os requisitos de design que eles deveriam cumprir, apenas para descobrir no futuro que os requisitos de design mudam completamente.
Por isso, gosto de favorecer a estabilidade e a dissociação completa, para poder dizer pelo menos com confiança: "Essa pequena biblioteca isolada usada há anos e protegida por testes completos quase não tem probabilidade de exigir mudanças, independentemente do que se passa no caótico mundo exterior. . " Isso me dá um pouco de sanidade, não importa que tipo de mudanças de design sejam necessárias para o exterior.
Acoplamento e estabilidade, exemplo ECS
Também adoro sistemas de componentes de entidades e eles introduzem muito acoplamento rígido, porque o sistema para dependências de componentes acessa e manipula dados brutos diretamente, da seguinte maneira:
Todas as dependências aqui são bastante restritas, pois os componentes apenas expõem dados brutos. As dependências não estão fluindo para abstrações, elas estão fluindo para dados brutos, o que significa que cada sistema tem a quantidade máxima 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 violando os dados brutos. No entanto, é muito fácil argumentar sobre um sistema como esse, pois é muito simples. Se uma textura sair esquisita, você saberá imediatamente com esse sistema que apenas o sistema de renderização e pintura acessa os componentes de textura e provavelmente pode descartar rapidamente o sistema de renderização, pois ele lê apenas texturas conceitualmente.
Enquanto isso, uma alternativa fracamente acoplada pode ser a seguinte:
... com todas as dependências fluindo para funções abstratas, não dados, e tudo nesse diagrama expondo uma interface pública e uma funcionalidade própria. Aqui todas as dependências podem estar muito soltas. Os objetos podem nem depender diretamente um do outro e interagir entre si por meio de interfaces puras. Ainda é muito difícil argumentar sobre esse sistema, especialmente se algo der errado, dado o complexo emaranhado de interações. Também haverá mais interações (mais acopladas, embora mais frouxas) do que o ECS, porque as entidades precisam conhecer os componentes que agregam, mesmo se souberem apenas da interface pública abstrata da outra.
Além disso, se houver alterações de design em qualquer coisa, você terá mais quebras em cascata do que o ECS, e normalmente haverá mais razões e tentações para alterações de design, pois tudo está tentando fornecer uma interface e abstração orientadas a objetos. Isso imediatamente vem com a idéia de que cada uma das pequenas coisas tentará impor restrições e limitações ao design, e essas restrições geralmente são o que justificam mudanças no design. A funcionalidade é muito mais restrita e precisa 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 os sistemas mais fracamente acoplados, com uma complexa teia de aranhas de dependências frouxas e, o mais importante para mim, encontro poucas razões. para que a versão do ECS precise alterar quaisquer componentes existentes, pois os componentes dos quais dependem não têm responsabilidade, exceto para fornecer os dados apropriados necessários para o funcionamento dos sistemas. Compare a dificuldade de projetar uma IMotion
interface pura e um objeto de movimento concreto implementando essa interface que fornece funcionalidade sofisticada ao tentar manter invariantes sobre dados privados versus um componente de movimento que só precisa fornecer dados brutos relevantes para resolver o problema e não se incomoda com funcionalidade.
A funcionalidade é muito mais difícil de acertar do que os dados, e é por isso que acho que é preferível direcionar o fluxo de dependências para os dados. Afinal, quantas bibliotecas de vetores / matrizes existem por aí? Quantos deles usam exatamente a mesma representação de dados e diferem apenas sutilmente na funcionalidade? Inúmeros, e ainda assim 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 nenhuma, e novamente mostrando que a funcionalidade é muito mais instável e propensa a alterações de design do que os dados em muitos cenários. É claro que, em algum momento, precisamos de funcionalidade, mas você pode projetar sistemas nos quais a maior parte das dependências flui para os dados, e não para abstrações ou funcionalidades em geral. Isso priorizaria a estabilidade acima do acoplamento.
As funções mais estáveis que já escrevi (do tipo que uso e reutilizo desde o final dos anos 80 sem precisar alterá-las) eram aquelas que dependiam de dados brutos, como uma função de geometria que apenas aceitava uma variedade de flutuadores e números inteiros, não aqueles que dependem de um Mesh
objeto ou IMesh
interface complexo , ou multiplicação de vetores / matrizes que apenas dependem float[]
ou double[]
não, dos que dependem FancyMatrixObjectWhichWillRequireDesignChangesNextYearAndDeprecateWhatWeUse
.