Here is a design pattern I have adopted for a few projects that seems to work pretty well. This excludes the use of third-party libraries like RequireJS, which are great in their own right, but not always vital. In any case, you can use both strategies in conjunction with each other, to maximize your performance and maintenance.
The concept is simple, and relies on common modular design patterns while enforcing a few guidelines. Each component is a file, so it’s important to think of it in those terms. Below is a list that can be used as a concrete rubric with which to measure your architecture against.
- Treat each module as a reusable, separate component, that works individually of any other component
- Make sure each component has little to no dependencies to other components
- Make sure each component is sub-namespaced under a global namespace for the entire library
- Make sure each component has no data “leaking” out
- Each component returns an object from a closure once called, so you call a function, you get back an object “module”
- If possible, try to make it so instantiating an entire module is done only once
An example application
Taking from our previous example, let’s look at an example of this architecture. Below is a potential graphics application that could be written in JavaScript. There are a lot of nice, modular pieces here that we can think of.
Instantiation code myInit.js
This code lives outside the library itself.
// myInit.js
/ We’ll keep the app in a nice namespace /
var myPhotoShlopApp = PhotoshlopApp();
var logoStuffs = myPhotoShlopApp.logo();
var kerningThangs = myPhotoShlopApp.kerning();
var leadingFellas = myPhotoShlopApp.leading();
appConfig.js
This is our parent file, that inherits all the individual components. This is a good spot to keep global state machines, or configuration options (unless you prefer a specific config file.)
// appConfig.js
window.PhotoshlopApp = PhotoshlopApp || function() {
return {
config: {
constants: {
SOMECONSTANT: 32,
MAX_BRUSH_SIZE: 100,
MIN_BRUSH_SIZE: 2,
FLOWERS_PER_SQ_INCH: 2.12031
}
},
state: {
monkeys: true,
cats: ‘meow’,
getState: function(state_name) {
// getter
return state[state_name];
},
setState: function(state_name, new_value) {
// setter
return state[state_name] = new_value;
}
},
prepThings: function() {},
loadStuff: function() {}
// etc…
};
};
Some module kerning.js
// photoshlopApp/kerning.js
PhotoshlopApp.kerning = function() {
return {
setKerning: function() {},
getKerning: function() {},
adjustKerning: function() {},
// etc…
};
};
Some module leading.js
// photoshlopApp/leading.js
PhotoshlopApp.leading = function() {
return {
setLeading: function() {},
setLeading: function() {},
adjustLeading: function() {},
// etc…
};
};
Some module logo.js
// photoshlopApp/logo.js
PhotoshlopApp.logo = function() {
return {
makeItBigger: function() {
return alert(‘Make the logo bigger!’);
}
};
};
You can see the pattern here…
…and it’s all very straightforward. These examples are a bit silly, but one nice result that comes from this methodology is a clean, obvious file structure. I’m a big fan of how Python modules work, and how directory structure inevitably gets laid out. Here’s an example of our library with some more components:
/photoShlopPlugin/appConfig.js
/photoShlopPlugin/leading/metrics.js
/photoShlopPlugin/leading/optical.js
/photoShlopPlugin/kerning/metrics.js
/photoShlopPlugin/kerning/optical.js
/photoShlopPlugin/logo/logo.js
/photoShlopPlugin/logo/logo-inspiration.js
/photoShlopPlugin/logo/logo-exporter.js
/photoShlopPlugin/patterns/pattern-maker.js
/photoShlopPlugin/patterns/pattern-importer.js
/photoShlopPlugin/patterns/pattern-examples.js
/photoShlopPlugin/patterns/pattern-tools.js
As you can see, the library can continually grow and still be well organized, and still easy to instantiate/implement in a modular fashion.
What about importing files?
Since JavaScript doesn’t actually have true importing from disk, the only way to do it is to link each file in html as needed. My preferred method is to use a build tool like Grunt, and create build targets for each need (or just build all files into a minified version, like most popular libraries like jQuery or Bootstrap).
Either way, it has served me pretty well, and guards against technical debt when massive change and refactoring come into play.