como corrigir problemas de dependência circulares desagradáveis de uma vez por todas no JavaScript & TypeScript
-
index.js
necessita deAbstractNode.js
- o carregador do módulo começa a carregar
AbstractNode.js
e a correr o código do módulo. A coisa que ele encontra pela primeira vez é uma declaração de necessidade (importação) paraLeaf
- de modo que o carregador de Módulo começa a carregar o arquivo
Leaf.js
. Que, por sua vez, começa por exigirAbstractnode.js
. -
AbstractNode.js
já está sendo carregado, e está sendo imediatamente devolvido do cache do módulo. No entanto, uma vez que esse módulo ainda não foi executado além da primeira linha (a exigência deLeaf
), as declarações que introduzem a classeAbstractNode
ainda não foram executadas! - assim, a classe
Leaf
tenta estender – se a partir do valorundefined
, em vez de uma classe válida. O que lança a excepção de tempo de execução mostrada acima. BOOM!
tentativa de correcção 1
assim, acontece que a nossa dependência circular causa um problema desagradável. No entanto, se olharmos de perto, é muito fácil determinar o que a ordem de carregamento deve ser:
- carregar a classe
AbstractNode
primeira classe - carregar a classe
Node
e a classeLeaf
depois disso.
em outras palavras, vamos definir a classe AbstractNode
em primeiro lugar, e então ter que exigir Leaf
e Node
. Isso deve funcionar, porque Leaf
e Node
não tem que ser conhecido ainda ao definir a classe AbstractNode
. Desde que sejam definidos antes que AbstractNode.from
seja chamado pela primeira vez devemos ficar bem. Então, vamos tentar o seguinte alteração:
acontece que, há alguns problemas com esta solução:
First, this is ugly and doesn’t scale. Em uma grande base de código, isso resultará em mover as importações aleatoriamente até que as coisas apenas funcionam. O que muitas vezes é apenas temporário, como uma pequena refactoração ou mudança nas declarações de importação no futuro pode sutilmente ajustar a ordem de carregamento do módulo, reintroduzindo o problema.
Secondly, whether this works is highly dependent on the module bundler. Por exemplo, no codesandbox, ao agregar nosso aplicativo com pacote (ou Webpack ou Rollup), esta solução não funciona. No entanto, ao executar isto localmente com o nó.módulos js e commonjseste trabalho pode funcionar muito bem.
evitando o problema
assim, aparentemente, este problema não pode ser resolvido facilmente. Poderia ter sido evitado? A resposta é sim, existem várias maneiras de evitar o problema. Em primeiro lugar, podíamos ter mantido o código num único ficheiro. Como mostrado em nosso exemplo inicial, dessa forma podemos resolver o problema como ele dá total controle sobre a ordem em que o código de inicialização do módulo corre.
em segundo lugar, algumas pessoas vão usar o problema acima como argumento para fazer afirmações como” um não deve usar classes”, ou”não usar herança”. Mas isso é uma simplificação excessiva do problema. Embora eu concorde que os programadores muitas vezes recorrem à herança muito rapidamente, para alguns problemas é simplesmente perfeito e pode produzir grandes benefícios em termos de estrutura de código, reutilização ou desempenho. Mas o mais importante, este problema não se limita à herança de classe. Exatamente o mesmo problema pode ser introduzido ao ter dependências circulares entre variáveis de Módulo e funções que correm durante a inicialização do módulo!
poderíamos re-organizar nosso código, de tal forma que podemos quebrar a AbstractNode
classe em pequenos pedaços, de modo que AbstractNode
não tem dependências em Node
ou Leaf
. Nesta caixa de areia, o método from
foi retirado da classe AbstractNode
E colocado em um arquivo separado. Isso resolve o problema, mas agora nosso projeto e API estão estruturados de forma diferente. Em grandes projetos pode ser muito difícil determinar como fazer este truque, ou mesmo impossível! Imagine, por exemplo, o que aconteceria se o método print
dependesse de Node
ou Leaf
na próxima iteração do nosso aplicativo…
Bônus: um truque feio adicional que eu usei antes: devolver classes base de funções e função de alavancagem içar para obter as coisas carregadas na ordem certa. Nem sei como explicar como deve ser.
the internal module pattern to the rescue!
tenho lutado com este problema em várias ocasiões através de muitos projetos alguns exemplos incluem o meu trabalho em Mendix, MobX, MobX-state-tree e vários projetos pessoais. Em algum momento, alguns anos atrás eu até escrevi um script para concatenar todos os arquivos fonte e apagar todas as declarações de importação. Um empacotador de módulos para homens pobres só para ter uma noção da ordem de carregamento do módulo.
no entanto, depois de resolver este problema algumas vezes, um padrão apareceu. Um que dá total controle sobre a ordem de carregamento do módulo, sem a necessidade de reestruturar o projeto ou puxando hacks estranhos! Este padrão funciona perfeitamente com todas as cadeias de ferramentas em que o tentei (Rollup, Webpack, pacote, nó).
the crux of this pattern is to introduce an index.js
and internal.js
file. As regras do jogo são as seguintes::
- o módulo
internal.js
importa e exporta tudo a partir de cada módulo local do projecto - todos os outros módulos do projecto apenas importam do ficheiro
internal.js
, e nunca directamente a partir de outros ficheiros do projecto. - o arquivo
index.js
é o principal ponto de entrada e importa e exporta tudo a partir deinternal.js
que você quer expor ao mundo exterior. Note que este passo só é relevante se você estiver publicando uma biblioteca que é consumida por outros. Então saltamos este passo em nosso exemplo.
Note que as regras acima só se aplicam às nossas dependências locais. As importações de módulos externos são deixadas como está. Afinal de contas, não estão envolvidos nos nossos problemas circulares de dependência. Se aplicarmos esta estratégia à nossa aplicação de demonstração, o nosso código ficará assim:
quando você aplica este padrão pela primeira vez, ele pode se sentir muito artificial. Mas tem alguns benefícios muito importantes!
- em primeiro lugar, resolvemos o nosso problema! Como demonstrado aqui, o nosso aplicativo está correndo alegremente novamente.
- a razão pela qual isso resolve o nosso problema é: agora temos controle total sobre a ordem de carregamento do módulo. Seja qual for a ordem de importação em
internal.js
, será a nossa ordem de carregamento do módulo. (Você pode querer verificar a imagem abaixo, ou reler a explicação de ordem do módulo acima para ver por que este é o caso) - Nós não precisamos aplicar refactorings que não queremos. Nem somos forçados a usar truques feios, como mover requer declarações para o fundo do arquivo. Não temos de comprometer a arquitectura, a API ou a estrutura semântica da nossa base de códigos.Bónus: as declarações de importação tornar-se-ão muito mais pequenas, uma vez que estaremos a importar coisas de menos ficheiros. Por exemplo
AbstractNode.js
tem apenas na declaração de importação agora, onde tinha dois antes. - bónus: com
index.js
, temos uma única fonte de verdade, dando controle fino sobre o que expomos ao mundo exterior.