Que mãe fez uma pergunta! Eu posso me envergonhar de tentar este com meus pensamentos peculiares (e eu adoraria ouvir sugestões se realmente estiver de folga). Mas, para mim, a coisa mais útil que aprendi recentemente em meu domínio (que incluía jogos no passado, agora VFX) foi substituir as interações entre interfaces abstratas por dados como um mecanismo de dissociação (e, finalmente, reduzir a quantidade de informações necessárias entre coisas e sobre o outro ao mínimo absoluto). Isso pode parecer completamente insano (e eu posso estar usando todos os tipos de terminologia ruim).
No entanto, digamos que eu lhe ofereça um emprego razoavelmente gerenciável. Você tem este arquivo contendo dados de cena e animação para renderizar. Há documentação que cobre o formato do arquivo. Seu único trabalho é carregar o arquivo, renderizar imagens bonitas para a animação usando o rastreamento de caminho e enviar os resultados para os arquivos de imagem. Essa é uma aplicação de pequena escala que provavelmente não vai abranger mais de dezenas de milhares de LOC, mesmo para um renderizador bastante sofisticado (definitivamente não milhões).
Você tem seu próprio mundinho isolado para este renderizador. Não é afetado pelo mundo exterior. Isola sua própria complexidade. Além das preocupações de ler esse arquivo de cena e gerar seus resultados em arquivos de imagem, você se concentra inteiramente na renderização. Se algo der errado no processo, você sabe que está no renderizador e nada mais, pois não há mais nada envolvido nessa imagem.
Enquanto isso, digamos que você precisa fazer com que seu renderizador funcione no contexto de um grande software de animação que, na verdade, possui milhões de LOC. Em vez de apenas ler um formato de arquivo simplificado e documentado para obter os dados necessários para a renderização, você deve passar por todos os tipos de interfaces abstratas para recuperar todos os dados necessários para fazer o seu trabalho:
De repente, seu renderizador não está mais em seu próprio mundo pouco isolado. Parece tão, muito mais complexo. Você precisa entender o design geral de todo o software como um todo orgânico com potencialmente muitas partes móveis, e talvez até às vezes tenha que pensar em implementações de malhas ou câmeras se você encontrar um gargalo ou um bug em um dos As funções.
Funcionalidade vs. dados simplificados
E um dos motivos é que a funcionalidade é muito mais complexa que os dados estáticos. Também existem muitas maneiras pelas quais uma chamada de função pode dar errado de uma maneira que a leitura de dados estáticos não pode. Existem muitos efeitos colaterais ocultos que podem ocorrer ao chamar essas funções, mesmo que, conceitualmente, esteja apenas recuperando dados somente leitura para renderização. Também pode ter muitos outros motivos para mudar. Daqui a alguns meses, você poderá encontrar a interface de malha ou textura alterando ou descontinuando as peças de maneira a exigir que você reescreva seções pesadas do seu renderizador e acompanhe essas alterações, mesmo que esteja buscando exatamente os mesmos dados, embora a entrada de dados para o renderizador não foi alterada (apenas a funcionalidade necessária para acessar tudo).
Então, quando possível, eu descobri que os dados simplificados são um mecanismo de dissociação muito bom, que realmente evita que você tenha que pensar em todo o sistema como um todo e apenas concentra-se em uma parte muito específica do sistema para criar melhorias, adicione novos recursos, conserte coisas, etc. Ele segue uma mentalidade de E / S para as peças volumosas que compõem seu software. Entre com isso, faça o que quiser, produza isso e, sem passar por dezenas de interfaces abstratas, encerre inúmeras chamadas de função ao longo do caminho. E está começando a se parecer, até certo ponto, com programação funcional.
Portanto, essa é apenas uma estratégia e pode não ser aplicável a todas as pessoas. E, claro, se você estiver voando sozinho, ainda precisará manter tudo (incluindo o formato dos dados), mas a diferença é que, quando você se senta para fazer melhorias nesse renderizador, pode realmente se concentrar apenas no renderizador para a maior parte e nada mais. Torna-se tão isolado em seu próprio mundinho - quase tão isolado quanto poderia ser com os dados necessários para que a entrada seja tão simplificada.
E usei o exemplo de um formato de arquivo, mas ele não precisa ser um arquivo que fornece os dados simplificados de interesse para entrada. Pode ser um banco de dados na memória. No meu caso, é um sistema de componente de entidade com os componentes que armazenam os dados de interesse. No entanto, eu achei esse princípio básico de dissociação em relação a dados simplificados (como você faz) muito menos exigente em minha capacidade mental do que nos sistemas anteriores em que trabalhei, que giravam em torno de abstrações e muitas, muitas e muitas interações acontecendo entre todas essas interfaces abstratas que tornavam impossível apenas sentar-se com uma coisa e pensar apenas sobre isso e pouco mais. Meu cérebro encheu-se à beira desses tipos de sistemas anteriores e queria explodir porque havia tantas interações acontecendo entre tantas coisas,
Dissociação
Se você deseja minimizar a quantidade de bases de código maiores que sobrecarrega seu cérebro, faça com que cada parte robusta do software (todo um sistema de renderização, todo um sistema de física etc.) viva no mundo mais isolado possível. Minimize a quantidade de comunicação e interação que passa ao mínimo possível através dos dados mais simplificados. Você pode até aceitar alguma redundância (algum trabalho redundante para o processador ou mesmo para você) se a troca for um sistema muito mais isolado que não precise conversar com dezenas de outras coisas antes de poder fazer seu trabalho.
E quando você começa a fazer isso, parece que está mantendo uma dúzia de aplicativos de pequena escala em vez de um gigantesco. E acho isso muito mais divertido também. Você pode sentar e apenas trabalhar em um sistema para o conteúdo do seu coração sem se preocupar com o mundo exterior. Torna-se apenas a entrada dos dados corretos e a saída dos dados corretos no final para algum lugar onde outros sistemas possam acessá-los (nesse ponto, algum outro sistema poderá inserir isso e fazer suas coisas, mas você não precisa se preocupar com isso) ao trabalhar no seu sistema). É claro que você ainda precisa pensar em como tudo se integra à interface do usuário, por exemplo (eu ainda acho que tenho que pensar no design de tudo como um todo para as GUIs), mas pelo menos não quando você se senta e trabalha no sistema existente ou decida adicionar um novo.
Talvez eu esteja descrevendo algo óbvio para as pessoas que se mantêm atualizadas com os métodos de engenharia mais recentes. Eu não sei. Mas não era óbvio para mim. Eu queria abordar o design de software em torno de objetos interagindo uns com os outros e funções sendo chamadas para software em larga escala. E os livros que li originalmente sobre design de software em larga escala focaram-se em designs de interface acima de coisas como implementações e dados (o mantra da época era que implementações não importavam muito, apenas interfaces, porque as primeiras podiam ser facilmente trocadas ou substituídas ) Inicialmente, não me ocorreu intuitivamente pensar nas interações de um software como se resumissem apenas à entrada e saída de dados entre enormes subsistemas que mal se comunicam, exceto por esses dados simplificados. No entanto, quando comecei a mudar meu foco para projetar em torno desse conceito, tornou as coisas muito mais fáceis. Eu poderia adicionar muito mais código sem meu cérebro explodir. Parecia que eu estava construindo um shopping center em vez de uma torre que poderia desmoronar se eu adicionasse muito ou se houvesse uma fratura em qualquer parte.
Implementações complexas vs. interações complexas
Esse é outro que eu deveria mencionar, porque passei boa parte do início da minha carreira buscando as implementações mais simples. Então, eu decompus as coisas nos pedacinhos mais simples e adolescentes, pensando que estava melhorando a capacidade de manutenção.
Em retrospectiva, não percebi que estava trocando um tipo de complexidade por outro. Ao reduzir tudo para os bits e partes mais simples, as interações ocorridas entre essas partes pequeninas se transformaram na rede mais complexa de interações com chamadas de função que às vezes chegavam a 30 níveis de profundidade no pilha de chamadas. E, claro, se você olhar para qualquer função, é tão, tão simples e fácil saber o que ela faz. Mas você não está obtendo muitas informações úteis nesse ponto, porque cada função está fazendo muito pouco. Você acaba tendo que rastrear todos os tipos de funções e pular todos os tipos de argolas para descobrir o que eles contribuem de maneiras que podem fazer seu cérebro explodir mais do que um maior,
Isso não é para sugerir objetos de Deus ou algo assim. Mas talvez não precisemos dividir nossos objetos de malha nas menores coisas, como um objeto de vértice, objeto de aresta e objeto de face. Talvez pudéssemos mantê-lo em "malha" com uma implementação moderadamente mais complexa por trás dele em troca de radicalmente menos interações de código. Eu posso lidar com uma implementação moderadamente complexa aqui e ali. Não consigo lidar com um zilhão de interações com os efeitos colaterais que ocorrem quem sabe onde e em que ordem.
Pelo menos acho isso muito menos exigente no cérebro, porque são as interações que fazem meu cérebro doer em uma grande base de código. Nenhuma coisa específica.
Generalidade vs. Especificidade
Talvez ligado ao acima, eu adorava generalidade e reutilização de código, e pensava que o maior desafio de projetar uma boa interface era atender a maior variedade de necessidades, porque a interface seria usada por todo tipo de coisas diferentes, com diferentes necessidades. E quando você faz isso, inevitavelmente precisa pensar em centenas de coisas ao mesmo tempo, porque está tentando equilibrar as necessidades de centenas de coisas ao mesmo tempo.
Generalizar as coisas leva muito tempo. Veja as bibliotecas padrão que acompanham nossos idiomas. A biblioteca padrão C ++ contém tão pouca funcionalidade, mas exige que equipes de pessoas mantenham e sintonizem com comitês inteiros de pessoas debatendo e fazendo propostas sobre seu design. Isso ocorre porque essa pequena parte da funcionalidade está tentando lidar com as necessidades do mundo inteiro.
Talvez não precisemos levar as coisas tão longe. Talvez seja bom apenas ter um índice espacial usado apenas para detecção de colisão entre malhas indexadas e nada mais. Talvez possamos usar outro para outros tipos de superfícies e outro para renderização. Eu costumava ficar tão focado em eliminar esses tipos de redundâncias, mas parte do motivo era porque estava lidando com estruturas de dados muito ineficientes implementadas por uma grande variedade de pessoas. Naturalmente, se você tem uma octree que leva 1 gigabyte para uma malha de triângulo de 300k, não deseja ter mais uma na memória.
Mas por que os octrees são tão ineficientes em primeiro lugar? Posso criar octrees que usam apenas 4 bytes por nó e menos de um megabyte para fazer a mesma coisa que a versão em gigabytes enquanto constroem uma fração do tempo e realizamos consultas de pesquisa mais rápidas. Nesse ponto, alguma redundância é totalmente aceitável.
Eficiência
Portanto, isso é relevante apenas para campos críticos de desempenho, mas quanto melhor você obtiver eficiência na memória, mais poderá gastar um pouco mais (talvez aceite um pouco mais de redundância em troca de generalidade ou desacoplamento reduzido) em favor da produtividade . Além disso, ajuda a ficar muito bom e confortável com seus criadores de perfil e a aprender sobre a arquitetura do computador e a hierarquia de memória, porque você pode fazer mais sacrifícios à eficiência em troca da produtividade, porque seu código já é muito eficiente e pode ser um pouco menos eficiente, mesmo nas áreas críticas, enquanto ainda supera a concorrência. Descobri que melhorar nesta área também me permitiu fugir com implementações mais simples e mais simples,
Confiabilidade
Isso é óbvio, mas é melhor mencioná-lo. Suas coisas mais confiáveis exigem o mínimo de sobrecarga intelectual. Você não precisa pensar muito sobre eles. Eles apenas trabalham. Como resultado, quanto maior a sua lista de peças ultra confiáveis, que também são "estáveis" (não precisam ser alteradas) por meio de testes completos, menos você precisa pensar.
Específicos
Então, tudo isso acima cobre algumas coisas gerais que foram úteis para mim, mas vamos passar para aspectos mais específicos da sua área:
Nos meus projetos menores, é fácil lembrar um mapa mental de como todas as partes do programa funcionam. Fazendo isso, posso estar ciente de como qualquer alteração afetará o restante do programa e evitar erros de maneira muito eficaz, além de ver exatamente como um novo recurso deve se encaixar na base de código. Quando tento criar projetos maiores, no entanto, acho impossível manter um bom mapa mental que leve a códigos muito confusos e a inúmeros erros não intencionais.
Para mim, isso tende a estar relacionado a efeitos colaterais complexos e fluxos de controle complexos. Essa é uma visão de baixo nível das coisas, mas todas as interfaces de aparência mais agradável e toda a separação entre o concreto e o abstrato não podem facilitar a discussão sobre efeitos colaterais complexos que ocorrem em fluxos de controle complexos.
Simplifique / reduza os efeitos colaterais e / ou simplifique os fluxos de controle, idealmente ambos. e você geralmente achará muito mais fácil entender o que sistemas muito maiores fazem e também o que acontecerá em resposta às suas alterações.
Além dessa questão do "mapa mental", acho difícil manter meu código dissociado de outras partes dele. Por exemplo, se em um jogo multiplayer houver uma classe para lidar com a física do movimento do jogador e outra para lidar com a rede, não vejo como uma dessas classes não confiar na outra para obter dados de movimento do jogador no sistema de rede para enviá-lo pela rede. Esse acoplamento é uma fonte significativa da complexidade que interfere em um bom mapa mental.
Conceitualmente, você precisa ter algum acoplamento. Quando as pessoas falam sobre desacoplamento, geralmente significam substituir um tipo por outro, mais desejável (geralmente para abstrações). Para mim, dado meu domínio, a maneira como meu cérebro funciona etc., o tipo mais desejável de reduzir ao mínimo os requisitos do "mapa mental" é os dados simplificados discutidos acima. Uma caixa preta cospe dados que são alimentados em outra caixa preta, e ambos completamente alheios à existência um do outro. Eles só sabem de algum lugar central onde os dados são armazenados (por exemplo, um sistema de arquivos central ou um banco de dados central) através dos quais eles buscam suas entradas, fazem alguma coisa e cospem uma nova saída que outra caixa preta pode então inserir.
Se você fizer dessa maneira, o sistema de física dependerá do banco de dados central e o sistema de rede dependerá do banco de dados central, mas eles não saberão nada um do outro. Eles nem precisariam se conhecer. Eles nem precisariam saber que interfaces abstratas existem.
Por fim, muitas vezes me vejo criando uma ou mais classes de "gerente" que coordenam outras classes. Por exemplo, em um jogo, uma classe lidaria com o loop principal de ticks e chamaria métodos de atualização nas classes networking e player. Isso contraria a filosofia do que eu descobri em minha pesquisa de que cada classe deve ser testada em unidade e utilizável independentemente de outras, uma vez que qualquer classe de gerente, por seu próprio propósito, depende da maioria das outras classes do projeto. Além disso, a orquestração de classes de um gerente do restante do programa é uma fonte significativa de complexidade não mapeada pela mente.
Você tende a precisar de algo para orquestrar todos os sistemas do seu jogo. O Central talvez seja pelo menos menos complexo e mais gerenciável do que como um sistema de física que invoca um sistema de renderização depois de concluído. Mas aqui, inevitavelmente, precisamos que algumas funções sejam chamadas e, de preferência, são abstratas.
Portanto, você pode criar uma interface abstrata para um sistema com uma update
função abstrata . Em seguida, ele pode se registrar no mecanismo central e seu sistema de rede pode dizer: "Ei, eu sou um sistema e aqui está minha função de atualização. Por favor, ligue-me de tempos em tempos".E então seu mecanismo pode percorrer todos esses sistemas e atualizá-los sem chamadas de função codificadas para sistemas específicos.
Isso permite que seus sistemas vivam mais como seu próprio mundo isolado. O mecanismo de jogo não precisa mais conhecê-los especificamente (de maneira concreta). E então seu sistema de física pode ter sua função de atualização chamada; nesse momento, ele insere os dados necessários no banco de dados central para o movimento de tudo, aplica a física e, em seguida, retorna o movimento resultante.
Depois disso, seu sistema de rede pode ter sua função de atualização chamada, quando entra os dados necessários no banco de dados central e gera, por exemplo, dados de soquete para os clientes. Novamente, o objetivo que eu vejo é isolar cada sistema o máximo possível, para que ele possa viver em seu próprio pequeno mundo com o mínimo conhecimento do mundo exterior. Esse é basicamente o tipo de abordagem adotada no ECS que é popular entre os mecanismos de jogos.
ECS
Acho que devo cobrir o ECS um pouco, pois muitos dos meus pensamentos acima giram em torno do ECS e tentam racionalizar por que essa abordagem orientada a dados para desacoplar tornou a manutenção muito mais fácil do que os sistemas orientados a objetos e baseados em COM que mantive no passado, apesar de violar quase tudo que eu considerava sagrado originalmente, que eu aprendi sobre SE. Além disso, pode fazer muito sentido para você se você estiver tentando criar jogos em larga escala. Portanto, o ECS funciona assim:
E, como no diagrama acima, o MovementSystem
pode ter sua update
função chamada. Nesse ponto, ele pode consultar o banco de dados central em busca de PosAndVelocity
componentes como dados a serem inseridos (componentes são apenas dados, sem funcionalidade). Em seguida, ele pode percorrer esses pontos, modificar as posições / velocidades e efetivamente gerar os novos resultados. Em seguida, o RenderingSystem
pode ter sua função de atualização chamada, quando então consulta o banco de dados PosAndVelocity
eSprite
componentes e gera imagens na tela com base nesses dados.
Todos os sistemas estão completamente alheios à existência um do outro e nem precisam entender o que Car
é. Eles só precisam conhecer componentes específicos do interesse de cada sistema que compõem os dados necessários para representar um. Cada sistema é como uma caixa preta. Ele insere dados e gera dados com conhecimento mínimo do mundo externo, e o mundo externo também possui conhecimento mínimo sobre isso. Pode haver algum evento empurrando de um sistema e pulando de outro, de modo que, digamos, a colisão de duas entidades no sistema de física possa fazer com que o áudio veja um evento de colisão que faz com que ele reproduza som, mas os sistemas ainda podem estar inconscientes sobre nós. E eu achei esses sistemas muito mais fáceis de raciocinar. Eles não fazem meu cérebro querer explodir, mesmo que você tenha dezenas de sistemas, porque cada um é muito isolado. Você não não é preciso pensar na complexidade de tudo como um todo quando você aumenta o zoom e trabalha em um deles. E por isso, também é muito fácil prever os resultados de suas alterações.