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

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

В предыдущей статье была обозначена проблема поиска совместимости инкапсуляции на основе замыканий с принципом делегирования методов объектов, созданных одним конструктором, объекту-прототипу. В качестве возможного решения была продемонстрирована модель создания специального генератора объектов (называемых "privateState") внутри прототипа, в котором находятся методы и набор закрытых полей. Каждый из порождаемых генератором объектов в этой модели содержит вложенный набор переменных (переменных объекта), соответствующих переменным прототипа, а так же двух методов:
  • "get" - присваивание значений переменных объекта - переменным прототипа,
  • "set" - присваивание значений переменных прототипа - переменным объекта.
У этой модели имеется ряд неудобств при оформлении кода и ограничений при работе с получающимися объектами, как то:
  • Необходимость в начале каждого метода вставлять вызов метода "get" и в конце - метода "set";
  • Неправильное поведение при вызове одним методом объекта других методов того же объекта;
  • Необходимость каждый раз вписывать имена переменных в методы "set" и "get";
  • Неправильное поведение при вызове одним объектом другого объекта, являющегося потомком того же класса.
Попыткам разрешить эти неудобства и снять ограничения посвящена данная и последующие статьи данного цикла. Тема данной статьи - разрешение первых двух из приведённых пунктов. Мне хотелось бы разработать максимально удобный паттерн использования инкапсуляции с ООП на основе прототипов, поэтому необходимость при написании каждого метода писать 2 строки кода - одну ("this.privateState.get();") в начале метода, вторую ("this.privateState.set();") - в конце, не может не резать мне глаз. Итак, что мы могли бы сделать, что бы сократить эти две строчки? Было бы здорово, если бы у нас была возможность поставить некоторый фильтр на вызовах всех методов прототипа, производимых через объект. Мы могли бы просто вписать перед ним вызов get, а после вызова самого метода - вызов set и - дело с концом. Но такой возможности у нас нет, поэтому нам придётся этот фильтр всё-таки вызывать из каждого метода, т.е. от одной строки - вызова. Назвать данный метод можно "methodCaller", т.к. он будет ответственным за вызовы методов. Нетрудно будет его написать:
/** @returns {Object} результат выполнения метода в контексте полей
 * данного объекта. */
function methodCaller() {
    this.privateState.get();
    var /** @type Object */ result = //вызов вызвавшей данную функции
    this.privateState.set();
    return result;
}
Как можно было бы передать этому методу имя и параметры вызова той функции, которая его вызвала, что бы он узнал, какой именно метод и с какими параметрами ему нужно будет вызвать? Можно было бы передать функцию - первым аргументом, а параметры - массивом во втором аргументе, но есть вариант по-проще - мы можем передавать ссылку "arguments". Она содержит и параметры вызова нашей функции, являясь их псевдо-массивом и ссылку на неё саму (она содержится в поле "callee"). Необходимо помнить, что при вызове функции как метода объекта нам придётся пользоваться одним из методов объекта Function - "call" или "apply", которые предоставляют контроль над ссылкой "this" внутри неё, что бы подставлять методу правильную ссылку "this", иначе она будет вести не туда, куда нужно, что может привести к неадекватной работе метода. Кроме того, следует учесть, что метод может вызывать те или иные исключения. Несмотря на это, для него в любом случае необходимо вызвать метод "set". Для того, что бы этого добиться, поместим вызов этого метода в блок finally, а вызов исконного - в блок try перед ним. Тогда мы сможем элегантно избавиться от создания специальной переменной "result", поскольку блок "finally" выполняется даже после выполнения команды return. Итак, результирующий метод:
/** @returns {Object} результат выполнения метода в контексте полей
 * данного объекта. */
function methodCaller(args) {
    this.privateState.get();
    try {
        return args.callee.apply(this, args);
    } finally {
        this.privateState.set();
    }
}
Теперь рассмотрим возможный механизм вызова данного метода:
this.getX = function() {
    if (/* если вызов происходит извне */)
        return methodCaller.call(this, arguments);
    return x;
}
Осталось только придумать, как же все наши методы могли бы узнать, извне они вызваны или изнутри - методом "methodCaller"? В принципе, у объекта Function есть ссылка на функцию, вызвавшую данную - эта ссылка находится в свойстве caller и доступна только внутри функции. Мы могли бы написать выражение так:
if (arguments.callee.caller !== methodCaller) // если вызов происходит извне
, однако, к сожалению, метод caller не входит в стандарт ECMAScript и поэтому считается устаревшим. Почти все реализации JavaScript его до сих пор поддерживают, но у нас нет никаких гарантий того, что он будет поддерживаться дальше. Так что нужен какой-то другой механизм. Вспомним, что в первой статье данного цикла мы решили об-null-ять ссылки прототипа при окончании выполнения им методов, что бы освобождать память. Таким образом, на момент вызова метода извне, когда метод privateState.get ещё не вызван, у нас все закрытые поля ссылаются на null. Т.е. мы могли бы узнать, произведён вызов метода снаружи или изнутри методом methodCaller, просто проверив любое из полей объекта на равенство null, но вот незадача - мы не можем точно знать, что поле объекта, которое выберем для этого, не окажется равным null по логике работы самого класса и, соответственно, что мы не ошибёмся в нашем выводе о том, кто вызвал метод и не вызовем бесконечный цикл, в котором метод будет вызывать methodCaller, а тот в свою очередь - опять тот же метод. Иными словами, нам нужна такая ссылка в каждом объекте, про которую у нас всегда будет известно, что она - не null. Можно было бы сделать какой-нибудь простой служебный флаг типа boolean, который был бы скрытым полем любого объекта и всегда был бы равен true, что бы по его состоянию каждый метод мог определённо сказать - применён метод privateState.get или не применён, но есть идея по-лучше: помните, я в начале статьи писал о необходимости всегда помнить о том правиле, что приватные методы нам придётся вызывать не как обычно, просто открывая за их именами круглые скобки и перечисляя параметры, а с использованием специальных методов объекта function - call или apply, поскольку нам нужно явно указывать JavaScript-интерпретатору, что в этих методах ссылка this должна вести на объект, иначе она будет вести не туда. Помните? Так вот, теперь у нас появляется возможность обойти это жёсткое правило, приняв определённое соглашение. Т.к. нам всё равно нужна некоторая флаговая переменная для того, что бы мы по ней смогли гарантированно отличить, выполнен уже для объекта метод privateStage.get() или нет, мы можем этой переменной присвоить ссылку на сам этот объект. :) Т.е. введём специальную переменную в прототипе, назовём её _this, и присвоим ей ссылку this. Можно будет из любого закрытого метода использовать её вместо обычной ссылки this и тогда станет возможно вызывать эти методы обычным образом - как функциии, без использования таких дополнительных методов объекта function, как call и apply. ;) Тогда в начале private`ных методов мы можем писать следующую строчку:
if (_this !== this) return arguments.callee.apply(_this, arguments);
и это гарантирует нам правильную работу ссылки this в них. Т.е. теперь механизм вызова методов будет выглядеть так:
function methodCaller(args) {
    this.privateState.get();
    try {
        // Обратите внимание - теперь мы можем использовать ссылку "_this",
        // которая доступна между вызовами методов объекта privateState -
        // "get()" и "set()".
        return args.callee.apply(_this, args);
    } finally {
        this.privateState.set();
    }
}
Теперь рассмотрим возможный механизм вызова данного метода:
this.getX = function() {
    if (!_this) // если вызов происходит извне
        return methodCaller.call(this, arguments);
    return x;
}
И теперь обратим внимание, что мы решили и вторую проблему из списка проблем вначале статьи - "Неправильное поведение при вызове одним методом объекта других методов того же объекта", состоявшее в том, что метод "get" объекта privateState вызывается несколько раз, а потом происходят серии вызовов методов "set". Теперь, если наш метод "getX" вызовет какой-либо другой метод своего объекта, то этот метод пройдёт проверку в начале, т.к. у него уже установлено значение переменной "_this", ведь метод "privateState.set()" у него ещё не вызывался - он вызовется только когда метод "getX" завершит работу. Подведём предварительный итог, приведя весь получившийся код:
/** @constructor
 * @author Vyacheslav Lapin<se-la-vy.blogspot.com> */
function A(x) {
    this.privateState = this.createPrivateState();
    this.setX(x);
}
A.prototype = new function() {
    var /** @type A */ _this,
        /** @type number */ x;
    /** Конструктор объекта состояния private-полей объекта. Метод предназначен
     * для вызова один раз и только из конструктора объекта. Поэтому после
     * вызова он перекрывает объекту доступ к себе, посредством установки у
     * объекта одноимённого свойства, равного null
     * @constructor
     * @return {Object} - privateState-объект для конструктора A */
    this.createPrivateState = function() {
        //Переменные для хранения private-полей объекта
        var /** @type number */ _x,
            /** @constant
             * @type A */
            __this = this;
        // Закрываем возможность вызова этого метода объектом в дальнейшем
        this.createPrivateState = null;
        return {
            get: function() {
                //Присвоение переменным прототипа значений private-полей объекта
                x = _x;
                _this = __this;
                // Об-null-ение переменной объекта на время, пока её значение
                // находится в прототипе, воизбежание возможных проблем со
                // сборкой мусора
                _x = null;
                //, а ссылку "__this" нет смысла об-null-ять - сборке мусора
                // никак не поможет, если мы на время выполнения метода запретим
                // объекту privateState ссылаться на объект, для которого он
                // создан.
            },
            set: function() {
                // Возвращение значения private-полю объекта после использования
                _x = x;
                // Возвращать значение ссылки "__this" из прототипа так же не
                // имеет смысла - по сути она - константа и с ней не должно
                // ничего происходить.
                //Об-null-ение переменных прототипа
                x =
                _this = null;
            }
        }
    }
    /** Cлужебный метод для вызова методов объекта,
     * которые используют его private-переменные. В методах, которые не
     * используют private-поля его вызывать не обязательно.
     * @param {Arguments} args */
    function methodCaller(args) {
        this.privateState.get();
        try {
            return args.callee.apply(_this, args);
        } finally {
            this.privateState.set();
        }
    }
    /** @return {number} x */
    this.getX = function() {
        if (!_this) return methodCaller.call(this, arguments);
        return x;
    }
    /** @param {number} newX Значение X для установки */
    this.setX = function(newX) {
        if (!_this) return methodCaller.call(this, arguments);
        if (typeof newX == 'number')
            x = newX;
        else
            if (typeof newX == 'object'
                && newX instanceof Number)
                x = newX.valueOf();
            else
                throw new Error(
                    'Param is not a number! Param type is ' + typeof newX
                );
    }
    /** Функция для проверки корректности вызовова методов внутри объекта */
    this.getX2 = function() {
        if (!_this) return methodCaller.call(this, arguments);
        return this.getX() * this.getX();
    }
}
if (!A.hasOwnProperty('name'))
    /** @constant
     * @type string
     * @ignore */
    A.name = 'A';
A.prototype.constructor = A;

// Тестируем:
var a1 = new A(5);
alert('a1.getX() = ' + a1.getX()); // Выводит 'a1.getX() = 5'
alert('a1.getX2() = ' + a1.getX2()); // Выводит 'a1.getX2() = 25'

alert(a1.createPrivateState); // Выводит 'null' - метод не доступен

var a2 = new A(40),
    a3 = new A(new Number(562)),
    a4 = new A(0);
alert('a2.getX() = ' + a2.getX()); // Выводит 'a2.getX() = 40'
alert('a2.getX2() = ' + a2.getX2()); // Выводит 'a2.getX2() = 1600'

alert('a3.getX() = ' + a3.getX()); // Выводит 'a3.getX() = 562'
alert('a3.getX2() = ' + a3.getX2()); // Выводит 'a3.getX2() = 315844'

alert('a4.getX() = ' + a4.getX()); // Выводит 'a4.getX() = 0'
alert('a4.getX2() = ' + a4.getX2()); // Выводит 'a4.getX2() = 0'

alert('a1.getX() = ' + a1.getX()); // Выводит 'a1.getX() = 5'
alert('a1.getX2() = ' + a1.getX2()); // Выводит 'a1.getX2() = 25'
Другие части: Часть 1: Основа Базовой модели Часть 3: Внутренние вызовы Часть 4: Наследование

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