Один из соломенных людей, на которого люди часто ссылаются при обсуждении событийно-ориентированного программирования, ala Node.js , — это страх, что сложное поведение на стороне сервера примет форму неуправляемо сложных, глубоко вложенных обратных вызовов.
Хотя большая часть кода, который я до сих пор писал с помощью Node, очень проста, обычно включает только один обратный вызов, мой мысленный образ Node — это поступающий запрос, который запускает серию операций с базой данных и другие асинхронные запросы, которые все объединить, каким-то темным образом, в один ответ. Я представляю себе такую просьбу, как пинбол, падающий в несколько бамперов и подпрыгивающий вокруг, сбивая цели, пока они не выстрелили обратно к плавникам.
Вот пример рабочего процесса из примера приложения, которое я создаю; Я управляю набором изображений, используемых в слайд-шоу; поэтому у меня есть сущность SlideImage в MongoDB (с использованием Mongoose ), и каждый SlideImage ссылается на файл, хранящийся в GridFS Mongo .
Когда приходит время удалить SlideImage, необходимо также удалить файл GridFS. Псевдокод для такой операции в системе, не основанной на событиях, может выглядеть примерно так:
def deleteSlideImageById(db, imageId) err, slideImage = db.readSlideImage(imageId) if err ... err, file = db.openGridFile(slideImage.fileId) if err ... err = file.delete() if err ... err = slideImage.delete() if err ...
Внутри Node, где весь код управляется событиями и ориентирован на обратный вызов, мы можем улучшить псевдокод, выполнив параллельное удаление документа SlideImage и файла GridFS. Ну, в любом случае, это теория, но я обычно придерживаюсь подхода водопада , так как отслеживание после завершения всех операций будет утомительным и подвержено ошибкам, особенно в свете правильной обработки любых ошибок, которые могут возникнуть.
Введите асинхронную функцию auto () . С помощью auto () вы определяете набор задач, которые зависят друг от друга. Каждая задача — это функция, которая получает обратный вызов, и объект результатов. auto () определяет, когда каждая задача готова к запуску.
Когда задача не выполняется, она передает ошибку в функцию обратного вызова. Когда задача завершается успешно, она передает нулевое значение и значение результата в функцию обратного вызова. Последующее выполнение задач позволяет увидеть результаты предыдущих задач в объекте результата, введя имя предыдущей задачи.
Как и в случае водопада (), одиночный обратный вызов передается объекту ошибки или объекту конечного результата.
Давайте посмотрим, как все это сочетается, в пять шагов:
- Найти документ SlideImage
- Откройте файл GridFS
- Удалить файл GridFS
- Удалить документ SlideImage
- Отправить ответ (успех или неудача) клиенту
Степень детализации здесь частично зависит от конкретных API и их обратных вызовов.
Код для этого на удивление труден:
app.delete "/api/images/pending/:id", (req, res) -> async.auto find: [(callback) -> schema.SlideImage.findById req.params.id, callback] remove: ["find", (callback, results) -> results.find.remove callback] openFile: ["find", (callback, results) -> new GridStore(mongoose.connection.db, results.find.file, "r").open callback] removeFile: ["openFile", (callback, results) -> results.openFile.unlink callback], (err) -> if err res.send "unable to delete SlideImage or File", 500 else res.send result: "ok"
auto () передается два значения; объект, который сопоставляет ключи с массивами, и окончательный обратный вызов. Каждый массив состоит из имен зависимостей для задачи, за которыми следует функция задачи (вы можете просто указать функцию, если у задачи нет зависимостей, но я предпочитаю согласованность каждой записи, являющейся массивом).
Так что поиск не имеет никаких зависимостей, и пинки всего процесса. Я думаю, что действительно важно то, насколько последовательны API-интерфейсы Node.js: базовый обратный вызов, состоящий из ошибки и результата, позволяет очень легко интегрировать код из множества разных библиотек и авторов (я думаю, что там есть некая монада ). В коде обратный вызов, созданный auto () и переданный для поиска, вполне подходит для перехода в findById. Все это с низким импедансом: не нужно писать какие-либо прокладки или адаптеры.
Более поздние задачи принимают дополнительный параметр результатов; results.find — это документ SlideImage, предоставленный задачей поиска.
Задачи удаления и openFile зависят от поиска: они будут выполняться в определенном порядке после поиска; что еще более важно, их обратные вызовы будут вызываться в непредсказуемом порядке, основанном на завершении различных асинхронных операций.
Только после того, как все задачи выполнены (или одна задача передала ошибку своему обратному вызову), вызывается последний обратный вызов; это то, что отправляет клиенту ошибку 500 или успех 200 (с ответом JSON {«result»: «ok»}.
Я думаю, что этот код и читабелен, и сжат; на самом деле, я не могу себе представить, чтобы это было намного более кратким. Мой мозг начинает действительно идти параллельно: часть моего мозга оценивает все с точки зрения Java-кода и идиом, в то время как остальное охватывает идиомы JavaScript, CoffeeScript и Node.js; Java-часть впечатлена тем, насколько эти решения JavaScript отказываются от сложных API-интерфейсов в пользу работы с определенной «формой» данных; если бы я писал что-то подобное на Java, я был бы заинтересован в гибких интерфейсах и скрытых реализациях с тонной кода для написания и тестирования.
Я не уверен, что приложение, которое я пишу, будет иметь какие-либо рабочие процессы обработки, значительно более сложные, чем этот бит, но если это произойдет, я вполне рад иметь auto () в своем наборе инструментов.