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

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