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

Бабулины сказки

Все такие крутые, могут настроить 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 пробела на таб — роскошь, я знаю и наслаждаюсь) и весь код маячит далеко справа при использовании хотя бы десятка таких функций подряд


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

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


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

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

Всем хорошего говнокода, а мне — срача в каментах, покажите дедушке, что он отстал от времени и нихрена не шарит без очков.

3 Комментарии “Пирамида смерти

    1. Давай, короче, без таких приколов. Функциональный подход в этом примере идет от архитектуры навязанной нодой, было бы желание и потребность, можно обернуть все в классы и иметь копию java.nio.file, тока мне за это пока не заплатили.

  1. Привет!
    Когда-то вместе на SSI работали, было дело)
    Некто А.Абакумов.
    Если такого поца помнишь ) —> https://www.facebook.com/o.abakumov
    Вижу по твоему СV — на всех галерах Украины напахался уже)

    Как жизнь?

Комментарии закрыты