В данном посте хочу рассказать как предпочитаю реализовывать наследование в объемном JavaScript приложении.
Допустим для проекта необходимо множество родственных и не очень классов.
Если мы попытаемся каждый тип поведения описать в отдельном классе, то классов может стать очень много. И у финальных классов может быть с десяток предков. В таком случае обычного JavaScript наследования через prototype может оказаться не достаточно. Например мне понадобилась возможность из метода вызывать аналогичный метод класса-предка. И захотелось создавать и наследовать некоторые статические свойства и методы класса. Такую функциональность можно добавить, вызывая для каждого класса ниже изложенную ф-ию extend:
Создаем 3 класса, каждый — наследник предидущего.
А теперь узнаем, в каком порядке выполняются их действия.
В результате ежедневной практики, у меня примерно такая структура объектов:
Сейчас объясню почему именно так.
Для создания класса необходимо вызывать конструктор, тот конструктор, что вызывается при new Foo() — не вызывается при создании объектов-потомков данного класса.
Вот например:
А мне хочется, чтобы все объекты, наследники класса Foo имели уникальный id и предупреждали пользователя, что умеют взрываться.
Для реализации этого — я создаю специальный метод cnstruct (constructor — уже занято), и выполняю его при создании каждого объекта. Чтобы не забыть его выполнять, отказываюсь от создания объектов через new Foo() и создаю объекты через статический метод Foo.stat.create().
Далее представлена укороченная версия реально используемого класса, как пример того, какими получаются классы.
Данный класс необходимо рассматривать как один из многих в цепочке прототипов от базового класса к финальному (скорее в обратную сторону).
Добавлю, что такой подход стоит использовать именно для классов объектов, а для «одиноких» объектов (например cSO.localStorage) стоит использовать традиционную фабрику объектов.
P.S. Понимаю, что большинство концепций программирования и скорость исполнения при таком подходе сильно страдают.
Так же понимаю, что такой стиль не нов, и наверняка существуют другие, более подходящие (спасибо если укажете их).
P.S. Не ругайтесь сильно на код, моя проблема еще и в том, что я практически ни разу не показывал своего кода другим.
В личном блоге
Допустим для проекта необходимо множество родственных и не очень классов.
Если мы попытаемся каждый тип поведения описать в отдельном классе, то классов может стать очень много. И у финальных классов может быть с десяток предков. В таком случае обычного JavaScript наследования через prototype может оказаться не достаточно. Например мне понадобилась возможность из метода вызывать аналогичный метод класса-предка. И захотелось создавать и наследовать некоторые статические свойства и методы класса. Такую функциональность можно добавить, вызывая для каждого класса ниже изложенную ф-ию extend:
Функция extend
cSO = {}; // Просто для отдельного пространства имен.
cSO.extend = function(child, parent, other) {
if(parent) {
var F = function() {};
F.prototype = parent.prototype;
child.prototype = new F();
child.prototype.constructor = child;
child.prototype.proto = function() {
return parent.prototype;
}
// Пока все стандартно.
} else {
child.prototype.proto = function() {
return;
}
}
/*
* У классов есть параметр stat, предназначенный для статических ф-ий и данных.
* Он доступен через _class.stat или из объекта(экземпляра) класса через this.stat.
* Потомки могут обращаться к статическому методу предка, для этого их нужно
* объявлять так: _class.stat.prototype.myStaticMethod = function() {...<anchor>habracut</anchor>
* Можно запретить наследовать метод, объявляя его без prototype.
* Все данные в stat доступны наследникам, если не перекрываются другими данными.
* Для удаления переменной из stat - используем _class.stat.deleteStatVal("name");
*/
child.statConstructor = function() {};
if(parent && ("statConstructor" in parent) && parent.statConstructor && typeof (parent.statConstructor) === "function") {
var S = function() {};
S.prototype = parent.statConstructor.prototype;
child.statConstructor.prototype = new S();
child.statConstructor.prototype.constructor = child.statConstructor;
child.statConstructor.prototype.proto = function() {
return parent.statConstructor.prototype;
}
child.statConstructor.prototype.protoStat = function() {
return parent.stat;
}
} else {
child.statConstructor.prototype.proto = function() {
return;
}
child.statConstructor.prototype.protoStat = function() {
return;
}
}
var oldChildStat = child.stat; // если в stat что-то уже добавляли...
child.stat = new child.statConstructor();
if(oldChildStat) { // не забываем перенести старые методы и свойства.
for(var k in oldChildStat) {
child.stat[k] = oldChildStat[k];
}
}
child.stat.prototype = child.statConstructor.prototype;
if(oldChildStat && oldChildStat.prototype) {
// не забываем перенести старые методы и свойства для прототипа.
for(var k in oldChildStat.prototype) {
child.stat.prototype[k] = oldChildStat.prototype[k];
}
}
child.prototype.stat = child.stat;
if(other) {
// Выполняем условленные действия по дополнительным параметрам.
if(other.statConstruct) {
child.stat.prototype.construct = other.statConstruct;
}
}
child.stat._class = child; // чтобы ссылаться на класс из статических методов.
child.stat.deleteStatVal = function(name) {
// Удаляет переменную так, чтобы после ссылаться на переменную предка.
if( name in child.stat) {
try {
delete child.stat[name];
} catch(e) {
}
if(parent) {
child.stat[name] = child.stat.protoStat()[name];
}
}
}
child.prototype.protoFunc = child.statConstructor.prototype.protoFunc = function(callerFuncName, args, applyFuncName) {
/*
* Позволяет вызвать функцию более ранней версии в иерархии прототипов (Правильное имя
* вызывающей ф-ии - необходимо передать в первом параметре). Если установленна
* переменная applyFuncName - вместо callerFuncName будет вызываться другая ф-ия но из
* прототипа на один уровень старше, чем прототип обладающий вызывающей ф-ией.
*/
if(!args) {
args = [];
}
if(applyFuncName) {
// Пока не стал заморачиваться, решил отложить на лучшие времена.
} else {
applyFuncName = callerFuncName;
}
var tProto = this;
var ok = false;
do {
if(ok && arguments.callee.caller !== tProto[applyFuncName]) {
if(( applyFuncName in tProto) && ( typeof (tProto[applyFuncName]) === "function")) {
return tProto[applyFuncName].apply(this, args);
}
} else if(arguments.callee.caller === tProto[callerFuncName]) {
ok = true;
}
} while(("proto" in tProto) && (tProto = tProto.proto()))
return;
}
if(child.stat.construct) {
// Вызывается при создании класса или создании потомка без stat.construct
child.stat.construct();
}
}
Небольшая проверка как работает, и какие результаты выдает ф-ия extend()
Создаем 3 класса, каждый — наследник предидущего.
cSO.class001 = function() {
}
cSO.extend(cSO.class001, 0, {"statConstruct":function(sc1) {
console.log("statConstruct001");
}}); // выведет statConstruct001
cSO.class001.prototype.construct = function(c1) {
console.log('c1');
this.protoFunc("construct", arguments);
}
cSO.class001.stat.prototype.st = function(s1) {
console.log('st1');
this.protoFunc("st");
}
cSO.class001.stat.dat = ["hello1"];
cSO.class002 = function() {
}
cSO.extend(cSO.class002, cSO.class001, {"statConstruct":function(sc2) {
console.log("statConstruct002");
this.protoFunc("construct", arguments);
}}); // выведет statConstruct002 statConstruct001
cSO.class002.prototype.construct = function(c2) {
console.log('c2');
this.protoFunc("construct", arguments);
}
cSO.class002.stat.st = function(s2) {
console.log('st2');
this.protoFunc("st");
}
cSO.class003 = function() {
}
cSO.extend(cSO.class003, cSO.class002); // выведет statConstruct002 statConstruct001
cSO.class003.prototype.construct = function(c3) {
console.log('c3');
this.protoFunc("construct", arguments);
}
cSO.class003.stat.prototype.st = function(s3) {
console.log('st3');
this.protoFunc("st");
}
cSO.class003.stat.dat = ["hello3"];
А теперь узнаем, в каком порядке выполняются их действия.
var obj001 = new cSO.class001();
var obj002 = new cSO.class002();
var obj003 = new cSO.class003();
obj001.construct(); // c1
obj002.construct(); // c2 c1
obj003.construct(); // c3 c2 c1
cSO.class001.stat.st(); // st1
cSO.class002.stat.st(); // st2 st1
cSO.class003.stat.st(); // st3 st1 // потому что объявили cSO.class002.stat.st без prototype
console.log(obj003.stat.dat); // ["hello3"]
obj002.stat.dat = ["world"];
console.log(obj002.stat.dat); // ["world"]
cSO.class002.stat.deleteStatVal("dat");
console.log(obj002.stat.dat); // ["hello1"]
console.log(obj001.stat.dat); // ["hello1"]
Еще несколько штрихов, которые мне показались важными
В результате ежедневной практики, у меня примерно такая структура объектов:
_class={
construct:function(){}, // Вызываем при создании каждого объекта-наследника.
destruct:function(){}, // Вызываем при удалении любого объекта-наследника.
// и т.д.
stat:{
create:function(){}, // Вызывается при создании класса или потомка класса.
collection:[], // В некоторых классах удобно журналировать все созданные экземпляры.
clearAll:function(){} // Иногда удобно иметь возможность удалить всю коллекцию.
// и т.д.
}
}
Сейчас объясню почему именно так.
Для создания класса необходимо вызывать конструктор, тот конструктор, что вызывается при new Foo() — не вызывается при создании объектов-потомков данного класса.
Вот например:
var id = 0;
var Foo = function() {
this.id = id++;
console.log("Вы создали объект, имеющий метод boom");
}
foo.prototype.boom = function() {}
var Bar = function() {
}
Bar.prototype = new Foo();
var fooOb = new Foo(); // Вы создали объект, имеющий метод boom
var barOb = new Bar();
var barOb2 = new Bar();
console.log(fooOb.id); // 1
console.log(barOb.id); // 0
console.log(barOb2.id); // 0
А мне хочется, чтобы все объекты, наследники класса Foo имели уникальный id и предупреждали пользователя, что умеют взрываться.
Для реализации этого — я создаю специальный метод cnstruct (constructor — уже занято), и выполняю его при создании каждого объекта. Чтобы не забыть его выполнять, отказываюсь от создания объектов через new Foo() и создаю объекты через статический метод Foo.stat.create().
Далее представлена укороченная версия реально используемого класса, как пример того, какими получаются классы.
Реальный пример
Данный класс необходимо рассматривать как один из многих в цепочке прототипов от базового класса к финальному (скорее в обратную сторону).
(function() {
var _class = cSO.LocalStorageSyncDataType = function () {
/*
* Вообще класс описывает поведение объектов, которые
* сохраняются и загружаются с жесткого диска клиента.
* Но это только часть реализации, остальное вырезал.
*/
}
cSO.extend(_class, cSO.ServerSyncDataType, {"statConstruct": function() {
this.protoFunc("construct", arguments); // Если конструктор объявлен, но в нем не вызывается protoFunc - цепочка предидущих конструкторов обрывается.
if("addToClassesForSnapshot" in this) { // Условие не удовлетворяется для cSO.LocalStorageSyncDataType, который по идее абстрактен, а только для его потомков.
this.addToClassesForSnapshot(this._class); // Все потомки по умолчанию будут регистрироваться.
}
}});
var _class_stat = _class.stat; // Такими присвоениями - позволяем минимизатору (компилятору) уменьшать размер скриптов на 15%-25% ежели без присвоений. Обычно устанавливаю подсветку этих слов как констант.
var _class_stat_prototype = _class_stat.prototype;
var _class_prototype = _class.prototype;
var cfs = _class_stat.classesForSnapshot = [];
_class_stat.create = function(args) {
// Метод написан просто для примера, такой метод д.б. в финальных классах.
this.addedToLocalStorage = false;
if(args.addedToLocalStorage) {
this.addedToLocalStorage = true;
}
this.protoFunc("construct", arguments);
}
_class_prototype.construct = function(args) {
/*
* Конструктор достраивает созданный объект, но автоматически не вызывается.
* Он добавляет достоинства концепции фабрики объектов в данный стиль.
*/
this.addedToLocalStorage = false;
if(args.addedToLocalStorage) {
this.addedToLocalStorage = true;
}
this.protoFunc("construct", arguments);
}
_class_prototype.setLoaded = function(val) {
this.protoFunc("setLoaded", arguments);
// знаю, что здесь будет код, но пока не знаю какой именно.
}
_class_stat.addToClassesForSnapshot = function(clas) {
clas = clas || this._class;
for(var i = 0; i < cfs.length; i++) {
if(cfs[i] === clas) return;
}
cfs.push(clas);
}
_class_stat.createAllSnapshots = function() {
for(var i = 0; i < cfs.length; i++) {
cfs[i].stat.createSnapshot();
}
}
_class_stat_prototype.createSnapshot = function() {
var co = this.collection;
var str = "";
for(var i in co) {
if(co[i]) {
if(!str) {
str = "[";
} else {
str += ",";
}
str += co[i].getJSON();
}
}
if(str) str += "]";
this.snapshot = str;
}
_class_stat.saveAllSnapshotsOnLocalStorage = function() {
for(var i = 0; i < cfs.length; i++) {
cfs[i].stat.saveSnapshotOnLocalStorage();
}
}
_class_stat_prototype.saveSnapshotOnLocalStorage = function() {
if(this.snapshot) {
cSO.localStorage.setItem(this.tableName, this.snapshot);
}
}
_class_stat.setAllBySnapshotsFromLocalStorage = function() {
for(var i = 0; i < cfs.length; i++) {
cfs[i].stat.setBySnapshotFromLocalStorage();
}
}
_class_stat_prototype.setBySnapshotFromLocalStorage = function() {
var arr = $.parseJSON(cSO.localStorage.getItem(this.tableName));
for(var i = 0; i < arr.length; i++) {
if(arr[i]) {
this.createOrGet({"cells":arr[i], "addedToLocalStorage":true});
}
}
}
})();
Добавлю, что такой подход стоит использовать именно для классов объектов, а для «одиноких» объектов (например cSO.localStorage) стоит использовать традиционную фабрику объектов.
P.S. Понимаю, что большинство концепций программирования и скорость исполнения при таком подходе сильно страдают.
Так же понимаю, что такой стиль не нов, и наверняка существуют другие, более подходящие (спасибо если укажете их).
P.S. Не ругайтесь сильно на код, моя проблема еще и в том, что я практически ни разу не показывал своего кода другим.
В личном блоге