Pull to refresh

Мой extend и стиль наследования классов

Reading time8 min
Views14K
В данном посте хочу рассказать как предпочитаю реализовывать наследование в объемном JavaScript приложении.

Допустим для проекта необходимо множество родственных и не очень классов.
Если мы попытаемся каждый тип поведения описать в отдельном классе, то классов может стать очень много. И у финальных классов может быть с десяток предков. В таком случае обычного 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. Не ругайтесь сильно на код, моя проблема еще и в том, что я практически ни разу не показывал своего кода другим.
В личном блоге
Tags:
Hubs:
+4
Comments19

Articles