Como posso gerenciar a base de código de software significativamente complexo?


8

Costumo criar programas para mim e para os outros usando várias linguagens de programação orientadas a objetos. Ao fazer isso, eles geralmente são relativamente pequenos (alguns milhares de linhas no máximo). Recentemente, no entanto, tenho tentado fazer projetos maiores, como mecanismos de jogos completos. Ao fazê-lo, pareço frequentemente encontrar um obstáculo: complexidade.

Nos meus projetos menores, é fácil lembrar um mapa mental de como todas as partes do programa funcionam. Fazendo isso, posso estar totalmente 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. No entanto, quando tento criar projetos maiores, acho impossível manter um bom mapa mental que leva a códigos muito confusos e a inúmeros erros não intencionais.

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.

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 uma 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.

Tomados em conjunto, isso me impede de escrever software de alta qualidade, sem erros, de tamanho substancial. O que desenvolvedores profissionais fazem para lidar efetivamente com esse problema? Estou especialmente interessado nas respostas OOP direcionadas para Java e C ++, mas esse tipo de conselho é provavelmente muito geral.

Notas:

  • Tentei usar diagramas UML, mas isso apenas parece ajudar no primeiro problema, e mesmo assim apenas quando se trata da estrutura de classes, e não (por exemplo) da ordenação de chamadas de método em relação ao que é inicializado primeiro.

7
"Ao negociar ações, como faço para ganhar o máximo de dinheiro possível?" - mesma pergunta, ocupação diferente.
MaxB

1
Quanto tempo você gasta em design e modelagem? Muitos dos problemas que encontramos durante o desenvolvimento vêm do vazamento de perspectiva. O tipo de perspectiva que focamos durante a modelagem. Depois, tentamos resolver esse vazamento com código. Código que também vaza de perspectiva (a abstração adequada). Projetos de grande escala precisam de planejamento, para ter uma visão global clara. Do nível da base de código, será difícil ter a visão. " Não deixe que as árvores o impeçam de ver a floresta ".
LAIV

4
Modularização e isolamento. Mas parece que você já está fazendo isso, mas falhando nisso. Nesse caso, realmente depende de que tipo de projeto é e que tipo de tecnologias são usadas.
Euphoric

1
A refatoração constante é uma parte necessária do desenvolvimento de software - considere uma indicação de que você está fazendo algo certo. Qualquer sistema não trivial sempre contém "incógnitas desconhecidas" - isto é, as coisas que você não poderia ter previsto nem considerado no seu design original. A refatoração é a realidade do desenvolvimento iterativo e da evolução progressiva em direção a uma solução 'boa'. É a razão pela qual os desenvolvedores tendem a favorecer escrever código que é fácil de refatorar, o que frequentemente envolve seguintes princípios sólidos
Ben Cottrell

2
O UML IMHO não possui o tipo de diagrama mais útil para isso: diagramas de fluxo de dados. No entanto, isso não é um problema, a modularização e o isolamento não ocorrem automaticamente usando diagramas de fluxo de dados.
Doc Brown

Respostas:


8

Muitas vezes pareço encontrar um obstáculo: complexidade.

Existem livros inteiros escritos sobre esse assunto. Aqui está uma citação de um dos livros mais importantes já escritos sobre desenvolvimento de software, o Code Complete de Steve McConnell :

Gerenciar a complexidade é o tópico técnico mais importante no desenvolvimento de software. Na minha opinião, é tão importante que o Imperativo Técnico Principal do Software tenha que gerenciar a complexidade.

Como um aparte, eu recomendo a leitura do livro se você tiver algum interesse no desenvolvimento de software (o que eu suponho que sim, já que você fez essa pergunta). No mínimo, clique no link acima e leia o trecho sobre os conceitos de design.


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.

Nesse caso em particular, eu consideraria sua PlayerMovementCalculatorclasse e sua NetworkSystemclasse completamente não relacionadas uma à outra; uma classe é responsável pelo cálculo do movimento do jogador e a outra é responsável pela E / S da rede. Talvez até em módulos independentes separados.

No entanto, eu certamente esperaria que houvesse pelo menos algum bit adicional de fiação ou cola em algum lugar fora dos módulos que medeia dados e / ou eventos / mensagens entre eles. Por exemplo, você pode escrever uma PlayerNetworkMediatorclasse usando o Padrão do Mediador .

Outra abordagem possível pode ser desacoplar seus módulos usando um Agregador de Eventos .

No caso de programação assíncrona , como o tipo de lógica envolvida nos soquetes de rede, você pode usar o Observables de exposição para organizar o código que 'escuta' essas notificações.

Programação assíncrona também não significa necessariamente multithread; é mais sobre a estrutura do programa e o controle de fluxo (embora o multi-threading seja o caso de uso óbvio para a assincronia). Observáveis ​​podem ser úteis em um ou nos dois módulos para permitir que classes não relacionadas se inscrevam para receber notificações de alterações.

Por exemplo:

  • NetworkMessageReceivedEvent
  • PlayerPositionChangedEvent
  • PlayerDisconnectedEvent

etc.


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 uma 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.

Enquanto parte disso certamente se resume à experiência; o nome Managerem uma classe geralmente indica um cheiro de design .

Ao nomear classes, considere a funcionalidade pela qual a classe é responsável e permita que seus nomes reflitam o que ela faz .

O problema com os gerentes no código é um pouco como o problema com os gerentes no local de trabalho. Seu objetivo tende a ser vago e pouco compreendido, mesmo por eles mesmos; Na maioria das vezes, estamos melhor sem eles.

A programação orientada a objetos é principalmente sobre comportamento . Uma classe não é uma entidade de dados, mas uma representação de algum requisito funcional no seu código.

Se você pode nomear uma classe com base no requisito funcional que ela atende, reduzirá sua chance de acabar com algum tipo de Objeto Divino inchado e terá mais chances de ter uma classe cuja identidade e objetivo em seu programa sejam claros.

Além disso, deve ser mais óbvio quando métodos e comportamentos extras começam a aparecer quando realmente não pertence, porque o nome começará a parecer errado - ou seja, você terá uma classe que está fazendo um monte de coisas que não são ' t refletido por seu nome

Por fim, evite a tentação de escrever classes cujos nomes parecem pertencer a um modelo de relacionamento de entidade. O problema com nomes de classe, como Player, Monster, Car, Dog, etc. é que o nada sobre seu comportamento implica, e só parecem descrever uma coleção de dados ou atributos relacionados logicamente. O design orientado a objetos não é modelagem de dados, é modelagem de comportamento.

Por exemplo, considere duas maneiras diferentes de modelagem de um Monstere Playercalcular os danos:

class Monster : GameEntity {
    dealDamage(...);
}

class Player : GameEntity {
    dealDamage(...);
}

O problema aqui é que você pode razoavelmente esperar Playere Monsterter vários outros métodos que provavelmente não têm nenhuma relação com a quantidade de dano que essas entidades podem causar (Movimento, por exemplo); você está no caminho para o objeto de Deus mencionado acima.

Uma abordagem mais naturalmente orientada a objetos é identificar o nome da classe com base em seu comportamento, por exemplo:

class MonsterDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

class PlayerDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

Com esse tipo de design, seus objetos Playere Monsterprovavelmente não possuem nenhum método associado a eles, porque esses objetos contêm os dados necessários para todo o aplicativo; provavelmente são apenas entidades de dados simples que vivem dentro de um repositório e contêm apenas campos / propriedades.

Essa abordagem é geralmente conhecida como Modelo de Domínio Anêmico , que é considerado um antipadrão para DDD (Design Orientado a Domínio) , mas os princípios do SOLID naturalmente levam você a uma separação limpa entre entidades de dados 'compartilhadas' (talvez em um repositório) e classes comportamentais modulares (de preferência sem estado) no gráfico de objetos do seu aplicativo.

SOLID e DDD são duas abordagens diferentes para o projeto OO; enquanto atravessam de várias maneiras, tendem a seguir direções opostas com relação à identidade de classe e à separação de dados e comportamento.


Voltando à citação de McConnell acima - gerenciar a complexidade é a razão pela qual o desenvolvimento de software é uma profissão qualificada, e não uma tarefa clerical mundana. Antes de McConnell escrever seu livro, Fred Brooks escreveu um artigo sobre o assunto que resume claramente a resposta à sua pergunta - Não há nenhuma bala de prata no gerenciamento da complexidade.

Portanto, embora não haja uma resposta única, você pode facilitar ou dificultar a vida, dependendo da maneira como a aborda:

  • Lembre - se de KISS , DRY e YAGNI .
  • Entenda como aplicar os Princípios SOLID de Design / Desenvolvimento de Software OO
  • Entenda também o design orientado a domínio, mesmo se houver locais em que a abordagem entre em conflito com os princípios do SOLID; SÓLIDO e DDD tendem a concordar um com o outro mais do que discordam.
  • Espere que seu código seja alterado - escreva testes automatizados para detectar as conseqüências dessas alterações (você não precisa seguir o TDD para escrever testes automatizados úteis - na verdade, alguns desses testes podem ser testes de integração usando aplicativos de console "descartáveis" ou aplicativos de equipamento de teste)
  • Mais importante - seja pragmático. Não siga servilmente nenhuma orientação; o oposto da complexidade é a simplicidade, por isso, em caso de dúvida (novamente) - KISS

Resposta sólida. Chalaça pretendida.
ipaul

4
Não concordo que DDD e SOLID estejam em desacordo. Eu acho que eles são bastante complementares. Parte do motivo pelo qual eles podem parecer contraditórios, ou talvez ortogonais, é que eles descrevem coisas diferentes em um nível diferente de abstração.
precisa saber é o seguinte

1
@RibaldEddie Eu concordo que eles são complementares na maior parte; A principal questão que vejo é que o SOLID parece incentivar ativamente o chamado modelo de domínio "Anêmico" como uma conseqüência natural de levar o SRP à sua conclusão lógica de uma verdadeira responsabilidade única por classe, enquanto o DDD chama isso de padronizar. Ou talvez haja algo sobre a descrição de Fowler do Anemic Domain Model que eu interpretei mal?
precisa

1

Eu acho que se trata de gerenciar a complexidade, que sempre diminui quando as coisas ao seu redor se tornam complexas demais para serem gerenciadas com o que você tem atualmente e provavelmente ocorre algumas vezes ao crescer de uma empresa global para meio milhão de pessoas. .

Em geral, a complexidade cresce quando o projeto cresce. E, em geral, no momento em que a complexidade se torna muito (independente de TI ou de gerenciamento do programa, programas ou da empresa completa), você precisa de mais ferramentas ou substitui as ferramentas por ferramentas que lidam melhor com a complexidade, o que a humanidade tem feito desde sempre. E as ferramentas andam de mãos dadas com processos mais complexos, já que alguém precisa produzir algo em que outra pessoa precisa trabalhar. E eles andam de mãos dadas com "pessoas extras" que têm funções específicas, por exemplo, um arquiteto corporativo que não é necessário em um projeto de 1 pessoa em casa.

Ao mesmo tempo, sua taxonomia por trás disso deve crescer por trás de sua complexidade até que chegue o momento em que a taxonomia é muito complexa e você passa para dados não estruturados. Por exemplo, você pode fazer uma lista de sites e categorizá-los, mas se precisar de uma lista de 1 bilhão de sites, não há como fazê-lo dentro de um prazo razoável, para que você vá para algoritmos mais inteligentes ou apenas pesquise dados.

Em um projeto pequeno, você pode trabalhar em uma unidade compartilhada. Em um projeto um tanto grande, você instala .git ou .svn ou o que for. Em um programa complexo com vários projetos, todos com lançamentos diferentes, todos ativos e com datas de lançamento diferentes e todos dependentes de outros sistemas, pode ser necessário mudar para clearcase se for responsável pelo gerenciamento da configuração, em vez de apenas pela versão alguns ramos de um projeto.

Então, acho que o melhor esforço dos seres humanos até agora é uma combinação de ferramentas, funções específicas, processos para gerenciar a crescente complexidade de tudo.

Felizmente, existem empresas que vendem estruturas de negócios, estruturas de processos, taxonomias, modelos de dados, etc. Portanto, dependendo do setor em que você está, você pode comprar, quando a complexidade cresceu tanto que todos reconhecem que não há outra maneira, um modele e processe imediatamente para, por exemplo, uma companhia de seguros global, a partir daí você trabalha para refatorar seus aplicativos existentes para alinhar tudo novamente com a estrutura geral comprovada, que já inclui um modelo e processos de dados comprovados.

Se não houver mercado no mercado para você, talvez exista uma oportunidade :)


Parece que você está basicamente sugerindo alguma combinação de "contratar pessoas" ou "comprar algo que fará com que seus problemas desapareçam ™". Estou interessado em gerenciar a complexidade como um desenvolvedor individual sem orçamento trabalhando em projetos de hobby (por enquanto).
precisa saber é o seguinte

Acho que nesse nível provavelmente sua pergunta é algo como "nomear 100 melhores práticas que um bom arquiteto de aplicativos aplica"? Porque acho que ele está gerenciando a complexidade por trás de tudo o que faz. ele define padrões para combater a complexidade.
Edelwater

É isso que estou perguntando, mas estou mais interessado em uma filosofia geral do que em uma lista.
precisa saber é o seguinte

É uma questão ampla uma vez que existem 21.000+ livros sobre práticas de arquitetura application / padrões / projetar etc: safaribooksonline.com/search/?query=application%20architecture
edelwater

O livro só mostra a popularidade de uma pergunta, não a amplitude de boas respostas. Se você acha que é necessário um livro para responder a essa pergunta, faça uma recomendação.
john01dav

0

Pare de escrever tudo OOP e adicione alguns serviços.

Por exemplo, recentemente escrevi um GOAP para um jogo. Você verá os exemplos na Internet acoplando suas ações ao mecanismo de jogo com o estilo OOP:

Action.Process();

No meu caso, eu precisava processar ações no mecanismo e também fora dele, simulando ações de uma maneira menos detalhada em que o jogador não está presente. Então, em vez disso, usei:

Interface IActionProcessor<TAction>
{
    void ProcessAction(TAction action);
}

isso me permite dissociar a lógica e ter duas classes.

ActionProcesser_InEngine.ProcessAction()


ActionProcesser_BackEnd.ProcessAction()

Não é OOP, mas significa que as ações agora não têm vínculo com o mecanismo. Assim, meu módulo GOAP é completamente separado do resto do jogo.

Uma vez terminado, apenas uso a dll e não penso na implementação interna.


1
Como o seu processador de ação não é orientado a objetos?
Greg Burghardt

2
Penso que o seu ponto é: “Não tente produzir um design de POO, porque POO é apenas uma das muitas técnicas para lidar com a complexidade - outra coisa (por exemplo, uma abordagem processual) pode funcionar melhor no seu caso. Não importa como você chegue lá, desde que você tenha limites claros de módulo que abstraem os detalhes da implementação. ”
amon

1
Eu acho que o problema não está no nível do código. Está em níveis mais altos. Os problemas com a base de código são sintomas, não doenças. A solução não virá do código. Eu acho que virá do design e da modelagem.
Laiv

2
Se eu li isso imaginando-me como o OP, acho que essa resposta está faltando em detalhes e não fornece informações úteis, pois não descreve como o IActionProcessor realmente fornece valor, em vez de apenas adicionar outra camada de indireção.
Whatsisname

o uso de serviços como esse é processual e não operacional. Isso fornece uma separação mais clara entre responsabilidades do que o OO, que, como o OP descobriu, pode rapidamente se tornar excessivamente acoplado
Ewan

0

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).

insira a descrição da imagem aqui

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:

insira a descrição da imagem aqui

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 updatefunçã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:

insira a descrição da imagem aqui

E, como no diagrama acima, o MovementSystempode ter sua updatefunção chamada. Nesse ponto, ele pode consultar o banco de dados central em busca de PosAndVelocitycomponentes 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 RenderingSystempode ter sua função de atualização chamada, quando então consulta o banco de dados PosAndVelocityeSprite 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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.