воскресенье, 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: Наследование

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

Когда-то Дуглас Крокфорд впервые заметил, что на основе замыканий (closures) можно имитировать инкапсуляцию примерно так:
function A() {
    var /* number */ x = 5;
    this.getX = function() { return x;}
    this.setX = function(newX) {
        if (typeof x == 'number')
            x = newX;
        else
            throw new Error('Param is not a number!');
    }
}

var y = new A();
alert(y.getX()); // Вернёт 5
alert(y.x); // Вернёт 'undefined'
Вроде бы, выглядит логично, не правда ли? Вот только есть одна проблема, на которую в своё время обратил моё внимание участник форума Vingrad с ником AKS: каждый объект содержит весь набор методов своего класса, и если объектов будет достаточно много, то этот метод приведёт к очень неэкономному расходованию памяти.

JavaScript относится к языкам с ООП, основанным на прототипах (prototype-based OOP). В данном случае это значит, что у объектов с большим количеством методов, что бы сэкономить память, рекомендуется эти методы выносить в общий для них прототип. Это легко сделать, отказавшись от инкапсуляции и сделав эти поля открытыми, что бы методы прототипа каждый раз могли их считывать в зависимости от того, у какого объекта они вызваны. Но как добиться этого, не отказываясь от инкапсуляции для этих полей?

Т.е. существует противоречие между инкапсуляцией на основе замыканий и принципами ООП на базе прототипов.

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


Итак, наиболее удобным контейнером для методов класса является, конечно, его прототип (в нашем случае - A.prototype). На него ссылаются все объекты данного класса и, если вызываемый метод не находится в них, то следует вызов метода из их прототипа. Так что задача состоит в том, что бы позволить методам прототипа работать с закрытыми полями объектов, из которых эти методы вызываются.

Объект-прототип является чужим для того замыкания, в котором находятся поля. Он связан с ним неявной ссылкой (в FF она носит название "__proto__"), но она не поможет ему считать закрытые переменные. Так что же делать?

С другой стороны, объекту, который не содержит методов для работы со своими закрытыми полями, иметь эти поля и необязательно. Что он будет с ними делать, кроме как передавать и извлекать их из прототипа, если всё равно все методы для работы с ними находятся в прототипе? Ничего. Т.е. на самом деле они ему не нужны - ему нужен только персональный механизм, позволяющий загружать свой набор закрытых полей в прототип и затем сохранять их из прототипа.

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

Тогда какими должны быть требования к этому вспомогательному замыканию? Оно должно уметь делать две вещи:
  • Устанавливать в закрытые переменные прототипа значения закрытых переменных объекта;
  • Записывать в закрытые переменные объекта значения закрытых переменных прототипа (после того, как прототип произвёл с ними некоторые действия).
Т.к. одного действия нам не достаточно, это не может быть функция - это должен быть объект с двумя методами:
  • метод "get" выполняет взятие значений переменных из объекта, при этом самим переменным объекта неплохо было бы присвоить значение null, что бы не иметь дублирующихся ссылок;
  • метод "set" устанавливает значения обратно в объект, об-null-яя аналогичные переменные в прототипе.
Сам этот объект назовём "privateState", поскольку он несёт закрытую часть состояния объекта.

Итак, что нам в итоге нужно:
  • метод для генерации внутреннего замыкания в прототипе объекта, который будет возвращать объект с двумя методами: get и set, которые, соответственно, будут устанавливать в прототип значения закрытых полей из этого объекта и в объект - из прототипа;
  • механизм вызова для методов прототипа, гарантирующий, что перед выполнением будут установлены значения закрытых полей.
Можно было бы, конечно, просто сделать внутри прототипа массив, который содержал бы privateState-объекты, а каждый объект просто содержал бы некоторый идентификатор, по которому прототип узнавал бы, с каким множеством значений ему нужно в данный момент работать. Но в таком случае мы не сможем рассчитывать на сборку мусора этих полей - т.к. ссылка содержится в прототипе, а в объекте - лишь идентификатор, то при уничтожении объекта у нас не будет возможности узнать о том, что следует удалить и принадлежащее ему множество закрытых полей (его privateState-объект) - и они останутся в памяти при том, что использоваться не будут. В итоге мы получим проблему с утечкой памяти. Поэтому единственная ссылка на privateState-объект должна быть у объекта, которому она принадлежит, прототип ссылаться на неё не должен.

Итак, метод генерации privateState-объекта внутри прототипа может выглядеть примерно так:
// Переменная прототипа, в которую будем помещать значение private-поля
// объекта каждый раз, когда будем выполнять какой-либо его метод.
var x;

// Функция, являющаяся конструктором provateState-объекта
this.createPrivateState = function() {

    //"_x" - переменная для хранения private-поля объекта
    var _x;

    return {
        get: function() {
            //Присвоение переменной прототипа значения private-поля объекта
            x = _x;

            // Об-null-ение переменной объекта на время, пока её значение
            // находится в прототипе, во избежание дублирующейся информации
            _x = null;
        },

        set: function() {
            // Возвращение значения private-полю объекта после использования
            _x = x;

            //Об-null-ение переменной прототипа
            x = null;
        }
    }
}
При этом нам нужно так же продумать механизм вызова методов. В наиболее простейшем варианте мы могли бы сделать это так:
this.method1 = function() {
    this.privateState.get();

    //здесь располагается код метода, работающего с private-полями...

    this.privateState.set();
}
Тогда в конструкторе объекта мы могли бы просто вызывать метод createPrivateState() прототипа:
function A() {
    this.privateState = this.createPrivateState();
}

A.prototype = new function() {
    var x;
    this.createPrivateState = function(){...} //Описан выше
    this.method1 = function() {...} //Описан выше
    //Другие методы...
}

A._className = "A"; //Записываем в специальное свойство класса его имя
A.prototype.constructor = A; //Присваиваем прототипу ссылку на конструктор
Однако для промышленного применения этот метод не годится по ряду причин. Во-первых, мы не учли ситуации, когда один метод вызывает другой метод того же объекта (что делается довольно часто), во-вторых мы не учли, что в методе может происходить вызов метода другого объекта этого же класса. И, наконец, механизм вызова, состоящий из двух строчек вначале и в конце объявления метода выглядит излишне-громоздким. Тому, как наиболее элегантно справиться с этими и более мелкими проблемами, будут посвящены следующие статьи цикла "Особенности инкапсуляции на основе замыканий". :)

Другие части:
Часть 2: Механизм вызова методов.
Часть 3: Внутренние вызовы.
Часть 4: Наследование.

JavaScript: Дополняем объект Date

На мой взгляд, объекту Date в JavaScript не хватает небольшого количества констант для более удобной работы с ним. Вот как можно все их создать для него:
// Adding consts for comfortable work with Date constructor.
/** @type {number}*/ Date.MS_PER_YEAR = 365 * (
    /** @type {number} */ Date.MS_PER_DAY = 24 * (
        /** @type {number} */ Date.MS_PER_HOUR = 60 * (
            /** @type {number} */ Date.MS_PER_MINUTE = 60 * (
                /** @type {number} */ Date.MS_PER_SECOND = 1000
            )
        )
    )
);

/** @type {number} */ Date.MS_PER_LEAP_YEAR =
                            Date.MS_PER_YEAR + Date.MS_PER_DAY;
LEAP YEAR - это високосный год.
Т.к. с датой чаще всего бывает удобнее всего работать как с числом, возвращаемым методом getTime() и передавать число в конструкторе, то эти константы часто помогают производить более сложные операции.

Update №1 (18.01.2010):
Учитывая, что дату и время в объекте Date можно менять, периодически возникает задача вернуть объект Date, на который ориентируется внутренняя структура какого-либо объекта и который в нём инкапсулирован (при помощи замыканий) и изменения в котором могут негативно отразиться на логике его работы. Для таких случаев в Java предназначен метод clone. Он возвращает точную копию объекта. Вот какой могла бы быть его реализация:
Date.prototype.clone = function() {
    return new Date(this.getTime());
};

JavaScript: небольшое отличие function от function

Некоторое время назад я писал на форуме, что не вижу разницы между следующими конструкциями:
function fnName() {
//...
}
, и
var fnName = function () {...}
, но на самом деле разница есть. Состоит она в том, что в первом случае функция определяется на этапе синтаксического анализа, а во втором - в момент выполнения. Что бы её продемонстрировать, давайте посмотрим на следующий пример:
x(); // Выводит "1", хотя функция ещё вроде бы не определена...
var x = 2;
alert(typeof x); // Выводит "number"
// Следующая строка выполнилась ещё до того, как выполнилась первая строка данного
// примера, так что сейчас она ничего не меняет.
function x(){alert(1);}
alert(typeof x); // Опять выводит "number"
var x = function(){alert(3);}
x(); // Выводит "3". Функция переопределена.

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

JavaScript: используйте arguments.callee вместо названия функции

Часто разработчики, когда им нужно изнутри функции сослаться на саму эту функцию, используют её имя, заявленное при её объявлении. Например, так:
var factorial = function(x){
    return x == 1 ? 1 : x + factorial(--x);
}
В данном коде мы, фактически, обращаемся к внешней цепочке областей видимости и читаем из неё переменную "factorial" и, надеясь, что это - функция, вызываем её.

Проблема заключается в том, что JavaScript - язык, в высшей степени чреватый конфликтами имён в случае, если над разработкой одного и того же функционала работает не один, а несколько человек. И в этой ситуации нельзя быть до конца уверенным, что переменной "factorial" в какой-то момент кто-нибудь не решит присвоить другую функцию или даже какое-нибудь значение типа числа, при этом функция может быть, скажем, записана в другую переменную и по этой причине разработчик, использующий эту функцию, может думать, что всё впорядке и функция должна работать:
var fac = factorial;
factorial = 27;
alert(fac(factorial));

Но функция выдаст ошибку:
factorial is not a function
factorial(26)test.js (line 2)
Просто нужно помнить, что функции в JavaScript - это такой особый тип данных, а не неизменяемая часть программы, как, например, в Java. Функцию можно присваивать любой переменной, свойству объекта или элементу массива, жонглируя ей как угодно. Эта возможность фактически означает, что у функций в JavaScript нет своих постоянных имён! Поэтому для того, что бы не лишать программистов этой возможности, лучше всегда изнутри функции ссылаться на неё с помощью встроенного и поддерживаемого свойства псевдо-массива Arguments, называемого "callee". Это свойство всегда ссылается на функцию, которая выполняется в данный момент, поэтому вы оказываетесь застрахованы от недоразумений, продемонстрированных выше:
function factorial(x) {
    return x == 1 ? 1 : x + arguments.callee(--x);
}
var fac = factorial;
factorial = 27;
alert(fac(factorial)); // Возвращает "378".
Но в JavaScript всё-таки есть способ элегантно обойти данную ситуацию. Если вам не очень нравится прибегать к вызову свойства callee объекта Arguments, то можно воспользоваться конструкцией, называемой "литералом функции" с необязательным параметром, содержащим имя функции:
var factorial = function f(x) {
    return x == 1 ? 1 : x + f(--x);
};
alert(f); // Ошибка: "f is not defined"
В этом коде не создаётся внешней переменной "f", здесь функция, как и в предыдущем примере, присваивается только внешней переменной "factorial", а переменная "f" определяется только внутри области видимости функции и служит для ссылки на данную функцию, так же, как и "arguments.callee", так что можно без опаски использовать переменную "f" для своих нужд - на работу данной функции это никак не повлияет.

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

среда, 14 мая 2008 г.

Программная обработка исключительных ситуаций в JavaScript

Зачем JS-приграммисту может понадобиться обрабатывать ошибки в сценариях? В принципе, не существует задач, при решении которых без механизма исключений никак нельзя было бы обойтись. Однако часто их использование оказывается заметно удобнее бесконечных if`ов - поскольку в коде мы с их помощью изолируем себя от некоторых ситуаций, в которых программа должна вести себя по-другому (главным образом, это может быть связано с неадекватной работой пользователя, например, при вводе данных) - и сосредотачиваемся на описании алгоритма в случае нужных нам условий, а уже потом отдельно разбираться, как говорится, с косяками. Это как в жизни - мы часто отодвигаем проблемы на потом, просто помним, что они у нас есть, и когда будет время - мы их решим, в противовес if`овому подходу, когда мы всё время должны решать проблемы реакции на неадекватные зигзаги сценария не отходя от кассы. По себе могу сказать, что писать сложный код так намного удобней - и более того, по мере усложнения кода использование исключений становится всё более и более необходимым. А если забыть обработать какое-то исключение, заявленное при написании "чистого" кода - браузер об этом скорее всего напомнит при тестировании.

Для тех, кто не знаком с механизмом исключений в Java


Механизм исключений в JavaScript очень похож на механизм исключений в Java, так что если вы знакомы с последним, можете спокойно пропустить этот раздел и перейти к чтению следующего - в этом рассматриваются азы, общие для этих двух механизмов.

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

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


Для тех, кто знаком с механизмом исключений в Java


Здесь я кратко приведу различия между реализацией исключений в Java и JavaScript - пропустите этот раздел если с Java вы не знакомы.

Главное отличие от Java-реализации состоит в отсутствии (в силу не классовой, а прототипной реализации ООП) возможности использовать множественный оператор catch, основанный на наследовании и восходящем преобразовании. Здесь оператор catch может быть только один для каждого try`я. Этот приём можно менее изящно реализовать в обработчике на основе оператора switch.

Блок finally присутствует, но здесь он нужен ещё реже. Сам я, работая с JavaScript, ни разу не сталкивался с ситуацией, где бы он мне понадобился или был удобен - поэтому здесь я о нём тихонько умолчу, только намекнув, что он, в принципе, здесь есть...

Ну и, конечно, нет великолепной возможности (отсутствующей даже в C#) Java, обязывающей указывать возможность вызова исключения функцией при её описании с помощью ключевого слова throws, отсутствие какового сулит обернуться большой путаницей при использовании больших сторонних библиотек...

Объект исключения


Обычно в качестве объекта исключения выступает объект Error, хотя, строго говоря, конструкция throw может передавать абсолютно любой объект (!), который может быть принят оператором catch и соответственно обработан.

К сожалению, и область обработки исключительных ситуаций задела браузерная война - боевые действия в этой области привели к тому, что объекты Error в ECMAScript и в JScript практически не имеют ничего общего.

Какую информацию о состоянии может в себе нести исключение? По большому счёту всего 2 поля - некое название, характеризующие тип исключения, и сообщение, уточняющее особенности конкретного исключения в данном случае - если пользователь возбуждает исключение сам, то он придумывает сообщение сам, если его генерирует система - то уже она решает, какое сообщение писать. Прибавим к этим 2-м свойствам 3-е - prototype, для возможности изменять его характеристики ОО-средствами и 4-е стандартное toString() - и получим объект исключения от ECMAScript, в котором название называется "name" (в случае пользовательских объектов оно содержит строку "Error", а в случае ошибки браузера - название из небольшого списка, представленного в следующей таблице), а строка сообщения - свойство "message". Конструктор исключения от ECMAScript выглядит так:
new Error(message);
У Microsoft вместо имени у ошибки появляется составной номер (свойство "number"), состоящий из кода источника ошибки (facility code) и номера самой ошибки. Что бы выделить номер источника, можно воспользоваться выражением
(e.number >> 16) & 0x1FFF
, а что бы выделить код самой ошибки - выражением
e.number & 0xFFFF
, где e - объект Error), и "description", полностью аналогичное свойству "message" в ECMAScript (начиная с IE 5.5 значение свойства "description" доступно и через свойство "message", т.е. по существу, они - синонимы, ссылающиеся на одно и то же значение). Соответствие номеров описаниям для JScript можно найти в этой таблице.
Конструктор объекта исключения от Microsoft IE выглядит так:
new Error(errorNumber, message);
- этот формат не совместим с приведённым ранее форматом, соответственно, к сожалению, для различных браузеров придётся писать разные возбуждения исключений...

Впрочем, популярный особенно в Linux-среде open-source`ный браузер Mozilla так же нередко расширяет стандарты (хотя существующие - поддерживает, в отличае от IE). В объекте Error для этого браузера добавлено 2 интересных свойства для локализации исключения - "fileName" и "lineNumber", хранящие, соответственно, информацию о файле, где произошла ошибка (актуально, когда в проекте много *.js - файлов и непонятно в котором из них ошибка) и о номере строки в этом файле.

Пример использования


Теперь давайте перейдём к практике. Для отлова исключений используется блок try..catch, выглядит это примерно так:
try {
    оператор1
} catch (исключение) {
    оператор2
}
, где: оператор1 - оператор или их группа, где может быть вызвано исключение, оператор2 - обработчик исключения, исключение - обычно объект Error, характеризующий исключение и передаваемый оператору2.

Отметим, что оператор1 может включать вызовы функций - если в них возникнут исключения и там не окажется более глубоко вложенных блоков try..catch, внутри которых оно будет вызвано, то их поймает именно этот catch.

Теперь о самостоятельном вызове с помощью оператора throw. Как и в других темах, существует 2 способа писать универсальный код:
  • Различать браузеры и писать разный код для разных браузеров, и
  • Писать более простой код, который относится к области пересечения браузеров.
Я нахожу более мудрым второй способ и по возможности стараюсь использовать его. В данном случае это означает, что лучше отказаться от объекта Error из-за разного синтаксиса конструкторов и присваивать исключению простую строку:
function getMonthName(month) {
    month--; // Переводим month в индекс массива (1=январь, 12=декабрь)
    var months=["январь","февраль","март","апрель","май","июнь","июль",
"август","сентябрь","октябрь","ноябрь","декабрь"];
    if (months[month] != null)
        return months[month];
    else
        throw "Неверный месяц";
}

try {
    monthName = getMonthName(myMonth); // возможно исключение
} catch (e) {
    monthName="неизвестно";
}
document.write(monthName);
Ссылки по теме для более глубокого изучения:
1) Ю.С. Лукач, "Справочник Web-разработчика", раздел "Обработка исключений";
2) Степанищев Евгений: Блоки try... catch... finally... в JScript 5. Статья на сайте CitForum.ru

P.S. Это одна из ранних моих статей, опубликованных на форуме программистов "Vingrad". Ознакомиться с комментариями Vingrad`цев можно тут.

понедельник, 21 апреля 2008 г.

Java: ParamsBean - альтернативный механизм вызова методов

1. Каждый может принять решение, располагая достаточной информацией
2. Хороший руководитель принимает решение и при её нехватке
3. Идеальный - действует в абсолютном неведении


Законы исходных данных Спенсера
из "Законов Мёрфи"


Считается, что ООП гораздо ближе к реальному миру, к модели его восприятия человеком, нежели предшествующие подходы. Действительно, мы можем создать объект "собака" и реализовать методы "голос", "сидеть", "лежать" и "фас" - и их вызов будет по смыслу отдалённо напоминать поведение человека по отношению к собаке на прогулке. Всё вроде бы логично, да не совсем - чем на самом деле отличается окружающий мир от программы, так это обилием различных факторов. Собака получает команду "фас" не в отрыве от всей остальной реальности, а в условиях окружения множеством различных второстепенных факторов окружающей действительности и в совокупности эти факторы имеют порой гораздо большее влияние на реакцию собаки на нашу команду, чем сама эта команда. Очевидно, с методом объекта Dog этого не будет - он обработает лишь те параметры, которые ему передадут, коими может стать, пожалуй, лишь громкость голоса, измеренная в децибелах. Он может так же учесть параметры внутреннего состояния, инкапсулированные в нём, но не данные окружающей его среды.

Рассмотрим другой пример. Специалист в какой-то области архитектурно предстаёт перед нами как ресурс, способный справиться с каким-то классом задач (соответствующих данной предметной области). Когда возникает задача, специалист сначала выясняет более узкий тип задачи, затем входит в курс дела, погружаясь в специфику задачи, затем более узко - в специфику конкретной ситуации, в которой возникла проблема, затем ищет похожие случаи в своей практике и решает задачу либо на основе опыта (архитектурно - кэша уже выполненных задач или не выполненных, про которые сделаны выводы) либо, в отсутствии такового - пытается решить творчески.


Среда, в которой действуют объекты реальной действительности, обычно имеет фоновый характер и лишь косвенно влияет на процесс. Она состоит из множества разнообразных факторов. Соответственно, было бы логично передать её некоторым количеством переменных, редко изменяемых и имеющих значения по умолчанию для типичных ситуаций. Однако небольшой, ограниченный ряд свойств среды в контексте данного конкретного действия (работы метода) имеет первостепенную важность - очевидно, что эти-то свойства в меру возможностей мы и должны в первую очередь определять, передавая методу, остальные же, менее существенные можем считать равными значениям поумолчанию, если у нас нет достаточных ресурсов, что бы их уточнять.


Брюс Эккель писал в "Философии Java" о том, что разработчиков удобно разделять на создателей и пользователей библиотек. Архитектурно задача программиста-пользователя - как можно точнее определить внешнюю среду (одним из ключевых факторов которой является само посылаемое объекту сообщение, т.е. вызываемый метод) объекта, которую и передать объекту, а задача программиста-разработчика библиотек - определить внутреннюю структуру объекта, что бы он как можно более адекватно работал в условиях, которые показывает ему программист-пользователь.


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

Т.е. для моделирования более реальных ситуаций, нам необходимо иметь возможность писать методы, которые имеют не обязательные параметры. Позволяют ли стандартные принятые подходы такое делать? Да, но очень не удобно, не эффективно и не гибко.

Прикладные аспекты описания сигнатур методов или чем нам может помочь подход ParamsBean?


Обычно когда мы планируем архитектуру приложения, мы закладываем в сигнатуры методов объектов в библиотеках какие-то параметры, которые играют для выполняемой задачи ключевую роль. Им необходимо их обработать и выдать приемлемый результат. Эти параметры имеют свои типы, имена и жёстко определённый порядок перечисления при вызове метода. Для того, что бы корректно работать с определённым таким образом методом, программист, который будет использовать нашу библиотеку, обязан считаться с этим.

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

Однако у этого метода есть ряд существенных недостатков, главным образом - он, очевидно, обладает очень низкой масштабируемостью. Он привносит элементы т.н. жесткого связывания обоих этих программистов по Эккелю (автора библиотек и клиента), а хорошо бы их разнести, что бы они не были вынуждены плотно общаться - особенно это может оказаться важно в больших проектах. Делать же перегруженные методы на все случаи жизни, очевидно, не разумно - количество возможных комбинаций растёт в геометрической прогрессии пропорционально количеству параметров.

Как-то была у меня такая задача - надо было вытащить данные из базы и передать их в функцию, в которой очень-очень много параметров (около 30-ти). Реально нужны из них только 5, остальные - значения по умолчанию, но было известно, что этот метод понадобится ещё для реализации большого числа других функций и так же понадобится модифицировать некоторые из параметров вызова, какие - заранее сказать было сложно. Создавать конструктор с 30ю параметрами было не просто очень неудобно - было нужно иметь возможность задать некоторые параметры, а остальные что бы остались такими, какими должны быть по умолчанию.

Создавать конструкторы для всех возможных комбинаций, естественно, я не стал.

Я создал JavaBean (точнее именно POJO - обрезанный лишь getter`ами и settter`ами JavaBean), в котором инкапсулировались все параметры по умолчанию, и его через промежуточную функцию стал передавать той самой с огромным числом параметров.

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

Есть множество аспектов языков, о которых не все знают, поскольку на них не принято акцентировать внимание. Так, недавно я открыл для себя, что оператор вызова метода - "." - не обязательно должен сразу следовать за тем объектом, за которым он вызывается, как это обычно принято делать, а может вызываться и на другой строке - главное, что бы метод был рядом, т.е., кто не знает, конструкция:
Obj1 theObj1 = new Obj1();

theObj1
    .setProp1(1)
    .setProp2(2);
- вполне valid`ная. Такая конструкция навешивания изменений свойств была бы идеальна для моей задачи и очень лаконична - если бы я при вызове метода создавал объект и передавал ему те параметры, которые отличаются от параметров по умолчанию, все же остальные оставались бы со значениями по умолчанию.

И вот я подумал - а почему принято setter`ы делать без возвращаемого значения, т.е. "void"? А может быть, стоит заставить их выдавать ссылку на объект своего класса, в конце прописывая выражение "return this;"? Тогда приведённая выше конструкция будет вполне реальной!

Сделал и всё отлично получилось! :-))) Выполнил задачу и остался очень собой доволен.
class A implements Serializable {

    private int forumID = 1;
    public int getForumID(){ return forumID; }
    public A setForumID(int forumID){ this.forumID = forumID; return this; }

    private int memberID = 8;
    public int getMemberID(){ return memberID; }
    public A setMemberID(int memberID){ this.memberID = memberID; return this; }

    private String postBody = "";
    public String getPostBody(){ return postBody; }
    public A setPostBody(String postBody){ this.postBody = postBody; return this; }

    // и т.д.
}

public class B {

    public static void main(String[] args){

        ClassToGo.go( new A()); //Можем вызывать со всеми параметрами по-умолчанию
   
        // Можем задавать все параметры
        ClassToGo.go(
            new A()
                .setForumID(5)
                .setMemberID(2)
                .setPostBody("ляляля")
        );
   
        // Можем выборочно
        ClassToGo.go(
            new A()
                .setPostBody("боди поста")
        );
    }
}

Пока назвал я это ParamsBean - по-моему, очень удобная штука. :)

Преимущества


Теперь я могу каждый раз при вызове менять значения только тех параметров, которые мне нужно изменить по сравнению с параметрами по умолчанию. Кроме того, я всегда знаю их имена и не путаю их, ведь я вызываю специальные методы, содержащие их имена. Так же теперь я жестко не привязан к порядку их следования при вызове метода - порядок отныне произволен. Могу теперь и пользоваться преимуществами модели JavaBeans для параметров метода, т.е. вводить ограничения на данные и преобразовывать их для хранения в другой формат, вообще, реализуя причудливую логику, инкапсулировать в бине часть функций по обработке данных - например, можно задать методу, которому нужна в качестве параметра длина, ParamsBean, в разных setter`ах которого можно задавать длину в метрах, милях, футах или дюймах, а он уж с методом сам как-нибудь разберётся - это уже вешается на составителя библиотеки, а не на клиента.

Кроме того, в сеттерах-геттерах можно инкапсулировать логику прямого или косвенного влияния одних факторов на другие, таким образом, пользователь такого бина получает возможность задавать одним методом несколько факторов.

Это возможность распараллелить работу программистов, выполняющих разные роли по Эккелю. Например, они договорились о каком-то методе, которому в качестве параметров передаётся объект "pBean", метод реализует какую-то функциональность. Потом понадобилось изменить метод, передавая ему дополнительные параметры, который в зависимости от них будет менять поведение - для того, что бы старый код работал, просто в этот pBean добавляется новое поле и оно передаётся методу - не надо перегружать метод. Зато программист-пользователь сможет гибче использовать метод и не заморачиваться с теми параметрами, которые ему не известны, при этом не должен будет клянчить каждый раз у разработчика библиотеки специальный перегруженный метод для его ситуации. Т.е. фактически данный подход - это как бы конструктор всех возможных индивидуальных перегрузок метода.

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

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


P.S. Это сокращённый вариант моей статьи на Vingrad`е. Можно ознакомиться с коментариями Vingrad`цев тут.