понедельник, 20 апреля 2009 г.

JavaScript: особенности инкапсуляции на основе замыканий. Часть 4: Наследование

Существует огромное множество различных реализаций наследования в JavaScript - от кустарных приёмов до реализаций в раскрученных библиотеках типа JQuery и Prototype. Но всех их объединяет один недостаток - они не позволяют использовать инкапсуляцию на основе замыканий (и никакую другую тоже). По крайней мере мне не попадалось ни одной реализации наследования в JS, которая позволяла бы скрывать данные.

В предыдущих статьях данного цикла (1, 2, 3 части) мной была пошагово продемонстрирована разработка "базовой модели", которая позволяет реализовать структуру, предоставляющую возможность экономно создавать экземпляры классов сложных объектов, разруливая проблему совмещения инкапсуляции с ООП на основе прототипов. Конечно, само по себе решение этой проблемы довольно-таки важно, но истинную мощь "базовая модель" получит только тогда, когда будет дополнена механизмами наследования, поскольку одноуровневые объектные структуры весьма ограничены в рамках дизайна больших приложений (на которые и нацелена "базовая модель").

Итак, давайте разберёмся, что реально означает наследование одного класса от другого на уровне работы кода?
Если смотреть извне, то это означает, что любой код, нормально работающий с объектом унаследованного класса, должен продолжать нормально работать с объектом его потомка. Т.е. у объекта потомка присутствуют все методы объекта родителя и они внешне (т.е. на уровне типов возвращаемых значений, принимаемых параметров и возбуждаемых исключений) ведут себя так же.
Если же смотреть на наследование изнутри, то наследник:
  1. Должен в конструкторе первым делом вызывать конструктор класса-предка.
  2. Всегда иметь ссылку на экземпляр созданного в конструкторе класса-предка в переменной super.
  3. Некоторые public-методы могут быть переопределены и добавлены новые. То же самое в отношении public-полей.
  4. Существует возможность открывать потомкам доступ к некоторым функциям и полям (модификатор protected в Java).
  5. Только те исключения (и их потомков), которые мог вызывать переопределённый метод, может вызывать метод переопределяющий. Либо последний исключений может не вызывать вовсе.
По поводу 2 пункта замечу, что слово super в JavaScript является зарезервированным и поэтому его использовать не получится, довольствуясь лишь _super, что вобщем-то даже имеет положительную сторону - будет сразу восприниматься в качестве особой переменной.
Ну а по поводу 4 пункта, честно признаюсь, сколько ни думал о том, как это реализовать с применением замыканий, так ничего путного и не надумал. Возможно, я придумаю, как можно хитро вывернуться, всё-таки реализовав это, но пока давайте про этот пункт забудем и реализуем наследование без него, считая, что такого модификатора у нас нет.

Принципиально важным, на мой взгляд, является возможность реализации наследования так, что бы не замыкать её только на объекты, созданные в соответствии с базовой моделью, а дать возможность использовать в отношении любых конструкторов пользовательских объектов.

Итак, как нам нужно теперь модифицировать наш код, что бы стало возможным использование наследования? Для начала давайте посмотрим на схему:
Схема наследования для Расширенной модели
Мы видим, что прототип ссылается неявной ссылкой (доступной в FireFox по имени __proto__ и недоступной в других браузерах - по-этому я называл её здесь именно так) на объект-прототип класса-предка, а на экземпляр ссылается только ссылка _super.

Первое изменение коснётся механизма создания прототипа. Т.к. разработчики спецификации ECMAScript не оставили нам возможности напрямую контролировать ссылку __proto__, мы вынуждены хитрить, что бы заставить её ссылаться на то, что нам будет нужно.

В Базовой модели конструктор прототипа выглядел так:
A.prototype = new function() {/*здесь код конструктора прототипа*/}
A.prototype.constructor = A;
Таким образом, у нас создавалась безымянная (анонимная) функция и тут же использовалась в качестве конструктора нового объекта-прототипа для класса A. Затем, что бы не нарушать принятые в JavaScript договорённости относительно взаимных ссылок между конструкторами и прототипами, мы выставляли свойству constructor прототипа ссылку на класс.

Теперь же, учитывая, что мы не можем напрямую контролировать ссылку __proto__, единственная возможность для нас прописать в неё то значение, которое нам нужно, состоит в том, что бы функция, создающая объект, который должен ссылаться посредством __proto__ туда, куда нам надо, должна до создания объекта ссылаться туда свойством prototype:
function X() {}
function Y() {this.z = 5;}
var x1 = new X;
X.prototype = new Y;
var x2 = new X;
alert(x1.z); //Выведет 'undefined'
alert(x2.z); //Выведет '5';
Т.е. получается, что функция, создающая для нас прототип, теперь не сможет быть безымянной. А как не хочется засорять пространство имён ещё одной переменной... По-этому я предлагаю немного схитрить - присвоить прототипу сначала эту функцию, а потом, произведя все нужные изменения, присвоить ему уже её результат. Выглядеть это будет вот так:
A.prototype = function() {/*...*/}
A.prototype.prototype = B.prototype; // B - конструктор, от которого мы наследуем A
A.prototype = new A.prototype;
A.prototype.constructor = A;
На выходе получим прототип, ссылающийся ссылкой __proto__ на прототип конструктора-предка.

Теперь осталось разобраться со ссылкой _super. Она будет обладать похожим поведением на ссылку _this - так же будет переменной, имеющий двойника - переменную __super, являющуюся полем объекта. Так же, как и ссылку __this, её не будет смысла очищать в методе privateState.get() и, соответственно, наполнять в privateState.set(). Так же она будет константой для пользователя.
Кроме того, она будет присваиваться вызову конструктора-предка вначале метода pcreatePivateState и в нём же из неё будут извлекаться все свойства и присваиваться ссылке __this - если они, конечно, не будут переопределены в классе-потомке.

Теперь соберём всё вместе и протестируем. Вот, какая получается расширенная модель:
function B() {
    var y = 15;

    this.getY = function(){
        return y;
    }

    this.setY = function(_y) {
        y = _y;
        return this;
    }
}

/**
* @author Vyacheslav Lapin
* @version 0.01 09.04.2009 21:10:04
*
* @constructor
* @extends B
*
*
* @param {number} x +
*/
function A(x) {

    if (this.constructor !== arguments.callee)
        return new arguments.callee(x);

    this.privateState = this.createPrivateState(x);
}

A.prototype = function() {

    var /** @type {A} */ _this,
        /** @type {B} */ _super,

        /**
         * @static
         * @type {Array<A>}
         */
         __callInstances = [];

    this.createPrivateState = function(__x) {

        //Переменные для хранения private-полей объекта
        var /** @type {number} */ _x = __x,

            /**
             * @constant
             * @type {A}
             */
            __this = this,

            /**
             * @constant
             * @type {B}
             */
            __super = new B(); // Вызываем конструктор класса-предка

        this.createPrivateState = null;

        // Отдельный функциональный блок для того, что бы не заводить в
        // closure ещё одно поле - переменную i
        (function() {
            //Здесь мы перечисляем свойства экземпляра класса-предка
            for (var /** @type {string} */ i in __super)
                // Проверяем, собственное это свойство экземпляра super-класса
                // или унаследованное? Ведь унаследованные свойства нам не нужны -
                // мы и так их унаследуем по цепочке прототипов, если они не будут
                // переопределены в нашем прототипе, а тогда - унаследуем
                // переопределённые, что нас вполне устроит.
                if (__super.hasOwnProperty(i)
                    // Так же проверяем, есть ли такие же в прототипе
                    && !this.constructor.prototype.hasOwnProperty(i)
                    // или в самом объекте?
                    && !this.hasOwnProperty(i)
                )
                    try {
                        this[i] = __super[i];
                    } catch (/** @type {Error} */ e) { }
        })();

        return {
            get: function() {

                if (_this) {
                    __callInstances.push(_this);
                    _this.privateState.set();
                }

                x = _x;
                //...
                _this = __this;
                _super = __super;

                _x =
                //...
                null;
            },

            set: function() {

                _x = x;
                //...

                x =
                //...
                _this =
                _super = null;

                if (__callInstances.length
                    && __callInstances[__callInstances.length - 1] !== __this)

                    __callInstances.pop().privateState.get();
            }
        }
    }

    /**
     * @param {Arguments} args +
     */
    function methodCaller(args) {
        this.privateState.get();
        var result = args.callee.apply(_this, args);
        this.privateState.set();
        return result;
    }

    //--------------------------------------------------------------------------

    var /** @type {number} */ x;
    //...

    /**
     * @return {number}
     */
    this.getX = function() {

        if (_this !== this) return methodCaller.call(this, arguments);

        return x;
    }

    /**
     * @param {number} _x
     * @return {A} this
     */
    this.setX = function(_x) {

        if (_this !== this) return methodCaller.call(this, arguments);

        x = _x;

        return this;
    }

    /**
     * @override
     */
    this.setY = function(_y) {

        if (_this !== this) return methodCaller.call(this, arguments);

        alert('Hello from A!');
        _super.setY(_y);
       
        return this;
    }

}

/** @type {string} */ A._className = "A";
A.prototype.prototype = B.prototype;
A.prototype = new A.prototype;
A.prototype.constructor = A;

// Импортируем в конструктор-потомок так же статические поля
// конструктора-предка- свойства класса
(function() {
    for (var /** @type{string} */ i in B)
        if (!(i in A))
            try {
                A[i] = B[i];
            } catch (/** @type {Error} */ e) { }
})();

//test
var a = new A(10);
alert(a.getY()); //Выводит '15'
alert(a.setY(20).getX()); // Выводит сначала 'Hello from A!', затем 10
alert(a.getY()); //Выводит '20'
Вот и всё. Можно теперь делать любые сложные деревья наследования в JavaScript в полном соответствии с принципами инкапсуляции.

В дальнейших статьях цикла мы поговорим о том, как упростить разработку конструкторов по базовой и расширенной модели, поскольку у меня нет иллюзий о том, что вручную писать такой код не слишком-то удобно.

P.S. Напоследок хочу предостеречь вас от попытки на радостях наследовать от системных конструкторов - к сожалению, с ними может не пройти выражение:
__this[i] = __super[i];
Дело в том, что они не являются в полном смысле JavaScript`овыми объектами и для них такой возможности часто не предусмотрено. Например, мне не удалось наследовать от XMLHttpRequest - там это выражение вылетало с ошибкой даже при том, что я обромил его блоком try/catch:
try {__this[i] = __super[i];} catch(/** @type {Error} */ e) {}
Всё равно, даже такой бронебойный код вываливался с ошибкой в браузере. По крайней мере в FF это дело у меня не прошло. Так что ещё раз повторюсь - расширенная модель предназначена для наследования от родных JavaScript-конструкторов объектов. Будьте осторожны!

Другие части:
Часть 1: Основа Базовой модели
Часть 2:Механизм вызова методов
Часть 3: Внутренние вызовы

Комментариев нет: