Как сделать переопределение write для экземпляра Writable без создания класса наследника?nodejs-12

Основной подход

В Node.js можно переопределить метод write у экземпляра Writable stream, используя паттерн "композиция вместо наследования". Вот как это сделать:

const { Writable } = require('stream');

// Создаем базовый Writable
const writable = new Writable({
  write(chunk, encoding, callback) {
    // Базовая реализация (может быть пустой)
    console.log('Default write:', chunk.toString());
    callback();
  }
});

// Сохраняем оригинальный метод
const originalWrite = writable.write.bind(writable);

// Переопределяем метод write
writable.write = function(chunk, encoding, callback) {
  console.log('Overridden write:', chunk.toString());

  // Можно модифицировать данные перед записью
  const modifiedChunk = Buffer.from(chunk.toString().toUpperCase());

  // Вызываем оригинальный метод с модифицированными данными
  originalWrite(modifiedChunk, encoding, callback);
};

// Использование
writable.write('hello');
writable.end();

Альтернативный способ через Object.defineProperty

Для более чистого переопределения:

const { Writable } = require('stream');

const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log('Internal write:', chunk.toString());
    callback();
  }
});

Object.defineProperty(writable, 'write', {
  value: function(chunk, encoding, callback) {
    console.log('Overridden write:', chunk.toString());

    // Пропускаем через оригинальный _write
    if (typeof encoding === 'function') {
      callback = encoding;
      encoding = null;
    }

    this._write(chunk, encoding || 'utf8', callback || (() => {}));
  },
  writable: true,
  configurable: true
});

writable.write('test');

Важные нюансы

  1. Разница между write и _write:

    • write - публичный API
    • _write - внутренний метод, который реализуется в опциях
  2. Обработка callback:

    • Нужно правильно обрабатывать случаи, когда encoding является функцией
  3. Совместимость с pipe():

    • Переопределенный метод должен сохранять стандартное поведение stream

Пример с модификацией данных

const { Writable } = require('stream');

const writable = new Writable({
  write(chunk, encoding, callback) {
    console.log('Writing:', chunk.toString());
    callback();
  }
});

const originalWrite = writable.write.bind(writable);

writable.write = function(chunk, encoding, callback) {
  // Добавляем префикс ко всем данным
  const modified = Buffer.concat([
    Buffer.from('PREFIX: '),
    Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
  ]);

  originalWrite(modified, 'utf8', callback);
};

process.stdin.pipe(writable);

Резюмируем

  1. Основные способы:

    • Прямое присваивание нового метода
    • Использование Object.defineProperty
  2. Обязательные действия:

    • Сохранение и вызов оригинального метода
    • Правильная обработка параметров (encoding/callback)
  3. Применение:

    • Модификация данных перед записью
    • Логирование операций записи
    • Добавление дополнительной валидации
  4. Ограничения:

    • Не нарушайте контракт Writable stream
    • Учитывайте особенности работы с буферами и кодировками

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