Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Last active February 12, 2024 00:35
Show Gist options
  • Save unscriptable/5920818 to your computer and use it in GitHub Desktop.
Save unscriptable/5920818 to your computer and use it in GitHub Desktop.
How AOP simplified some code when writing a prototype module loader. Promises would make this code even cleaner and saner. FWIW, a Promises/A+ implementation is the second thing that gets loaded by this code! If promises were native to Javascript, I'd definitely be using them instead of callbacks.
// The following two functions are composed from simpler functions using AOP.
// getShim(callback) is the entry point. The composed functions still
// require lots of work to test, but since they're composed of simpler,
// testable functions, we can be more confident in our test coverage
// and test validity.
/**
* Gets the shim and calls back. Uses before advice to modify the getShim
* function to instruct any further calls to just queue callbacks.
* @function
* @param {Function} cb
*/
var getShim = before(
function (cb) {
// fetch the shim and then save it
fetchShim(saveShimImpl);
},
function (cb) {
// rewrite getShim
getShim = waitForShim;
// register the first callback as a waiter
waitForShim(cb);
}
);
/**
* Saves the shim implementation. Uses after advice to modify the getShim
* function to stop queueing callbacks and calls them immediately, instead.
* All callbacks are called at this point.
* @function
*/
var saveShimImpl = after(
saveShimProto,
function () {
// rewrite getShim
getShim = callShimNow;
// call all of the waiting callbacks
callShimWaiters();
}
);
// The following functions are very simple and single-purposed.
// These are incredibly easy to test via stubbing / spies, rather
// than sophisticated mocking.
function fetchShim (callback) {
fetch('lib/Loader', callback);
}
function waitForShim (callback) {
shim.waiters.push(callback);
}
function callShimWaiters () {
var waiter;
while (waiter = shim.waiters.unshift()) waiter();
}
function saveShimProto (impl) {
// save the shim's prototype to get at the methods.
shim.impl = impl.prototype;
}
function callShimNow (cb) { cb(shim.impl); }
/***** AOP functions *****/
// I wrote some simple, brute-force advising functions since
// I could not rely on a module system in this code, but
// you should use a more elegant AOP solution, if possible.
// meld.js is my favorite: https://github.com/cujojs/meld/
function before (func, advice) {
return function () {
advice.apply(null, arguments);
return func.apply(this, arguments);
}
}
function after (func, advice) {
return function () {
var result = func.apply(this, arguments);
advice(result);
return result;
}
}
// This is not important to the AOP, but is here for
// completeness.
var shim = {
impl: null,
waiters: []
};
function fetch () { /* ... */ }
// This is the original set of functions.
// They're doing many things at once and require sophisticated mocking
// to test them.
// Note: most of the complexity in these functions is due to the side-effects.
// Specifically, the `getShim` variable is reassigned at specific times.
// In the AOP version, the side-effects are moved to functions that are
// composed into the main functions via AOP.
var getShim = function () {
// rewrite getShim
getShim = waitForShim;
// register the first callback as a waiter
waitForShim(cb);
// fetch the shim
fetch('lib/Loader', onGotLoader);
};
function onGotLoader (Loader) {
// save shim
shim.impl = Loader.prototype;
// rewrite getShim
getShim = callShimNow;
// notify waiters
callShimWaiters();
}
// These functions are the same. They are easy to test.
function waitForShim (callback) {
shim.waiters.push(callback);
}
function callShimWaiters () {
var waiter;
while (waiter = shimWaiters.unshift()) waiter();
}
function callShimNow (cb) { cb(shim.impl); }
// This is not important to the AOP, but is here for
// completeness.
var shim = {
impl: null,
waiters: []
};
function fetch () { /* ... */ }
@unscriptable
Copy link
Author

i guess it'd be easy enough to write the functions like this:

var getShim = function (cb) {
    // rewrite getShim
    getShim = waitForShim;
    // register the first callback as a waiter
    waitForShim(cb);
    // fetch the shim and then save it
    fetchShim(saveShimImpl);
};

var saveShimImpl = function (Loader) {
    saveShimProto(Loader);
    // rewrite getShim
    getShim = callShimNow;
    // call all of the waiting callbacks
    callShimWaiters();
); 

However, the AOP approach is more declarative and may work in more situations where you may not be in control of all of the functions and/or methods.

@unscriptable
Copy link
Author

Simple AOP:

var origMethod = someObject.method;
someObject.method = function () {
    // do something here
    var result = origMethod.apply(this, arguments);
    // do something else here
    return result;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment