Адаптеры asyncify, promisify, callbackify
Адаптер — паттерн преобразования контракта функции. asyncify: sync → callback. promisify: callback → Promise. callbackify: Promise → callback. Тройка демонстрирует паттерн Adapter на функциях, без классов. V8 особо оптимизирует двухуровневое каррирование.
asyncify: sync → callback
const asyncify = (fn) => (...args) => {
const callback = args.pop();
setTimeout(() => {
try {
callback(null, fn(...args));
} catch (err) {
callback(err);
}
}, 0);
};
const slowAdd = asyncify((a, b) => a + b);
slowAdd(2, 3, (err, sum) => console.log(sum)); // 5 (асинхронно)
Сначала пишу asyncify — из синхронной функции делать функцию с контрактом асинхронности, а потом из этого делаем promisify.
Зачем: вписать тяжёлую sync-функцию в event loop без блокировки, либо унифицировать контракт во всей программе.
error-first callback
try {
const result = fn(...args);
callback(null, result);
} catch (err) {
callback(err);
}
В колбеках принято ошибки на первый аргумент присылать. Результат — на второй. Если успешно — первый аргумент null.
Контракт Node.js: (err, data) => {}.
promisify: callback → Promise
const promisify = (fn) => (...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, data) => err ? reject(err) : resolve(data));
});
const readFileAsync = promisify(fs.readFile);
const data = await readFileAsync('config.json');
Уже встроено в Node.js: util.promisify.
callbackify: Promise → callback
const callbackify = (asyncFn) => (...args) => {
const cb = args.pop();
asyncFn(...args).then(
(data) => cb(null, data),
(err) => cb(err)
);
};
// Встроен: util.callbackify
const asyncFn = async (x) => x * 2;
const cb = util.callbackify(asyncFn);
cb(5, (err, result) => console.log(result));
Зачем: интегрировать новый async-код в legacy API, которое принимает callback.
V8 оптимизирует две стрелки подряд
В V8 они большие молодцы. Именно этот паттерн — последовательность из двух функций — особым образом оптимизируют, предполагая, что первая функция вызывается специально ради сохранения аргументов в замыкание.
const promisify = (fn) => (...args) => /* ... */;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ V8 видит паттерн
Это создаёт closure-фабрику. V8 не создаёт полноценную внешнюю функцию с накладными расходами — а вторая функция кэшируется как "method of bound context".
Трёх-четырёх уровней каррирования оптимизируется хуже — компилятор теряет паттерн.
Адаптер показан на функциях
Паттерн адаптер показан на функциях, не на классах. Паттерны — не только про объекты.
Классический GoF Adapter — это класс, оборачивающий другой класс под другой интерфейс. В ФП-стиле — функция-обёртка, преобразующая контракт.
Когда что использовать
| Ситуация | Адаптер |
|---|---|
| Sync функция блокирует event loop | asyncify (setImmediate/setTimeout) |
| Legacy callback API в новом async-коде | promisify |
| Async код в legacy callback API | callbackify |
| Promise-based код в legacy stream-API | разные адаптеры под кейс |
Связанные темы
- Promisify, callbackify, asyncify
- Async композиция и коллекторы
- callbackify и class adapter
- Замыкания (Closures)
Ресурсы
- Беседа GRASP/SOLID/GoF: LJJpbFcmKQs