В JavaScript существует множество разных способов наследования, классового и прототипного, фабричного и через примеси, прямого и непрямого, а так же гибриды нескольких методов. Но у Node.js есть его родной способ с применением util.inherits(ChildClass, ParentClass). До недавнего времени я использовал нодовский способ только для встроенных классов (когда нужно сделать своего наследника для EventEmitter, Readable/Writable Stream, Domain, Buffer и т.д.), а для моделирования предметной области применял общеупотребительные для всего JavaScript практики. И вот, впервые, понадобилось реализовать собственную иерархию системных классов, не наследников от встроенных, но и не классов предметной области, а классов, массово поражаемых в системном коде сервера приложений Impress. И простого использования util.inherits уже как-то не хватило, поискал я статьи и не найдя полностью всего, что мне нужно, изучил примеры наследования в исходниках самой ноды, подумал и сделал пример родного нодовского наследования себе на память и написал эту небольшую заметку, чтобы она, надеюсь, помогла еще и вам. Сразу предупреждаю, что реализация вызова метода родительского класса из переопределенного в дочернем классе метода, мне не очень нравится из-за громоздкости, поэтому, приветствую альтернативные способы и приглашаю коммитить их в репозиторий или в комментарии к этой заметке.
Требования к реализации:
Базовый пример
Имеем два класса, связанных наследованием и вызываем конструктор родительского класса из конструктора дочернего через ClassName.super_.apply(this, arguments). Естественно, этот вызов может быть как вначале дочернего, конструктора, так и его конце или в середине. Вызов может быть обернут в условие, т.е. мы полностью управляем откатом к функциональности конструктора предка.
Расширенный пример
Тут уже определяем методы и свойства как для родительского класса, так и для дочернего, через prototype. Напомню, что это будут методы и свойства не порожденных экземпляров, а самих классов, т.е. они будут видны у экземпляров, но содержатся в прототипах. По выводу в консоль видно, что все работает так, как и должно, удобно и предсказуемо.
Пример с переопределением методов
Дальше интереснее, у ParentClass есть метод methodName и нам нужно переопределить его у наследника ChildClass с возможностью вызова метода предка из новой переопределенной реализации.
Альтернативный способ наследования
Для того, чтобы упростить синтаксис вызова метода предка, нам придется расплачиваться производительностью и добавлением метода override в базовый класс Function, т.е. для всех функций вообще (в текущем контексте ноды, или внутри песочницы/sandbox, если это все происходит внутри кода, запущенного в экранированном контексте памяти — песочнице). Вызов после этого становится изящным: this.inherited(...) или можно использовать универсальный вариант: this.inherited.apply(this, arguments), в котором не нужно подставлять все параметры по именам в вызов родительского метода.
UPD: Лучший вариант с переопределением методов
Совместно с xdenser найден самый быстрый и достаточно лаконичный вариант, который не использует super_ и не требует apply.
Сравнение производительности
Предпочтительный вариант очевиден, но все же нужно произвести измерения. Вызов метода класса предка на одном и том же оборудовании 10000000 вызовов:
Репозиторий с примерами кода и комментариями на русском и английском: https://github.com/tshemsedinov/node-inheritance
Требования к реализации:
- Использование Node.js нативного наследования util.inherits
- Определение полей и методов к классе предке и в классе наследнике
- Возможность вызова родительского конструктора из дочернего конструктора
- Возможность переопределения методов в дочернем классе
- Возможность вызова метода родительского класса из переопределенного в дочернем классе метода
Базовый пример
Имеем два класса, связанных наследованием и вызываем конструктор родительского класса из конструктора дочернего через ClassName.super_.apply(this, arguments). Естественно, этот вызов может быть как вначале дочернего, конструктора, так и его конце или в середине. Вызов может быть обернут в условие, т.е. мы полностью управляем откатом к функциональности конструктора предка.
var util = require('util');
// Определение классов
function ParentClass(par1, par2) {
this.parentField1 = par1;
this.parentField2 = par2;
}
function ChildClass(par1, par2) {
ChildClass.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) {
ChildClass.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) {
ChildClass.super_.apply(this, arguments);
this.childField1 = par1;
this.childField2 = par2;
}
// Наследование
util.inherits(ChildClass, ParentClass);
// Переопределение метода в дочернем классе
ChildClass.prototype.methodName = function(par) {
// Вызов метода родительского класса
ChildClass.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")
*/
Эта конструкция для вызова метода родительского класса конечно очень громоздка: ClassName.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) {
ChildClass.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")
*/
UPD: Лучший вариант с переопределением методов
Совместно с xdenser найден самый быстрый и достаточно лаконичный вариант, который не использует super_ и не требует apply.
var util = require('util');
// Средство для переопределения функций
function override(child, fn) {
child.prototype[fn.name] = fn;
fn.inherited = child.super_.prototype[fn.name];
}
// Конструктор родительского класса
function ParentClass(par1, par2) {
this.parentField1 = par1;
this.parentField2 = par2;
}
// Метод родительского класса
ParentClass.prototype.methodName = function(par) {
console.log('Parent method implementation: methodName("' + par + '")');
console.dir({t1:this})
this.parentField3 = par;
};
// Конструктор дочернего класса
function ChildClass(par1, par2) {
ChildClass.super_.call(this, par1, par2);
this.childField1 = par1;
this.childField2 = par2;
}
// Наследование
util.inherits(ChildClass, ParentClass);
// Переопределение метода в дочернем классе
override(ChildClass, function methodName(par) {
// Вызов метода родительского класса
methodName.inherited.call(this, par);
// Собственный функционал
console.log('Child method implementation: methodName("' + par + '")');
this.childField3 = par;
});
// Создание объекта дочернего класса
var childClassInstance = new ChildClass('Lev', 'Nikolayevich');
// Проверка результатов
childClassInstance.methodName('Tolstoy');
/* Консоль:
Parent method implementation: methodName("Tolstoy")
Child method implementation: methodName("Tolstoy")
*/
Сравнение производительности
Предпочтительный вариант очевиден, но все же нужно произвести измерения. Вызов метода класса предка на одном и том же оборудовании 10000000 вызовов:
- ClassName.super_.prototype.methodName.apply(this, arguments); 424 мс.
- Function.prototype.override(fn) и this.inherited(par); 1972 мс.
- Function.prototype.override(fn) и this.inherited.apply(this, arguments); 1800 мс.
- Последний вариант override(child, fn) и methodName.inherited.call(this, par); 338 мс.
Репозиторий с примерами кода и комментариями на русском и английском: https://github.com/tshemsedinov/node-inheritance
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какая реализация Вам больше нравится?
31.71% Лучше уж я буду наследовать как делал раньше в JavaScript39
17.89% Вариант util.inherits но без изменения Function меня устроит22
3.25% С переопределением Function выглядит лучше4
36.59% О, Аллах, за что нам этот JavaScript?45
10.57% Я счастлив с CoffeScript13
Проголосовали 123 пользователя. Воздержались 66 пользователей.