Как разравнять Пирамиду смерти

Настроить webpack по мануалу, запрограммировать ангуляр и даже послать json по ajax — кажись каждый может, но вот как взглянешь на сам код… В этом посте будет показана разница между нововведениями.

Итак вы открыли ноду и увидели, что почти все функции «из коробки» последним аргументом принимают колбэк.

var fs = require("fs");
fs.readdir(__dirname, function(error, files) {
    if (error) {
        console.error(error);
    } else {
        for (var i = 0, j = files.length; i < j; i++) {
            console.log(files[i]);
        }
    }
});

Пирамида смерти

А в чем собственно проблема? Проблема в том, что на маке с ретиной порой заканчивается место под пробелы (конечно можно сказать, что 4 пробела на таб — роскошь) и весь код маячит далеко справа при использовании хотя бы десятка таких функций подряд.

1f671d21658541e8813157f085ec60b4.png

var fs = require("fs");
var path = require("path");
var buffers = [];

fs.readdir(__dirname, function(error1, files) {
    if (error1) {
        console.error(error1);
    } else {
        for (var i = 0, j = files.length; i < j; i++) {
            var file = path.join(__dirname, files[i]);
            fs.stat(file, function(error2, stats) {
                if (error2) {
                    console.error(error2);
                } else if (stats.isFile()) {
                    fs.readFile(file, function(error3, buffer) {
                        if (error3) {
                            console.error(error3);
                        } else {
                            buffers.push(buffer);
                        }
                    });
                }
            });
        }
    }
});

console.log(buffers);

Так что же c этим можно сделать? Не применяя библиотек, для наглядности, так как с ними все примеры не займут и строчки кода, дальше будет показано как с этим справиться используя сахар es6 и es7.

Promise

Встроенный объект позволяющий немного разровнять пирамиду:

var fs = require("fs");
var path = require("path");

function promisify(func, args) {
    return new Promise(function(resolve, reject) {
        func.apply(null, [].concat(args, function(error, result) {
            if (error) {
                reject(error);
            } else {
                resolve(result);
            }
        }));
    });
}

promisify(fs.readdir, [__dirname])
    .then(function(items) {
        return Promise.all(items.map(function(item) {
            var file = path.join(__dirname, item);
            return promisify(fs.stat, [file])
                .then(function(stat) {
                    if (stat.isFile()) {
                        return promisify(fs.readFile, [file]);
                    } else {
                        throw new Error("Not a file!");
                    }
                })
                .catch(function(error) {
                    console.error(error);
                });
        }));
    })
    .then(function(buffers) {
        return buffers.filter(function(buffer) {
            return buffer;
        });
    })
    .then(function(buffers) {
        console.log(buffers);
    })
    .catch(function(error) {
        console.error(error);
    });

Кода стало немного больше, но зато сильно сократилась обработка ошибок.

Обратите внимание .catch был использован два раза потому, что Promise.all использует fail-fast стратегию и бросает ошибку, если ее бросил хотя бы один промис на практике такое пременение далеко не всегда оправдано, например если нужно проверить список проксей, то нужно проверить все, а не обламываться на первой «дохлой». Этот вопрос решают библиотеки Q и Bluebird и тд, поэтому его освещать не будем.

Теперь перепишем это все с учетом arrow functions, desctructive assignment и modules.

import fs from "fs";
import path from "path";

function promisify(func, args) {
    return new Promise((resolve, reject) => {
        func.apply(null, [...args, (err, result) => {
            if (err) {
                reject(err);
            } else {
                resolve(result);
            }
        }]);
    });
}

promisify(fs.readdir, [__dirname])
    .then(items => Promise.all(items.map(item => {
        const file = path.join(__dirname, item);
        return promisify(fs.stat, [file])
            .then(stat => {
                if (stat.isFile()) {
                    return promisify(fs.readFile, [file]);
                } else {
                    throw new Error("Not a file!");
                }
            })
            .catch(console.error);
    })))
    .then(buffers => buffers.filter(e => e))
    .then(console.log)
    .catch(console.error);

Generator

Теперь совсем хорошо, но…ведь есть еще какие-то генераторы, которые добавляют новый тип функций function* и ключевое слово yeild, что будет если использовать их?

import fs from "fs";
import path from "path";

function promisify(func, args) {
    return new Promise((resolve, reject) => {
        func.apply(null, [...args, (err, result) => {
            if (err) {
                reject(err);
            } else {
                resolve(result);
            }
        }]);
    });
}

function getItems() {
    return promisify(fs.readdir, [__dirname]);
}

function checkItems(items) {
    return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
        .then(stat => {
            if (stat.isFile()) {
                return file;
            } else {
                throw new Error("Not a file!");
            }
        })
        .catch(console.error)))
        .then(files => {
            return files.filter(file => file);
        });
}

function readFiles(files) {
    return Promise.all(files.map(file => {
        return promisify(fs.readFile, [file]);
    }));
}

function * main() {
    return yield readFiles(yield checkItems(yield getItems()));
}

const generator = main();

generator.next().value.then(items => {
    return generator.next(items).value.then(files => {
        return generator.next(files).value.then(buffers => {
            console.log(buffers);
        });
    });
});

Цепочки из generator.next ().value.then не лучше чем колбэки из первого примера однако это не значит, что генераторы плохие, они просто слабо подходят под эту задачу.

Async/Await

Еще два ключевых слова, с мутным значением, которые можно попробовать прилепить к решению, уже надоевшей задачи по чтению файлов- Async/Await

import fs from "fs";
import path from "path";

function promisify(func, args) {
    return new Promise((resolve, reject) => {
        func.apply(null, [...args, (error, result) => {
            if (error) {
                reject(error);
            } else {
                resolve(result);
            }
        }]);
    });
}

function getItems() {
    return promisify(fs.readdir, [__dirname]);
}

function checkItems(items) {
    return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
        .then(stat => {
            if (stat.isFile()) {
                return file;
            } else {
                throw new Error("Not a file!");
            }
        })
        .catch(console.error)))
        .then(files => {
            return files.filter(file => file);
        });
}

function readFiles(files) {
    return Promise.all(files.map(file => {
        return promisify(fs.readFile, [file]);
    }));
}

async function main() {
    return await readFiles(await checkItems(await getItems()));
}

main()
    .then(console.log)
    .catch(console.error);

Пожалуй самый красивый пример, все функции заняты своим делом и нету никаких пирамид.

Если писать этот код не для примера, то получилось бы как-то так:

import bluebird from "bluebird";
import fs from "fs";
import path from "path";

const myFs = bluebird.promisifyAll(fs);

function getItems(dirname) {
    return myFs.readdirAsync(dirname)
        .then(items => items.map(item => path.join(dirname, item)));
}

function getFulfilledValues(results) {
    return results
        .filter(result => result.isFulfilled())
        .map(result => result.value());
}

function checkItems(items) {
    return bluebird.settle(items.map(item => myFs.statAsync(item)
        .then(stat => {
            if (stat.isFile()) {
                return [item];
            } else if (stat.isDirectory()) {
                return getItems(item);
            }
        })))
        .then(getFulfilledValues)
        .then(result => [].concat(...result));
}

function readFiles(files) {
    return bluebird.settle(files.map(file => myFs.readFileAsync(file)))
        .then(getFulfilledValues);
}

async function main(dirname) {
    return await readFiles(await checkItems(await getItems(dirname)));
}

main(__dirname)
    .then(console.log)
    .catch(console.error);

Комментарии (4)

  • 30 июня 2016 в 14:18

    0

    У меня велосипед получился точно такой же. С точностью до названий функций. А переименую-ка я их как у Вас…

  • 30 июня 2016 в 14:18

    –1

    Кто нибудь скажет мне чем promise лучше «классического ajax»?
    • 30 июня 2016 в 14:28

      +1

      Тем же, чем мягкое лучше теплого.

      На самом деле, профит от промизов в том, что это асинхронный компонуемый примитив. Они легко выстраиваются параллельно или последовательно или в любой комбинации, да еще обработка ошибок довольно удобная.

      Справедливости ради, если не лепить анонимные функции, а грамотно все декомпозировать и разносить по модулям, то и без промисов все выглялит вполне прилично. Но промизы действительно легко компонуются.

  • 30 июня 2016 в 14:25 (комментарий был изменён)

    +1

    это не из разряда лучше, а скорее вместе

© Habrahabr.ru