Como evitar que o javascript se torne código espaguete?

8

Eu fiz bastante javascript ao longo dos anos e estou usando uma abordagem mais orientada a objetos, especificamente com o padrão do módulo. Que tipo de abordagem você usa para evitar uma base de código maior para se tornar espaguete. Existe alguma "melhor abordagem"?

marko
fonte
1
Como você saberia se tivesse escrito código legível e de fácil manutenção? - Seu colega avisa depois de revisar o código.
Gnat #
Mas se um desenvolvedor acha que as coisas ficam complicadas após três linhas de código? O que fazer então?
Marko #
2
Você não pode determinar isso sozinho, porque sabe mais como autor do que o código diz por si só. Um computador não pode lhe dizer, pelas mesmas razões que não pode dizer se uma pintura é arte ou não. Portanto, você precisa de outro ser humano - capaz de manter o software - para examinar o que escreveu e dar sua opinião. O nome formal do referido processo é "Avaliação pelos Pares" ( fonte Citação - superior votou resposta a uma pergunta que é mesmo que o seu)
mosquito
Isso seria incrível se fizéssemos no trabalho.
Marko #
Refatoração contínua. Sempre são tomadas más decisões. Eles se tornam dívida técnica quando você para de se preocupar com eles.
Pithikos

Respostas:

7

Dan Wahlin fornece orientação específica sobre como evitar o código de espaguete de função em JavaScript.

A maioria das pessoas (inclusive eu) começa a escrever código JavaScript adicionando funções após funções em um arquivo .js ou HTML. Embora certamente não haja nada de errado com essa abordagem, uma vez que ela faz o trabalho, ela pode ficar fora de controle rapidamente ao trabalhar com muito código. Ao agrupar funções em um arquivo, encontrar código pode ser difícil, refatorar código é uma tarefa árdua (a menos que você tenha uma ferramenta legal como o Resharper 6.0), o escopo variável pode se tornar um problema e a manutenção do código pode ser um pesadelo, especialmente se você não escreveu originalmente.

Ele descreve alguns padrões de design JavaScript que dão estrutura ao código.

Eu prefiro o padrão de protótipo revelador . Meu segundo favorito é o padrão de módulo revelador , que difere um pouco do padrão de módulo padrão, pois você pode declarar escopo público / privado.

Jim G.
fonte
Sim, é o tipo do que estou fazendo, ao construir coisas maiores.
Marko
1
Mas onde vi o padrão nomeado pela primeira vez, deve estar em Padrões Javascript - ( amazon.com/JavaScript-Patterns-Stoyan-Stefanov/dp/0596806752/… ).
22612 marko
Eu odeio ser o cara que diz "apenas use uma estrutura" ... mas, para mim, estou sempre pensando no cara que vem atrás de mim e em ter uma estrutura bem testada, documentada e com suporte da comunidade ... é um acéfalo para mim. O meu favorito é o Javascript MVC (que em breve será o CanJS). Possui scripts de andaime e um gerenciador de dependências muito intuitivo ( steal()) que permite que você tenha um aplicativo muito bem estruturado enquanto compila scripts para 1 ou 2 arquivos minificados.
Ryan Wheale 15/10/12
4

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 dirtye precisando ser repintado. Você pode até marcar todos os controles como precisando ser redimensionados; nesse ponto, um LayoutSystempode 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 IMotioninterface 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.


fonte
1
Eu realmente gostei desta resposta, mesmo que ela não responda diretamente à pergunta, ela foi muito esclarecedora. Obrigado por contribuir, talvez você possa escrever uma série de posts sobre o tópico? Eu leria!
Ben
Felicidades! Quanto ao blog, eu gosto de usar este lugar apenas para despejar meus pensamentos! :-D
2

Os padrões de código em geral são úteis.

Que significa:

Módulos são definitivamente necessários. Você também precisa de consistência na maneira como "classes" são implementadas, ou seja, "métodos no protótipo" vs "métodos na instância". Você também deve decidir qual versão do ECMAScript deseja segmentar e, se estiver direcionando, ou seja, o ECMAScript 5, use os recursos de idioma fornecidos (por exemplo, getters e setters).

Consulte também: TypeScript, que pode ajudá-lo a padronizar, por exemplo, classes. Um pouco novo no momento, mas não vejo nenhuma desvantagem em usá-lo, pois quase não existe nenhum bloqueio (porque ele é compilado para JavaScript).

Janus Troelsen
fonte
Eu acho que o Typecript é desnecessário.
Marko #
1
@marko: Desnecessário para qual aplicativo? Você implementaria um compilador de 25 kloc sem digitar estática?
Janus Troelsen
2

Uma separação entre o código de trabalho e o código implantado é útil. Eu uso uma ferramenta para combinar e compactar meus arquivos javascript. Para que eu possa ter qualquer número de módulos em uma pasta, todos como arquivos separados, para quando estiver trabalhando nesse módulo específico. Mas, para o tempo de implantação, esses arquivos são combinados em um arquivo compactado.

Eu uso o Chirpy http://chirpy.codeplex.com/ , que também suporta SASS e coffeeScript.

user69841
fonte