Краткая заметка про наследование в Node.js
В JavaScript существует множество разных способов наследования, классового и прототипного, фабричного и через примеси, прямого и непрямого, а так же гибриды нескольких методов. Но у Node.js есть его родной способ с применением util.inherits (ChildClass, ParentClass). До недавнего времени я использовал нодовский способ только для встроенных классов (когда нужно сделать своего наследника для EventEmitter, Readable/Writable Stream, Domain, Duffer и т.д.), а для моделирования предметной области применял общеупотребительные для всего JavaScript практики. И вот, впервые, понадобилось реализовать собственную иерархию системных классов, не наследников от встроенных, но и не классов предметной области, а классов, массово поражаемых в системном коде сервера приложений Impress. И простого использования util.inherits уже как-то не хватило, поискал я статьи и не найдя полностью всего, что мне нужно, изучил примеры наследования в исходниках самой ноды, подумал и сделал пример родного нодовского наследования себе на память и написал эту небольшую заметку, чтобы она, надеюсь, помогла еще и вам. Сразу предупреждаю, что реализация вызова метода родительского класса из переопределенного в дочернем классе метода, мне не очень нравится из-за громоздкости, поэтому, приветствую альтернативные способы и приглашаю коммитить их в репозиторий или в комментарии к этой заметке.Требования к реализации:
Использование Node.js нативного наследования util.inherits Определение полей и методов к классе предке и в классе наследнике Возможность вызова родительского конструктора из дочернего конструктора Возможность переопределения методов в дочернем классе Возможность вызова метода родительского класса из переопределенного в дочернем классе метода Базовый примерИмеем два класса, связанных наследованием и вызываем конструктор родительского класса из конструктора дочернего через this.constructor.super_.apply (this, arguments). Естественно, этот вызов может быть как вначале дочернего, конструктора, так и его конце или в середине. Вызов может быть обернут в условие, т.е. мы полностью управляем откатом к функциональности конструктора предка. var util = require ('util');
// Определение классов
function ParentClass (par1, par2) { this.parentField1 = par1; this.parentField2 = par2; }
function ChildClass (par1, par2) { this.constructor.super_.apply (this, arguments); this.childField1 = par1; this.childField2 = par2; }
// Наследование
util.inherits (ChildClass, ParentClass);
// Создание объекта дочернего класса и проверка результата
var obj = new ChildClass ('Hello', 'World'); console.dir ({ obj: obj });
/* Консоль:
{ obj: { parentField1: 'Hello', parentField2: 'World', childField1: 'Hello', childField2: 'World' } } */ Расширенный примерТут уже определяем методы и свойства как для родительского класса, так и для дочернего, через prototype. Напомню, что это будут методы и свойства не порожденных экземпляров, а самих классов, т.е. они будут видны у экземпляров, но содержатся в прототипах. По выводу в консоль видно, что все работает так, как и должно, удобно и предсказуемо. var util = require ('util');
// Конструктор родительского класса function ParentClass (par1, par2) { this.parentField1 = par1; this.parentField2 = par2; }
// Метод родительского класса ParentClass.prototype.parentMethod = function (par) { console.log ('parentMethod (»' + par + '»)'); };
// Свойство родительского класса ParentClass.prototype.parentField = 'Parent field value';
// Конструктор дочернего класса function ChildClass (par1, par2) { this.constructor.super_.apply (this, arguments); this.childField1 = par1; this.childField2 = par2; }
// Наследование util.inherits (ChildClass, ParentClass);
// Метод дочернего класса ChildClass.prototype.childMethod = function (par) { console.log ('childMethod (»' + par + '»)'); };
// Свойство дочернего класса ChildClass.prototype.childField = 'Child field value';
// Создание объектов от каждого класса var parentClassInstance = new ParentClass ('Marcus', 'Aurelius'); var childClassInstance = new ChildClass ('Yuriy', 'Gagarin');
// Проверка результатов console.dir ({ parentClassInstance: parentClassInstance, childClassInstance: childClassInstance });
console.dir ({ objectFieldDefinedInParent: childClassInstance.parentField1, classFieldDefinedInParent: childClassInstance.parentField, objectFieldDefinedInChild: childClassInstance.childField1, classFieldDefinedInChild: childClassInstance.childField });
parentClassInstance.parentMethod ('Cartesius'); childClassInstance.childMethod ('von Leibniz');
/* Консоль:
{ parentClassInstance: { parentField1: 'Marcus', parentField2: 'Aurelius' }, childClassInstance: { parentField1: 'Yuriy', parentField2: 'Gagarin', childField1: 'Yuriy', childField2: 'Gagarin' } } { objectFieldDefinedInParent: 'Yuriy', classFieldDefinedInParent: 'Parent field value', objectFieldDefinedInChild: 'Yuriy', classFieldDefinedInChild: 'Child field value' } parentMethod («Cartesius») childMethod («von Leibniz»)
*/ Пример с переопределением методовДальше интереснее, у ParentClass есть метод methodName и нам нужно переопределить его у наследника ChildClass с возможностью вызова метода предка из новой переопределенной реализации. var util = require ('util');
// Конструктор родительского класса function ParentClass (par1, par2) { this.parentField1 = par1; this.parentField2 = par2; }
// Метод родительского класса ParentClass.prototype.methodName = function (par) { console.log ('Parent method implementation: methodName (»' + par + '»)'); };
// Конструктор дочернего класса function ChildClass (par1, par2) { this.constructor.super_.apply (this, arguments); this.childField1 = par1; this.childField2 = par2; }
// Наследование util.inherits (ChildClass, ParentClass);
// Переопределение метода в дочернем классе ChildClass.prototype.methodName = function (par) { // Вызов метода родительского класса this.constructor.super_.prototype.methodName.apply (this, arguments); // Собственный функционал console.log ('Child method implementation: methodName (»' + par + '»)'); };
// Создание объекта дочернего класса var childClassInstance = new ChildClass ('Lev', 'Nikolayevich');
// Проверка результатов childClassInstance.methodName ('Tolstoy');
/* Консоль:
Parent method implementation: methodName («Tolstoy») Child method implementation: methodName («Tolstoy»)
*/ Эта конструкция для вызова метода родительского класса конечно очень громоздка: this.constructor.super_.prototype.methodName.apply (this, arguments), но другого способа для родной нодовской реализации наследования я не нашел. Единственное, сомнительное улучшение, которое пришло мне в голову приведено в следующем примере.Альтернативный способ наследованияДля того, чтобы упростить синтаксис вызова метода предка, нам придется расплачиваться производительностью и добавлением метода override в базовый класс Function, т.е. для всех функций вообще (в текущем контексте ноды, или внутри песочницы/sandbox, если это все происходит внутри кода, запущенного в экранированном контексте памяти — песочнице). Вызов после этого становится изящным: this.inherited (…) или можно использовать универсальный вариант: this.inherited.apply (this, arguments), в котором не нужно подставлять все параметры по именам в вызов родительского метода.
var util = require ('util');
// Средство для переопределения функций Function.prototype.override = function (fn) { var superFunction = this; return function () { this.inherited = superFunction; return fn.apply (this, arguments); }; };
// Конструктор родительского класса function ParentClass (par1, par2) { this.parentField1 = par1; this.parentField2 = par2; }
// Метод родительского класса ParentClass.prototype.methodName = function (par) { console.log ('Parent method implementation: methodName (»' + par + '»)'); };
// Конструктор дочернего класса function ChildClass (par1, par2) { this.constructor.super_.apply (this, arguments); this.childField1 = par1; this.childField2 = par2; }
// Наследование util.inherits (ChildClass, ParentClass);
// Переопределение метода в дочернем классе ChildClass.prototype.methodName = ParentClass.prototype.methodName.override (function (par) { // Вызов метода родительского класса this.inherited (par); // или this.inherited.apply (this, arguments); // Собственный функционал console.log ('Child method implementation: methodName (»' + par + '»)'); });
// Создание объекта дочернего класса var childClassInstance = new ChildClass ('Lev', 'Nikolayevich');
// Проверка результатов childClassInstance.methodName ('Tolstoy');
/* Консоль:
Parent method implementation: methodName («Tolstoy») Child method implementation: methodName («Tolstoy»)
*/ Сравнение производительностиПредпочтительный вариант очевиден, но все же нужно произвести измерения. Вызов метода класса предка на одном и том же оборудовании 10000000 вызовов: this.constructor.super_.prototype.methodName.apply (this, arguments); 424 мс. this.inherited (par); 1972 мс. this.inherited.apply (this, arguments); 1800 мс. Репозиторий с примерами кода и комментариями на русском и английском: https://github.com/tshemsedinov/node-inheritance
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.