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

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`цев тут.

воскресенье, 16 марта 2008 г.

Отсрочка для разработчиков BEA

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

Напомню, что данная покупка, состоявшаяся в начале этого года (и имевшая интересную историю), поставила в первую очередь для контор, ориентирующихся на продукты BEA вопрос о том, будет ли сохранена в неприкосновенности линейка продуктов BEA Aqualogic, т.е. решения SOA и BPM от BEA.

Легко предположить, что курицу, несущую золотые яйца, а именно J2EE Application Server BEA WebLogic, Ларри вряд ли будет трогать, ибо его собственный Oracle Application J2EE server имеет заметно меньший фрагмент рынка и особого смысла его развивать, имея теперь уже в арсенале одного из лидеров, на мой взгляд, нет. Так что в этой ситуации Oracle, с сервером БД которого и так преимущественно использовался BEA WebLogic, скорее всего, предпочтёт ограничиться облегчением своим клиентам перехода на него со своего детища и постепенно свести этот проект к поддержке, постепенно сокращая затраты на неё. С WebLogic`ом же Oracle превратится в гиганта J2EE-рынка, который, за счёт консолидации, будет иметь заметно большие шансы в борьбе за пирог рынка корпоративного ПО против IBM, чем суммарно BEA и Oracle имели раньше.
Дело в том, что один из основных козырей IBM как раз и заключался в том, что они предлагают всё - СУБД (DB2), сервер приложений (WebSphere), инструментарий разработки и моделирования (продукты купленной некоторое время назад Ratonal) и железо, т.е. работая с IBM, вы обеспечивались всем, что вам может понадобиться в этой области, в то время, как sales`ы BEA при всём желании не могли использовать гибкие маркетинговые схемы, например предложить скидку на покупку СУБД Oracle при покупке у них WebLogic`а в то время, как покупка первого действительно чаще всего означала покупку второго. Так что теперь преимущество IBM будет только в железе, софтверную же часть Oracle покроет полностью и получит свободу манёвра, позволяющую почти точно так же "обволакивать" клиентов со всех сторон.

Однако, что касается решений SOA/BPM, то здесь всё выглядит соврем не так однозначно - рынок ещё только формируется и здесь позиции Aqualogic вполне сравнимы с позициями не только традиционного конкурента BEA - IBM, но и большого количества других производителей, в том числе, что критически важно - с позициями собственной линейки SOA/BPM от Oracle. Достаточно вспомнить хотя бы то, что BPM-решение от BEA было куплено и до этого являлось разработкой малоизвестной компании Fuego (однако, по утверждению сотрудников BEA, к 6-й версии они полностью с нуля переписали этот продукт). И со стороны Ларри было бы странно и тут отказываться от своих наработок в пользу решений купленной им BEA.
В итоге из трёх принципиальных схем:
  1. Оставить две линейки жить параллельной жизнью, как это, например, сделала компания Quest Software со своими продуктами для Oracle (продукты TOAD for Oracle и SQLNavigator являются прямыми конкурентами, тем не менее имеют одного владельца и активно развиваются параллельно).
  2. Отказаться от одной линейки в пользу другой.
  3. Попытаться слить линейки и команды разработчиков и превратить в единый продукт.
выбор будет производиться скорее всего из 1-й и 3-ей, и последний пункт не очень приятен для тех, кто ориентировался на решения линейки Aqualogic. Могу сказать, что хотя в ней и хватало глюков и проблем (впрочем, не больше, чем у IBM`а и прочих), но она подавала большие надежды. Насколько я понял, пока чёткого решения на сей счёт ещё не принято.

От себя могу сказать, что в общем-то не хочу класть все яйца в одну карзину и решил пока подстраховаться, овладев линейкой SOA/BPM от IBM.

В общем, пока жить можно - живём и ждём середины лета...