Pull to refresh

«Лапша» из callback-ов — будьте проще

Reading time3 min
Views13K
По следам недавних топиков, а также постоянных рассказов в стиле «мой стартап не взлетел, потому что его зохавала лапша из callback-ов».

Как раз недавно я закончил небольшой проект (ссылку не даю, чтобы не заподозрили — кому надо см. профиль), полностью и на всех этапах написанном только на JS, и притом полностью асинхронный. Разумеется, я столкнулся с пресловутой проблемой «лапши». И, вы не поверите, совершенно спокойно решил её без всяких там фреймворков и хитрых приемов.

Итак, допустим, у нас есть задача: асинхронно выбрать из базы количество книг, потом асинхронно же выбрать из базы нужную пачку книг, потом асинхронно же выбрать из базы метаданные по книгам, а потом свести всё это в один dataset и отрендерить шаблон. Как это обычно выглядит?



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));
            });
        });
    });
}


Если накинуть ещё пару-тройку промежуточных шагов, то всё станет совсем плохо.
Теперь зададим себе простой вопрос: зачем мы написали эту лапшу? Действительно ли нам необходимы здесь три вложенных замыкания?

Нет, конечно. У нас нет ровно никакой нужды из третьей анонимной функции иметь доступ к замыканиям второй и первой. Перепишем немного код:

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));
   }
}


Пожалуйста. Код стал полностью линейным, и, что немаловажно, более структурированным; весь program flow у вас перед глазами, логические блоки кода оформлены отдельными функциями. Никаких фреймворков и хитрых синтаксисов. И никаких подводных камней — dataset замкнут в контексте обработки пары request-response, случайно залезть в какие-то разделяемые между request-ами данные не получится.

Всё немного усложняется, если нужно что-то запараллелить. У меня такой задачи не было, но если бы была (допустим, есть два набора метаданных), то я решал бы её так:

   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 () {
  }


Смысл всё тот же — создать отдельное замыкание для набора функций, который обычно объединяют в «лапшу», и расписать их последовательно.

Кажется, что в таком формате асинхронные callback-и не только не захламляют код, но и, напротив, делают его более структурированным и читабельным.
Tags:
Hubs:
+52
Comments119

Articles