Se você estiver usando OOP em um idioma moderno, o maior perigo normalmente não é " código de espaguete ", mas " código de ravioli "". Você pode acabar dividindo e conquistando problemas até onde sua base de código é composta de peças minúsculas, funções e objetos pequenininhos, todos fracamente acoplados e executando responsabilidades singulares, mas pequeninas, todos testados em testes de unidade, com uma teia de aranha de interações abstratas isso dificulta o raciocínio sobre o que está acontecendo em termos de efeitos colaterais, e é fácil pensar teimosamente que você projetou isso lindamente, pois as peças individuais podem realmente ser bonitas e aderirem ao SOLID, enquanto ainda encontram o seu cérebro à beira de explodir a partir da complexidade de todas as interações ao tentar compreender o sistema na íntegra.
E embora seja muito fácil argumentar sobre o que qualquer um desses objetos ou funções faz individualmente, uma vez que eles desempenham uma responsabilidade tão singular, simples e talvez até bela, ao mesmo tempo em que expressam pelo menos suas dependências abstratas por meio do DI, o problema é que quando você deseja Para analisar o cenário geral, é difícil descobrir o que mil coisas pequenininhas com uma teia de aranha acabam por fazer. É claro que as pessoas dizem, basta olhar para os grandes objetos e grandes funções que estão documentados e não detalham os pequeninos, e é claro que isso ajuda a entender pelo menos o que deveria acontecer de uma maneira de alto nível. ..
No entanto, isso não ajuda muito quando você precisa realmente alterar ou depurar o código; nesse momento, você precisa descobrir o que todas essas coisas contribuem para fazer, tanto quanto as idéias de nível inferior, como efeitos colaterais e mudanças persistentes de estado e como manter invariantes em todo o sistema. E é muito difícil reunir os efeitos colaterais que ocorrem entre interações de milhares de coisas pequeninas, estejam elas usando interfaces abstratas para se comunicar ou não.
ECS
Portanto, a coisa mais importante que descobri para mitigar esse problema são realmente os sistemas de componentes de entidades, mas isso pode ser um exagero para muitos projetos. Eu me apaixonei pelo ECS até o ponto em que agora, mesmo quando escrevo pequenos projetos, uso meu mecanismo ECS (mesmo que esse pequeno projeto possa ter apenas um ou dois sistemas). No entanto, para pessoas que não estão interessadas no ECS, tenho tentado descobrir por que o ECS simplificou tanto a capacidade de compreender o sistema e acho que estou pensando em algumas coisas que devem ser aplicáveis a muitos projetos, mesmo quando não o fazem. use uma arquitetura ECS.
Loops homogêneos
Um começo básico é favorecer loops mais homogêneos, o que tende a implicar mais passes nos mesmos dados, mas passes mais uniformes. Por exemplo, em vez de fazer isso:
for each entity:
apply physics to entity
apply AI to entity
apply animation to entity
update entity textures
render entity
... de alguma forma, parece ajudar muito se você fizer isso:
for each entity:
apply physics to entity
for each entity:
apply AI to entity
etc.
E isso pode parecer um desperdício de loop nos mesmos dados várias vezes, mas agora cada passagem é muito homogênea. Ele permite que você pense: "Tudo bem, durante esta fase do sistema, nada está acontecendo com esses objetos, exceto a física. Se há coisas sendo alteradas e efeitos colaterais acontecendo, todas elas são alteradas de maneira muito uniforme. " E, de alguma forma, acho que isso ajuda muito a raciocinar sobre a base de código.
Embora pareça um desperdício, também pode ajudá-lo a encontrar mais oportunidades de paralelizar o código quando tarefas uniformes estão sendo aplicadas sobre tudo em cada loop. E também tende a incentivarum maior grau de dissociação. Por natureza, quando você tem esses passes divorciados que não tentam fazer tudo com um objeto em um passo, você tende a encontrar mais oportunidades para desacoplar facilmente o código e mantê-lo dissociado. No ECS, os sistemas costumam ser completamente dissociados um do outro e não há "classe" ou "função" externa coordenando-os manualmente. O ECS também não sofre repetidas falhas de cache necessariamente, uma vez que não necessariamente executa o loop repetidamente dos mesmos dados várias vezes (cada loop pode acessar diferentes componentes localizados completamente em outro local da memória, mas associados às mesmas entidades). Os sistemas não precisam ser coordenados manualmente, pois são autônomos e responsáveis pelo loop. Eles só precisam acessar os mesmos dados centrais.
Portanto, é uma maneira de começar que pode ajudá-lo a estabelecer um tipo de fluxo de controle mais uniforme e simples sobre o seu sistema.
Achatamento da manipulação de eventos
Outra é reduzir a dependência na manipulação de eventos. A manipulação de eventos geralmente é necessária para descobrir coisas externas que aconteceram sem a pesquisa, mas muitas vezes existem maneiras de evitar eventos push em cascata que levam a fluxos de controle e efeitos colaterais muito difíceis de prever. A manipulação de eventos, por natureza, tende a lidar com coisas complexas que acontecem com um objeto minúsculo de cada vez, quando queremos focar em coisas simples e uniformes que acontecem com muitos objetos de cada vez.
Portanto, por exemplo, em vez de um evento de redimensionamento do sistema operacional redimensionar um controle pai que começa a enviar eventos de redimensionamento e pintura para cada filho, o que pode cascatear mais eventos para quem sabe onde, você só pode acionar eventos de redimensionamento e marcar o pai e os filhos como dirty
e precisando ser repintado. Você pode até marcar todos os controles como precisando ser redimensionados; nesse ponto, um LayoutSystem
pode pegar e redimensionar as coisas e acionar eventos de redimensionamento para todos os controles relevantes.
Em seguida, seu sistema de renderização da GUI poderá ser ativado com uma variável de condição e percorrer os controles sujos e repintá-los com uma passagem ampla (não uma fila de eventos), e essa passagem inteira será focada em nada além de pintar uma interface do usuário. Se houver uma dependência hierárquica de ordem para repintar, descubra as regiões ou retângulos sujos e redesenhe tudo nessas regiões na ordem z adequada, para que você não precise fazer uma travessia de árvore e possa percorrer os dados de maneira muito moda simples e "plana", não de forma recursiva e "profunda".
Parece uma diferença tão sutil, mas acho isso bastante útil do ponto de vista do fluxo de controle por algum motivo. Trata-se realmente de reduzir o número de coisas que acontecem com objetos individuais ao mesmo tempo, tentando apontar para algo semelhante ao SRP, mas aplicado em termos de loops e efeitos colaterais: o " Princípio do loop de tarefa única ", " O único tipo de lado Efeito por princípio de loop ".
Esse tipo de fluxo de controle permite que você pense sobre o sistema mais em termos de tarefas grandes, pesadas, mas extremamente uniformes, aplicadas em loops, nem todas as funções e efeitos colaterais que podem continuar com um objeto individual de cada vez. Por mais que isso não pareça, faria uma enorme diferença, eu achei que fazia toda a diferença no mundo, pelo menos na medida em que a capacidade de minha mente compreender o comportamento da base de código em todas as áreas que importavam ao fazer alterações ou depuração (que também achei muito menos necessário com essa abordagem).
Fluxo de dependências para dados
Essa é provavelmente a parte mais controversa da ECS e pode até ser desastrosa para alguns domínios. Isso viola diretamente o Princípio de Inversão de Dependências do SOLID, que afirma que as dependências devem fluir para abstrações, mesmo para módulos de baixo nível. Isso também viola a ocultação de informações, mas pelo menos para o ECS, não tanto quanto parece, pois normalmente apenas um ou dois sistemas acessam os dados de qualquer componente.
E acho que a idéia de dependências fluindo para abstrações funciona lindamente se suas abstrações são estáveis (como em inalteráveis). Dependências devem fluir em direção à estabilidade . No entanto, pelo menos na minha experiência, as abstrações geralmente não eram estáveis. Os desenvolvedores nunca as acertariam e encontrariam necessidade de alterar ou remover funções (adicionar não era muito ruim), além de descontinuar algumas interfaces um ou dois anos depois. Os clientes mudavam de idéia de maneira a quebrar os cuidadosos conceitos que os desenvolvedores construíam, derrubando a fábrica abstrata do conjunto abstrato de cartões abstratos.
Enquanto isso, acho que os dados são muito mais estáveis. Como exemplo, quais dados um componente de movimento precisa em um jogo? A resposta é bem simples. Ele precisa de algum tipo de matriz de transformação 4x4 e precisa de uma referência / ponteiro para um pai para permitir a criação de hierarquias de movimento. É isso aí. Essa decisão de design pode durar a vida útil de todo o software.
Pode haver algumas sutilezas como se devemos usar ponto flutuante de precisão única ou ponto flutuante de precisão dupla para a matriz, mas ambas são decisões decentes. Se o SPFP for usado, a precisão é um desafio. Se o DPFP for usado, a velocidade é um desafio, mas ambas são boas escolhas que não precisam ser alteradas ou necessariamente ocultas atrás de uma interface. Qualquer uma das representações é com a qual podemos nos comprometer e nos manter estáveis.
No entanto, quais são todas as funções necessárias para uma IMotion
interface abstrata e, mais importante, qual é o conjunto mínimo ideal de funções que ela deve fornecer para fazer as coisas de maneira eficaz contra as necessidades de todos os subsistemas que lidam com o movimento? Isso é muito, muito mais difícil de responder sem entender muito mais da totalidade das necessidades de design do aplicativo antecipadamente. E assim, quando tantas partes da base de código terminam dependendo disso IMotion
, podemos ter que reescrever muito a cada iteração de design, a menos que consigamos acertar na primeira vez.
Obviamente, em alguns casos, a representação dos dados pode ser muito instável. Algo pode depender de uma estrutura de dados complexa que possa precisar de substituição no futuro devido a inadequações na estrutura de dados, enquanto as necessidades funcionais do sistema associadas à estrutura de dados são facilmente antecipadas. Portanto, vale a pena ser pragmático e decidir, caso a caso, se as dependências fluem para abstrações ou dados, mas, às vezes, pelo menos, é mais fácil estabilizar os dados do que abstrações, e não foi até eu abraçar o ECS que eu até considerou fazer dependências fluírem predominantemente para os dados (com efeitos surpreendentemente simplificadores e estabilizadores).
Portanto, embora isso possa parecer estranho, nesses casos em que é muito mais fácil criar um design estável para dados em uma interface abstrata, sugiro direcionar as dependências para limpar os dados antigos. Isso pode economizar muitas iterações repetidas de reescritas. No entanto, referente aos fluxos de controle e ao código de espaguete e ravioli, isso também tenderá a simplificar seus fluxos de controle quando você não precisar ter interações tão complexas antes de finalmente obter os dados relevantes.