Circular Dependencies in JavaScript
Trust me, you need to remove those circular dependencies in your project!
The JavaScript world is emphasizing modular code, however, support for circular dependencies is limited. I propose a once-and-for-all solution to that problem here — by using what I call API binding.
Case
While developing a multi-precision JavaScript library, I had accumulated ~1,000 lines of code in a BigInteger
file. That’s not an acceptable amount of code in one file, so I decided to RIP it apart.
husky-multiprecision/
src/
BigInteger.js
IntegerMultiplication.js // provides a static multiply()
IntegerDivision.js // provides a static divide()
index.js
The structure of my project included these files. IntegerDivision
and IntegerMultiplication
obviously depend on BigInteger
, while BigInteger
depends on the other two to expose API functions BigInteger.multiply()
and BigInteger.divide()
. Now, using circular dependencies here is not a solution — that is because this is a library, not an application, which needs to satisfy all types of module systems, including those that don’t like circular dependencies.
Binding an extension API on the BigInteger
class will allow external modules to use BigInteger.multiply()
and BigInteger.divide()
while internal files will use IntegerMultiplication
and IntegerDivision
APIs.
Let’s use Binding
Since external libraries and applications using my multi-precision library will “require” the index.js
file, and won’t touch any internal file, I can hide the circular dependency inside the src
directory by creating a “Extension API” and binding it to the BigInteger
class.
// husky-multiprecision/BigInteger.extension.js// After binding BigIntegerExtension_* functions to
// the BigInteger class (in index.js), we can call
// multiply and divide on a BigInteger object.const BigInteger = require('./BigInteger');
const IntegerMultiplication = require('./IntegerMultiplication');
const IntegerDivision = require('./IntegerDivision);function BigIntegerExtension_multiply(other) {
return IntegerMultiplication.multiply(this, other);
}function BigIntegerExtension_divide(other) {
return IntegerDivision.divide(this, other);
}module.exports = {
multiply: BigIntegerExtension_multiply,
divide: BigIntegerExtension_divide
);
Now, we need to “bind” the API in BigInteger.extension.js
:
// husky-multiprecision/index.js/*
* Code in the global scope here is invoked only once,
* no matter how many times this file is imported, hence,
* don't worry about the binding occurring more than
* once.
*
* Don't believe me, try adding console.log("Binder"); anywhere
* here and import it twice. "Binder" will be shown only once.
*/const BigInteger = require('./src/BigInteger');
const BigIntegerExtAPI = require('./BigInteger.extension.js');BigInteger.prototype.multiply = BigIntegerExtAPI.multiply;
BigInteger.prototype.divide = BigIntegerExtAPI.divide;module.exports = {
BigInteger: BigInteger
};
Hooray!!! Now you can use the binding pattern to create fine, modular code without having to deal with circular dependencies.