воскресенье, 19 апреля 2009 г.

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

Продолжаю серию публикаций, направленную на усовершенствование применения инкапсуляции на основе замыканий. В первой статье, "Базовая модель", я поставил проблему несовместимости инкапсуляции на основе замыканий с реализацией ООП на основе прототипов, каковая наличествует в JavaScript и предложил т.н. "базовую модель" того, как можно было бы это обойти. Во второй статье цикла, "Механизм вызова методов", я сосредоточил внимание на упрощении вызова методов в рамках базовой модели и исправлении недочёта базовой модели в отношении использования ключевого слова this и в отношении вызовов объектом собственных методов. Данная статья посвящена окончанию разработки базовой модели. Осталась ещё не решённой проблема "внутренних вызовов", т.е. вызовов одним экземпляром объекта методов другого экземпляра этого же объекта. Итак, какая проблема возникает в базовой модели при внутреннем вызове? Давайте посмотрим по-внимательнее на методы get и set:
get: function() {
    x = _x;
    _this = __this;
    _x = null;
},
set: function() {
    _x = x;
    x =
    _this = null;
}
, а так же давайте снова взглянем на methodCaller:
function methodCaller(args) {
    this.privateState.get();
    var result = args.callee.apply(_this, args);
    this.privateState.set();
    return result;
}
Итак, что у нас получится, когда объект вызовет какой-нибудь метод другого объекта, являющегося экземпляром того же самого класса? Очевидно, у вызываемого объекта выполнится метод privateState.get(), который затрёт значения переменных вызывающего объекта и они окажутся безвозвратно утеряны. Наилучшим решением представляется модификация методов get и set так, что бы первый проверял, наполнен ли прототип в данный момент переменными какого-либо объекта или пуст перед тем, как вносить изменения в его переменные и сохранял их куда-то, а второй, после очищения прототипа проверял, были ли сохранены данные предыдущего объекта и возвращал бы в таком случае эти значения на место. Тогда сначала надо придумать, как определить - наполнен ли объект или пуст. Здесь нам опять поможет специальная переменная _this, в отношении которой мы в прошлой статье договорились, что она будет ссылаться на объект, переменными которого в данный момент наполнен прототип и очищаться в методе set, где ей будет присваиваться значение null:
get: function() {
    if (_this) //Объект уже чем-то наполнен
    //...
}
Ну хорошо, выяснили - наполнен, что делать дальше? Очевидно, нужно куда-то сохранить значения переменных, что бы потом, в методе set, присвоить их обратно. Но куда и как? Перечислять снова все переменные вручную - это уже окончательно перегрузит "базовую модель". Наверное, лучше всего просто вернуть переменные тому объекту, который их разместил, вызвав у его privateState метод set, с тем, что бы потом, в методе set вернуть их назад методом privateState.get. Примерно так:
var __instance = null;
return {
    get: function() {
        if (_this)
            (__instance = _this).privateState.set();
        x = _x;
        _this = __this;
        _x = null;
    },
    set: function() {
        _x = x;
        x =
        _this = null;
        if (__instance) {
            __instance.privateState.get();
            __instance = null;
        }
    }
}
Вроде бы, всё нормально. Но... теоретически, возможна ситуация, когда этого будет не достаточно. Представьте ситуацию, когда a1 и a2 являются объектами, созданными конструктором A и, соответственно, ссылаются на один и тот же прототип. Тогда давайте проанализируем, что же будет происходить:
  1. Какой-то метод a1 вызывает другой или тот же самый метод a2.
  2. Тогда метод a2.privateState.get() вызовет a1.privateState.set(), очищая прототип от переменных a1 и наполняя его своими переменными.
  3. После выполнения вызванного метода a2 вернёт всё обратно, вызвав в методе a2.privateState.set() метод a1.privateState.get().
  4. Метод объекта a1 продолжит выполнение, как ни в чём не бывало. В распоряжении прототипа в этот момент будут именно переменные a1.
Теперь давайте усложним ситуацию - введём дополнительно объект a3:
  1. Какой-то метод a1 вызывает другой или тот же самый метод a2.
  2. Тогда метод a2.privateState.get() вызовет a1.privateState.set(), очищая прототип от переменных a1 и наполняя его значениями своих полей.
  3. Выполняющийся в данный момент метод объекта a2 вызывает какой-то из методов объекта a3.
  4. Вызывается метод a3.privateState.get(), который вызывает метод a2.privateState.set(), который в свою очередь очищает прототип от переменных объекта a2 и (что нам вовсе не нужно) вызывает a1.privateState.get(), который прописывает в прототип значения переменных a1.
  5. После выполнения a2.privateState.set(), метод a3.privateState.get() продолжит выполняться и затрёт в прототипе все переменные объекта a1, присвоив им значения переменных a3. Объект a1 потеряет свои поля.
Таким образом, более сложная схема взаимодействия объектов, созданных одним конструктором, уже перестанет корректно работать. Решением данной проблемы представляется создание не одной переменной, а массива объектов и хранение его не в privateState, а в прототипе, как закрытую статическую переменную. В этот массив можно будет помещать объекты данного класса, которые находятся на более высоких, чем текущий, уровнях "лестницы вызова":
var /** @static
     * @type Array<Object> */
    __callInstances = [];
Тогда нам так же понадобится разделить функциональность метода privateState.set() - что бы выкатывать состояние предыдущего объекта в случае вызова этого метода methodCaller`ом и не выкатывать - в случае вызова privateState.get`ом другого экземпляра того же класса. Определить это проще всего, договорившись добавлять объект в методе privateState.get() в массив __callInstances прежде, чем вызывать у него метод privateState.set() - тогда по нахождению объекта __this в конце массива мы сможем со всей определённостью понять - вызывают метод privateState.set() из privateState.get()`а другого экземпляра того же класса (и тогда выкатывать переменные последнего объекта в массиве __callInstances не нужно) или он вызывается из methodCaller`а прототипа (и тогда, соответственно, нужно):
get: function() {
    if (_this) {
        __callInstances.push(_this);
        _this.privateState.set();
    }
    x = _x;
    _this = __this;
    _x = null;
},
set: function() {
    _x = x;
    x =
    _this = null;
    if (__callInstances.length
        && __callInstances[__callInstances.length - 1] !== __this)
        __callInstances.pop().privateState.get();
}
Так же изменения немного коснутся и механизма вызова methodCaller`а. Напомним, каким он у нас стал после выполнения действий из предыдущей статьи цикла:
if (_this) return methodCaller.call(this, arguments);
Теперь для того, что бы выполнять или не выполнять его, нам недостаточно будет знать о простом наличии в переменной _this какого-то не null`евого значения - ибо в случае вызова одним экземпляром метода другого экземпляра того же класса, у нас переменная _this будет иметь значение, но оно будет не верным, поскольку будет ссылаться на вызывающий, а не на вызываемый объект. Так что теперь нам нужно будет в этом случае осуществлять проверку на идентичность значения этого свойства переменной this, что бы понять, что переменная _this заполнена правильным значением:
if (_this !== this) return methodCaller.call(this, arguments);
Соберём всё вместе и протестируем:
/** @author Vyacheslav Lapin<se-la-vy.blogspot.com>
 * @version 0.01 08.04.2009 17:52:40
 * @constructor
 * @param {number} x */
function A(x) {
    if (this.constructor !== A)
        return new A(x);
    this.privateState = this.createPrivateState(x);
}
A.prototype = new function() {
    var /** @type A */ _this,
        /** @static
         * @type Array<A> */
        __callInstances = [];
    this.createPrivateState = function(__x) {
        //Переменные для хранения private-полей объекта
        var /** @type number */ _x = __x,
            /** @constant
             * @type A */
            __this = this;
        this.createPrivateState = null;
        return {
            get: function() {
                if (_this) {
                    __callInstances.push(_this);
                    _this.privateState.set();
                }
                x = _x;
                //...
                _this = __this;
                _x =
                //...
                null;
            },
            set: function() {
                _x = x;
                //...
                x =
                //...
                _this = 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.сall(this, arguments);
        x = _x;
        return this;
    }
    /** Метод, вызывающий себя же, но от другого объекта, переданного ему в
     * первом аргументе и передающий второй свой аргумент в качестве первого. В
     * случае, если первый аргумент не передан, выводится результат метода getX.
     * @param {A} [a1=null]
     * @param {A} [a2=null]
     * @param {A} [a3=null]
     * @return {number} */
    this.callMethod = function(a1, a2, a3) {
        if (_this !== this) return methodCaller.call(this, arguments);
        if (a1)
            return this.getX() + a1.callMethod(a2, a3);
        else
            return this.getX();
    }
}
A._className = 'A';
A.prototype.constructor = A;
//test
var /** @type A */ a1 = new A(1),
    /** @type A */ a2 = new A(2),
    /** @type A */ a3 = A(3),
    /** @type A */ a4 = A(4); //Можно вызывать и так - мы это предусмотрели в конструкторе
alert(a1.callMethod(a2, a3, a4));
Результат - "10" во всех браузерах, в которых я тестировал(IE, Opera, FF, Safari последних на данный момент стабильных версий). It works! :))))) На этом фрормирование первой версии "базовой модели" я считаю завершённым. Дальше в статьях данного цикла мы поговорим о том, как можно упростить разработку таких конструкторов с использованием инструментального средства Eclipse, а так же о том, как расширить "базовую модель", что бы применять её для наследования классов в JavaScript с сохранением преимуществ, которые даёт инкапсуляция на основе замыканий. А после этого мы поговорим о том, как расширить язык JavaScript, создав его диалект, направленный на более удобное использование базовой модели. Другие части: Часть 1: Основа Базовой модели Часть 2:Механизм вызова методов Часть 4: Наследование

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