JavaScript: how to remove circular dependencies from your project

Circular or cyclic dependencies are natural in many domain models where certain objects of the same domain depend on each other. But in software design, circular dependencies between larger software modules are not considered as a pattern because of their negative effects. Mutually recursive modules are common in functional programming which encourages inductive and recursive definitions.


The reference could be direct (A -> B -> A):


// file a.ts
import { b } from 'b';
...
export a;
// file b.ts
import { a } from 'a';
...
export b;
or indirect (A -> B -> C -> A):
// file a.ts
import { b } from 'b';
...
file a;
// filr b.ts
import { c } from 'c';
...
export b;
// file c.ts
import { a } from 'a';
...
export c;

Although circular dependencies don't result in errors directly, they will almost always have unintended consequences. In the current project, slow TypeScript type and frequent dev-server “out of memory” crashes have been checked.


Node.js supports require/import circular statements between modules but can get confusing quickly. According to the Node.js documentation, you should carefully plan to allow cyclic dependencies to function properly in an application.


Note, the best way to deal with circular dependencies is to avoid them altogether. Circular dependencies are usually an indication of incorrect code design and should be refactored and removed if possible.


Verifying Circular Dependencies


There are some Node packages that perform static analysis to look for circular dependencies, but they don't work well: some packages have found circular dependencies, while others completely missed all of them. The best circular dependency checker works on the packaging layer. The circular-dependency plugin is efficient and easy to use.


Taking the circular-dependency plugin documentation example:


// webpack.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin')
module.exports = {
  entry: "./src/index",
  plugins: [
    new CircularDependencyPlugin({
      // exclude detection of files based on a RegExp
      exclude: /a\.js|node_modules/,
      // add errors to webpack instead of warnings
      failOnError: true,
      // allow import cycles that include an asyncronous import,
      // e.g. via import(/* webpackMode: "weak" */ './file.js')
      allowAsyncCycles: false,
      // set the current working directory for displaying module paths
      cwd: process.cwd(),
    })
  ]
}

Immediately, the plugin found all kinds of circular dependencies introduced during the project.


Correcting Circular Dependencies


There are a few options to get rid of circular dependencies. For a longer chain A -> B -> C -> D -> A, if one of the references is removed, the cyclic reference pattern will also be broken.


For simpler patterns like A -> B -> A refactoring may be necessary. Modules can be moved from B to A. The required code could be also extracted to a C and both A and B can reference. If both modules perform similar behaviors, they can also be combined into a single module.


Fixing many circular dependencies can be a significant compromise, but can improve the ability to maintain the code base and reduce errors.


Thanks for attention!


References


en.wikipedia.org/wiki/Circular_dependency
nyanglish.com/circular-dependencies
railsware.com/blog/2018/06/27/how-to-analyze-circular-dependencies-in-es6

Also popular now: