Краткая заметка про наследование в 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

Какая реализация Вам больше нравится?

Проголосовало 8 человек. Воздержалось 6 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

ссылка на оригинал статьи http://habrahabr.ru/post/247325/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *