Слабое связывание компонентов в JavaScript. Произвольные события

HTML и CSSXSLTJavaScriptИзображенияСофтEtc
Дмитрий Филатов

31 октября 2007


Задача.

Сделать код более гибким.

Разрабатывая клиентский функционал, постоянно приходится сталкиваться с необходимостью как-то управлять взаимодействием различных компонентов между собой. Под компонентом здесь подразумеваются любые объекты — классы, виджеты, контроллеры и т. д. Решение «в лоб» — жестко программировать взаимодействие в коде компонентов. Такое решение может работать пока взаимосвязей немного, но даже в этом случае компоненты будут наделены лишним знанием о своем окружении, которым, в общем-то, они не должны обладать. Когда же таких зависимостей становится много, код быстро начинает превращаться в плохоуправляемую кашу, а о повторном его использовании в другом окружении не может быть и речи.

Например, мы запрограммировали красивую фотогалерею с превьюшками и анимацией. По нажатию на превью плавно выезжает полное изображение. Пока все хорошо — компонент галереи самодостаточен и ни от чего не зависит. Затем решаем, что в процессе выезда полного изображения должен меняться фон страницы. Добавляем код смены фона в код галереи. Первый тревожный звонок о чрезмерной связанности есть — компонент начинает делать то, что не относится к его области ответственности. Назавтра выясняется, что на одних страницах требуется менять фон, на других — нет, но нужно скрывать, допустим, навигацию и закрывать форму авторизации, если она была открыта. Если мы начинаем добавлять весь этот код в код галереи, то она становится полностью зависимой от своего окружения, которое еще и может меняться. А код галереи при этом становится похожим на сборную солянку и выполняет слишком много лишних действий. Разобраться в нем вообще становится практически невозможно.

Как же быть? Посмотрим в сторону паттернов. Нам подходят два — «Наблюдатель» (Observer) и «Посредник» (Mediator). Терминология взята из книги GoF.

Наблюдатель (Observer)

Данный паттерн предполагает две группы участников: «Наблюдатели» и «Наблюдаемые». Первые подписываются на события вторых, вторые при необходимости оповещают первых.

Немного доработав для нашей задачи, получаем следующий интерфейс Observable:


  

attachObserver(sEventType, mObserver)
detachObserver(sEventType, mObserver)
notify(sEventType)

Где sEventType — вид события, mObserver — наблюдатель, который может быть как объектом, так и callback-функцией.

Кстати, в самом javascript’е используется подобная модель с callback-функцией для добавления обработчиков событий на DOM-элементы.

Теперь каждый компонент, реализующий интерфейс Observable, может сам определять типы событий, которые в нем происходят. В случае с галереей, например, это могли бы быть такие виды событий: onInit, onStartOpen, onEndOpen, onStartClose, onEndClose. При необходимости компонент вызывает метод notify с указанием нужного типа события, при этом происходит оповещение всех наблюдателей, подписавшихся на данный вид события.

Как же одни компоненты будут подписываться на события других компонентов, при этом ничего не зная о них?

Посредник (Mediator)

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

Пример реализации паттерна «Наблюдатель»

Код использует некоторые возможности для работы с массивами из библиотеки Common.js.

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
function Observable() {

    this.aObservers = [];

}

Observable.prototype = {

    attachObserver : function(
        sEventType,
        mObserver
        ) {

        if(!(mObserver instanceof Object)) {
            return;
        }

        if(!this.aObservers[sEventType]) {
            this.aObservers[sEventType] = [];
        }

        this.aObservers[sEventType].push(mObserver);

    },

    detachObserver : function(
        sEventType,
        mObserver
        ) {

        if(this.aObservers[sEventType] && this.aObservers[sEventType].contains(mObserver)) {
            this.aObservers[sEventType].remove(mObserver);
        }

    },

    notify : function(
        sEventType
        ) {

        if(!this.aObservers[sEventType]) {
            return;
        }

        for(var i = 0, aObservers = this.aObservers[sEventType], iLength = aObservers.length; i < iLength; i++) {

            if(aObservers[i] instanceof Function) {
                aObservers[i](
                    sEventType,
                    this
                    );
            }
            else if(aObservers[i].update instanceof Function) {
                aObservers[i].update(
                    sEventType,
                    this
                    );
            }

        }

    }

};

Чтобы воспользоваться данным подходом, необходимо Observable сделать базовым классом вашего класса.

При использовании Common.js это будет выглядеть примерно так:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
function MyComponent() {

    MyComponent.baseConstructor.call(this);

    ...

}

MyComponent.EVENT_TYPE_SUPER_EVENT_A = 'superEventA';
MyComponent.EVENT_TYPE_SUPER_EVENT_B = 'superEventB';

MyComponent.inheritFrom(
    Observable,
    {

        methodA : function() {

            ...

            /* Если здесь нужно оповестить подписавшихся
                на MyComponent.EVENT_TYPE_SUPER_EVENT_A наблюдателей,
                вызываем метод notify */
            this.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_A);

            ...

        },

        methodB : function() {

            ...

            /* Если здесь нужно оповестить подписавшихся
                на MyComponent.EVENT_TYPE_SUPER_EVENT_B наблюдателей,
                вызываем метод notify */
            this.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_B);

            ...

        }


    }
    );

Если по каким-то причинам использовать наследование не получится, можно воспользоваться делегированием:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
function MyComponent() {

    ...

    this.oObservable = new Observable();

}

MyComponent.prototype = {

    attachObserver : function(
        sEventType,
        mObserver
        ) {

        this.oObservable.attachObserver(
            sEventType,
            mObserver
            );

    },

    detachObserver : function(
        sEventType,
        mObserver
        ) {

        this.oObservable.detachObserver(
            sEventType,
            mObserver
            );

    },

    methodA : function() {

        ...

        /* Если здесь нужно оповестить подписавшихся
            на MyComponent.EVENT_TYPE_SUPER_EVENT_A наблюдателей,
            вызываем метод notify объекта oObservable */
        this.oObservable.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_A);

        ...

    },

    methodB : function() {

        ...

        /* Если здесь нужно оповестить подписавшихся
            на MyComponent.EVENT_TYPE_SUPER_EVENT_B наблюдателей,
            вызываем метод notify oObservable */
        this.oObservable.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_B);

        ...

    }

};

Подписывание на события объекта MyComponent выглядит так:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
var oMyComponent = new MyComponent();

// В качестве наблюдателя выступает callback-функция
oMyComponent.attachObserver(
    MyComponent.EVENT_TYPE_SUPER_EVENT_A,
    function(oMyComponent) {

        // Событие произошло, выполняем нужную обработку

    }
    );


// В качестве наблюдателя выступает объект
oMyComponent.attachObserver(
    MyComponent.EVENT_TYPE_SUPER_EVENT_B,
    oObjectOfSomeAnotherClass
    );

/* При наступлении события MyComponent.EVENT_TYPE_SUPER_EVENT_B
    произойдет вызов метода update объекта oObjectOfSomeAnotherClass */