📜 ⬆️ ⬇️

"Noodles" from callbacks - keep it simple

In the footsteps of recent topics, as well as constant stories in the style of "my startup did not take off, because its noodles from callbacks sounded."

Just recently, I finished a small project (I don’t give a link to not be suspected - who needs to see the profile), completely and at all stages written only in JS, and, moreover, completely asynchronous. Of course, I ran into the notorious "noodle" problem. And, you will not believe, quite calmly decided it without any frameworks and cunning tricks.

So, let's say we have a task: asynchronously select the number of books from the database, then asynchronously select the right stack of books from the database, then asynchronously select the metadata for the books from the database, and then put it all into one dataset and render the template. How does it usually look like?
')


exports.processRequest = function (request, response) { db.query('SELECT COUNT(id) FROM books', function (res1) { // do something db.query('SELECT * FROM books LIMIT ' + Number(limit) + ' OFFSET' + Number(offset), function (res2) { // do something 2 db.query('SELECT * FROM bookData WHERE bookId IN (' + ids.join(', ') + ')', function (res3) { //     - dataset response.write(render(dataset)); }); }); }); } 


If you throw in a couple of intermediate steps, then everything will become completely bad.
Now we ask ourselves a simple question: why did we write these noodles? Do we really need three nested closures here?

Of course not. We have absolutely no need from the third anonymous function to have access to the closures of the second and first. Rewrite a little code:

 exports.processRequest = function (request, response) { var dataset = {}; getBookCount(); function getBookCount () { db.query('SELECT COUNT(id) FROM books', onBookCountReady); } function onBookCountReady (res) { // ... dataset getBooks(); } function getBooks () { db.query('SELECT * FROM books LIMIT ' + dataset.limit + ' OFFSET' + dataset.offset, onBooksReady); } function onBooksReady (res) { // ...  dataset getMetaData(); } function getMetaData () { db.query('SELECT * FROM bookData WHERE bookId IN (' + dataset.ids.join(', ') + ')', onMetaDataReady); } function onMetaDataReady (res) { // ...  dataset finish(); } function finish () { response.write(render(dataset)); } } 


You are welcome. The code has become completely linear, and, importantly, more structured; the whole program flow is in front of you, the logical blocks of code are decorated with separate functions. Any frameworks and cunning syntaxes. And no pitfalls - the dataset is closed in the context of processing the request-response pair, accidentally getting into some data shared between the request-s will not work.

Everything is a little more complicated if you need to parallelize something. I didn’t have such a task, but if I did (let's say there are two sets of metadata), then I would solve it like this:

  function getMetaData () { var parallelExecutor = new ParallelExecutor({ meta1: getMetaData1, meta2: getMetaData2 }); function getMetaData1 () { db.query('smthng', onMetaData1Ready); } function getMetaData2 () { db.query('smthng', onMetaData2Ready); } function onMetaData1Ready (res) { //  dataset parallelExecutor.ready('meta1'); } function onMetaData2Ready (res) { //  dataset parallelExecutor.ready('meta2'); } parallelExecutor.start(onMetaDataReady); } function onMetaDataReady () { } 


The meaning is the same - to create a separate closure for a set of functions, which is usually combined into "noodles", and paint them sequentially.

It seems that in such a format, asynchronous callback-and not only do not litter the code, but, on the contrary, make it more structured and readable.

Source: https://habr.com/ru/post/137818/


All Articles