A custom modularization helper

Release 0.2.2

  • Better code quality
  • Better documentation
  • Some tests were rewritten

One file to rule them all

Each developer works on a local copy of the source file and pushes to the file control system
catName("Chloe");function catName(name) {
console.log("My cat's name is " + name);
}
// The result of the code above is: "My cat's name is Chloe"
  • What could be the advantages of having a single source file?
  • What is the maximum file size?

Divide and rule

…Or divide and conquer
Each developer works on separate — smaller — source files

Files organization

  • Document the structure and make it known: ask people to Read The Fabulous Manual
  • Folder names are used to qualify the files they contain. Indeed, if a folder is named “controller”, it is more than expected to find only controllers in it. The same way, for a web application, a “public” folder usually indicates that its content is exposed
  • The folder qualification can be technical (“sources”, “tests”, “public”) or functional (“controllers”, “views”, “dialogs”, “stream”) but mixing at the same level should be avoided. For one technology stack, if dialogs are implemented with controllers: it makes sense to see “dialogs” below “controllers” but having both in the same folder will be confusing
  • Try to stick to widely accepted (and understood) names: use “public” instead “www”, use “dist” or “release” instead of “shipping”…
  • Try to avoid names that are too generic: “folder” (true story), “misc”, “util”, “helpers”, “data”…
  • Stick to one language (don’t mix French and English)
  • Select and stick to a naming formalism, such as Camel Case
  • Forget about the 8.3 or MAX_PATH limitations, names can be as long as necessary
… and 640K is enough for everyone :-)

Level of granularity

Modules

  • Testability: it is sometimes difficult to draw the line between unit testing and integration testing. Long story short, if several modules are needed to run a test, it sounds like integration. On the other hand, if the module can be tested with no or very few dependencies (maybe through mocking), this is unit testing. So, ideally, a module can be easily isolated to achieve unit testing
  • Maintainability: on top of the testability, the file size should be relatively small. Both facts greatly increase the module’s maintainability. This means that it is easier (in terms of complexity and effort) to improve or fix the module.
  • Reusability: no finger pointing here. Every developer starts his career by learning the ‘virtues’ of copy & paste. It takes time and experience to realize that code duplication is evil. Modules are an indisputable alternative to copy & paste because they are designed to be reusable.Loading

Loading Modules

Everything at once

Load all the files !
  • A requires B and C to be loaded
  • B requires D to be loaded
  • E requires F to be loaded
  • F can be loaded separately
  • D, B, C, A, F, E
  • C, D, F, B, A, E
  • F, D, C, B, A, E
  • F, E, D, B, C, A
  • Sequence being managed by a list, it gives a clear understanding of the loading process and allows the developers to fully control it
  • Unlike the unique concatenated file that is regenerated for each release, some files might not even change between versions. Consequently, the loading process can benefit from caching mechanism and the application starts faster.
  • Every time a new source is created, it must be added to the list in the correct order to be loaded
  • The more files, the bigger the list (and the more complex to maintain)
  • When the list becomes too big, one may lose track of which files are really required. In the worst situation, some may be loaded even if they are not used anymore
  • Any change in dependencies implies a re-ordering of the list

Lazy loading

Or the energy saving mode…
  • Loading the application is done with the booting list: D, B, C, A
  • And, when required, the feature implemented by E is loaded with: F, E
  • Faster startup of the application, it also implies smaller initial application footprint
  • Loading instructions are split into smaller lists which are consequently easier to maintain
  • Once the application started, accessing a part that has not been previously loaded will generate delay
  • If any trouble occurs during the additional loading phase (such as no network) the application breaks

Dependency management

Is there any way to get rid of these lists ?
  • A contains information that B and C must be loaded
  • B contains information that D must be loaded
  • E contains information that F must be loaded Then:
  • Loading the application is done by loading A
  • And, when required, the feature implemented by E is loaded with E
  • No big lists to maintain, each module declare its dependencies separately
  • Better visibility on module dependencies
  • Simplified reusability of modules
  • New files are loaded if declared as dependencies
  • Only required files are loaded
  • Loading a module is more complex: dependencies must be extracted and resolved recursively.
  • Having such a fine level of granularity increases the risk of tangled dependencies and, in the worst case, deadlocks. For instance, three modules: A, B and C. If A depends on B, B depends on C and C depends on A, it is impossible to load any of them.
  • If one module is a common dependency to several other modules, it should be loaded only once. If not, the loading performances will be decreased because of multiple loading of the same module.

Interface

Global variables

Look Woody …
// No dependencies
var ANY_CONSTANT = "value";
function myExportedFunction () {
/* ...implementation... */
}
// No dependencies
var
ANY_CONSTANT = "value",
myExportedFunction;
// IIFE
(function () {
"use strict";
// Begin of private scope
// Any functions or variables declared here are 'private'
// Reveal function
myExportedFunction = function () {
/* ...implementation... */
}
// End of private scope
}());

Module interface

Do. Or do not. There is no try.
  • Changing the code to support both formats in one function is not an option: for the sake of maintainability, there must be a wrapper function to detect the format and then switch to the proper implementation.
  • Rename the save and load methods to include a version number (for instance: saveV1, loadV1, saveV2 and loadV2). Then the code (production and test) must be modified to use the proper method depending on the format that needs to be serialized.
  • Create a namespace for each version and have the methods being set inside the proper namespace: app.io.v1.save, app.io.v1.load, app.io.v2.save and app.io.v2.load. Again, the code must be adapted but provided the namespace can be a parameter (thanks to JavaScript where namespaces are objects), this reduces the cost.
  • Define an interface and have one module per version that exposes this interface. It’s almost like with namespacing but accessing the proper object does not depend on global naming but rather on loading the right module.
var
currentVersion = 2,
io = loadModule("app/io/v" + currentVersion);
// io exposes save and load methods
function save (fileName) {
// Always save with the lastest version
return io.save(fileName);
}
function load (fileName) {
var version = detect(fileName);
if (version !== currentVersion) {
return loadModule("app/io/v" + version).load(fileName);
}
return io.load(fileName);
}

Module loader

  • An application is composed of modules, they are smaller parts encapsulating single functionalities
  • To simplify the loading of the application, modules declares their list of dependencies
  • Loading the module gives access to a given set of API a.k.a. interface

Mocking

Test double

Existing implementations

Browser includes

We are … browsers !
(function (exports) {
"use strict";
// Begin of private scope
// Any functions or variables declared here are 'private'
// Reveal function
exports.myExportedFunction = function () {
/* ...implementation... */
}
// End of private scope
}(this));

RequireJS

define([
"dependency"
], function (dependency) {
"use strict";
// Begin of private scope
// Any functions or variables declared here are 'private'
// Reveal function
return {
myExportedFunction: function () {
/* ...implementation... */
}
};
// End of private scope
});

NodeJs

Front end and back end…
"use strict";
// Begin of private scope
// Any functions or variables declared here are 'private'
var dependency = require("dependency");
// Reveal function
module.exports = {
myExportedFunction = function () {
/* ...implementation... */
}
};
// End of private scope
var gpf = require("gpf-js");
var CONST = require("./const.js");

PhantomJS

Bundlers

  • it enforces code reuse by enabling modularization when developing
  • it speeds up the application loading by limiting the number of files
  • by relying on the dependence tree, it also trims files which are never loaded
  • The generated code is usually obfuscated, debugging might be complicated
  • The application file must be generated before running the application. Depending on the number of files it might take some time
The lack of humility before vanilla JavaScript that’s being displayed here, uh… staggers me.

Browserify

WebPack

Transpilers

  • Most transpilers are also bundlers:
  • Babel which understands ES6
  • CoffeeScript, a sort of intermediate language compiled into JavaScript
  • Typescript, which can be extremely summarized by a JavaScript superset with type checking

Windows Scripting Host

var
fso = new ActiveXObject("Scripting.FileSystemObject"),
srcFile = OpenTextFile("dependency.js",1),
srcContent = srcFile.ReadAll();
srcFile.Close();
eval(srcContent);
eval((new ActiveXObject("Scripting.FileSystemObject")).OpenTextFile("dependency.js",1).ReadAll());
  • Language mixing, for instance VBA with JavaScript (but we are only interested by JavaScript, aren’t we?)
  • References to component or external files

Rhino

Javascript’s native import

Here comes a new keyword
import { myExportedFunction } from "dependency";
"use strict";
// Begin of private scope
// Any functions or variables declared here are 'private'
// Reveal function
export function myExportedFunction () {
/* ...implementation... */
}
// End of private scope

GPF-JS Implementation

My implementation

API

  • It is working the same way on all hosts
  • Loading is asynchronous
  • Module scope is private (because of the pattern leveraging factory functions)
  • API exposure is done by returning an object
  • caching of loaded modules
  • cache manipulation to simplify testing by injecting mocked dependencies
  • synchronization through a Promise resolved when all dependencies are loaded and the module is evaluated (or rejected if any issue occurred)
  • lazy loading as it may be called anytime
  • resource types, dependent files may be JSON files or JavaScript modules
  • The ‘GPF’ way:
gpf.require.define({
name1: "dependency1.js",
name2: "dependency2.json",
// ...
nameN: "dependencyN.js"
}, function (require) {
"use strict";
// Private scope
require.name1.api1();
// Implementation
// Interface
return {
api1: function (/*...*/) {
/*...*/
},
// ...
apiN: function (/*...*/) {
/*...*/
}
};
});
  • Using AMD format:
define([
"dependency1.js",
"dependency2.json",
/*...*/,
"dependencyN.js"
], function (name1, name2, /*...*/ nameN) {
"use strict";
// Private scope
name1.api1();
// Implementation
// Interface
return {
api1: function (/*...*/) {
/*...*/
},
// ...
apiN: function (/*...*/) {
/*...*/
}
};
});
"use strict";
// Private scope
var
name1 = require("dependency1.js"),
name2 = require("dependency2.json"),
// ...
nameN = require("dependencyN.js");
name1.api1();
// Implementation
// Interface
module.exports = {
api1: function (/*...*/) {
/*...*/
},
// ...
apiN: function (/*...*/) {
/*...*/
}
};

Implementation details

  • sample1.js
gpf.require.define({
dep1: "dependency1.json"
}, function (require) {
alert(dep1.value);
});
  • dependency1.json
{
"value": "Hello World!"
}
  • The exposed API for a JavaScript module
  • The JavaScript object for a JSON file
  • Resolves each dependency absolute path. This is done by simply concatenating the resource name with the contextual base.
  • Gets or associates a promise with the resource. Using the absolute resource path as a key, it checks if the resource’s promise is already in the cache. If not, the resource is loaded. This will be detailed later.
  • sample2.js
gpf.require.define({
dep2: "sub/dependency2.js"
}, function (require) {
alert(dep2.value);
});
  • sub/dependency2.js
gpf.require.define({
dep2: "dependency2.json"
}, function (require) {
return {
value: dep2.value;
};
});
  • sub/dependency2.json
{
"value": "Hello World!"
}
  • JavaScript modules loading
  • Relative path management
  • Subsequent dependencies must be loaded before resolving the promise associated to the module
function (gpf, define) {gpf.require.define({
dep2: "dependency2.json"
}, function (require) {
return {
value: dep2.value;
};
});
}
  • The process of creating this function compiles the JavaScript content. This won’t detect runtime errors, such as the use of unknown symbols, but it validates the syntax.
  • The function wrapping pattern generates a private scope in which the code will be evaluated. Hence, variable and function declarations remain local to the module. Indeed, unlike eval which can alter the global context, the function evaluation has a limited footprint.
  • It allows the definition (or override) and injection of ‘global’ symbols.
  • How does the library handles relative path?
  • How does it know when the subsequent dependencies are loaded?
  • Change the settings of the require API by taking into account the current path of the resource being loaded
  • Capture the result promise of gpf.require.define if used.

Future of gpf.require

A bright future

Custom resource processor

Handling cross references

gpf.require.define({
dependency: {
path: "dependency.js",
weak: true
}
}, function (require) {
return {
exposedApi: function () {
return require.dependency
.then(function (resolvedDependency) {
return resolvedDependency.api();
};
}
}
});

Module object

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store