The problem is that the author simply didn't notice the refactoring and abstraction opportunities available. (One question to ask yourself: "How do I test this?" If you can't answer that question, the code is wrong.)<p>We'll start with the synchronous example:<p><pre><code> myThing = synchronousCache.get("id:3244");
if (myThing == null) {
myThing = synchronousDB.query("SELECT * from something WHERE id = 3244");
}
</code></pre>
This is verbose and tedious. We should really make the API look like:<p><pre><code> myThing = database.lookup({'id':3244}, {'cache':cache_object});
</code></pre>
Let's apply this idea to his asynchronous example. We want the code to look like:<p><pre><code> database.lookup({'id':3244}, {'cache':cache_object}, function(myThing) {
// whatever
});
</code></pre>
So instead of writing this:<p><pre><code> asynchronousCache.get("id:3244", function(err, myThing) {
if (myThing == null) {
asynchronousDB.query("SELECT * from something WHERE id = 3244", function(err, myThing) {
// We now have a thing from DB, do something with result
// ...
});
} else {
// We have a thing from cache, do something with result
// ...
}
});
</code></pre>
We need to refactor this. Remember, node.js is a continuation-passing-style language. So let's set a convention and say that every function takes two continuations (success and error).<p>Then, to compose two functions of one argument:<p><pre><code> function f(x, result, error)
function g(x, result, error)
</code></pre>
To:<p><pre><code> h = f o g
</code></pre>
You write:<p><pre><code> function compose(f, g){
return function(x, result, error){
g(x, function(x_){ f(x_, result, error) }, error);
}
}
</code></pre>
(Data flows right-to-left over composition, so "do x, then do y" is written: "do y" o "do x".)<p>Now we can cleanly write a complex program from simple parts. We'll start by creating a result type:<p><pre><code> result = { 'id': null, 'value': null, 'not_found': null }
</code></pre>
Then, we'll implement cache functions that take keys (as results of this type) and return values (as results of this type). Looking up an entry in cache looks like:<p><pre><code> cache.lookup = function(key, result, error){
new_key = key.copy();
cache.raw_cache.lookup(key.id, function(value){
new_key.result = value;
new_key.not_found = false;
result(new_key)
},
function(error_type, error_msg){
if(error_type == ENOENT){
new_key.not_found = true;
result(new_key)
}
else {
error(error_type, error_msg);
}
});
};
</code></pre>
Looking up an entry in the database looks about the same. The key feature is that the "return value" and the "input" are of the same type. That makes composing, in the case of "try various abstract storage layer lookups in a fixed order", very easy. (Yes, the example is contrived.)<p><pre><code> dbapi.lookup = function(key, result, error){ ... };
</code></pre>
Now we can very easily implement the logic, "look up a value in the cache, if it's not there, look it up in the database":<p><pre><code> cached_lookup = compose(dbapi.lookup, cache.lookup);
cached_lookup(1234, do_next_step, handle_error);
</code></pre>
You can, of course, generalize compose to something like:<p><pre><code> my_program = do([cache.lookup, dbapi.lookup, print_result]);
</code></pre>
Writing clean and maintainable code in node.js is the same as writing it in any other language. You need to design your program correctly, and rewrite the parts that aren't designed correctly when you realize that your code is becoming messy.<p>Continuation-passing style is pretty weird, but you do get some benefits over the alternatives. Writing a program with coroutines involves deferring to the scheduler coroutine every so often, littering your code with meaningless lines like "yield();". Using "real" threads is even worse; your code looks like single-threaded code, but different parts of your program are running concurrently. (Did you share any non-thread-safe data structures, like Java's date formatter? Hope not, because you won't know you did until the production code dies at 3am.) Continuation-passing style lets you "pretend" that you are executing multiple threads concurrently, but the structure of the code ensures that only one codepath is running at a time. This means that libraries that don't do IO don't have to be thread safe, since only one "thread" runs at a time.<p>All concurrency models involve trade-offs over other concurrency models. But when comparing them, make sure you're comparing the actual trade-offs, not your programming ability with each model.