В попередній статті я описав дизайн патерни в загальному. В даній статті розглянемо 3 популярних шаблони: Модуль, Фасад, Медіатор. Хоч модуль і не входить в список дизайн патернів, він є дуже корисним і необхідним для вивчення.

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

Патерни потрібно застосовувати у великих JS додатках. Але що саме ми маємо на увазі коли говоримо про великі JS-додатки? Великими можна назвати додатки які вирішують нетривіальні задачі, а підтримка таких програм потребує від розробника серйозних зусиль. При цьому, більша частина роботи по маніпуляції даними і їх відображенням лягає на браузер.

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

Більшість JS-розробників в архітектурі своїх додатків зазвичай використовують різні комбінації наступних компонентів:

  • віджет;
  • моделі;
  • представлення (візуалізація);
  • контролери;
  • шаблони;
  • бібліотеки;
  • ядро додатку.

Можливо, ви виносите різні функції ваших додатків у різні модулі, або використовуєте якісь інші шаблони проектування для подібного розділення. Це дуже добре, але тут є ряд потенційних проблем, з якими ви можете зіткнутися при такому підході:

  1. Чи готова ваша архітектура до повторного використання коду прямо зараз?
  2. Скільки модулів в системі залежить один від одного?
  3. Чи зможе ваш додаток працювати далі, якщо його окрема частина зламається?
  4. Наскільки легко ви зможете протестувати окремі модулі?

В майбутньому, ви можете прийняти рішення замінити Dojo, jQuery, Zepto або YUI на щось конкретно інше. Причиною такого переходу може стати швидкодія, безпека або дизайн. Це може стати серйозною проблемою, тому що бібліотеки не передбачають простої заміни. Ціна заміни бібліотеки буде високою, якщо ваш додаток тісно з нею пов’язаний.

Ближче до суті

Ми хочемо отримати слабозв’язану архітектуру з функціональністю, розділену на незалежні модулі, які, в ідеалі, не повинні мати залежності один від одного. Коли трапляється щось цікаве, модулі повідомляють про це іншим частинам додатку, а проміжний шар інтерпретує їхні повідомлення і необхідним чином реагує на них.

Використовуємо окремий шар для обробки повідомлень модулів щоб:

а) Модулі не взаємодіяли напряму з ядром;

б) Модулі не взаємодіяли напряму один з одним.

Це допомагає не допустити падіння додатку через різні помилки в середині одного з модулів. Також це дозволяє нам перезапускати модулі якщо вони раптом перестали працювати через якусь помилку.

Ще один момент – безпека. Насправді, не багато з нас задумуються про внутрішню безпеку своїх додатків. Коли ми визначаємо структуру додатку, ми говоримо собі, що ми достатньо розумні для того щоб розуміти що в нашому коді повинно бути публічним а що приватним.

Архітектура яку ми плануємо розглянути являє собою комбінацію трьох відомих шаблонів проектування: модуль, фасад і медіатор.

Теорія модулів

Модуль – це цільна частина будь-якої хорошої архітектури додатку. Зазвичай модуль виконує одну конкретну задачу в більш масштабній системі і може бути взаємозамінним.

В даній архітектурі модулі мають дуже обмеженні знання про те що відбувається в інших частинах додатку. Замість цього ми делегуємо відповідальність медіатору і фасаду.

В цьому і полягає ідея даної архітектури – якщо модуль турбується виключно про те щоб повідомити систему про події що цікавлять його і не хвилюється чи запущені інші модулі, то система може додавати, видаляти і заміняти одні модулі, не ламаючи при цьому інші. Це було б неможливо при сильно зв’язаній архітектурі.

В JavaScript є декілька способів реалізації модулів, включно шаблон “Модуль” і Object Literal (літературний запис об’єкту var obj = {};).

Шаблон “Модуль”

“Модуль” – це популярна реалізація шаблону, який інкапсулює в себе приватну інформацію, стан і архітектуру, використовуючи замикання. Це дозволяє обгортати публічні і приватні методи і змінні в модулі, і запобігати їхньому проникненню в глобальний контекст, де вони можуть конфліктувати з інтерфейсами інших розробників. Шаблон “Модуль” повертає лише публічну частину API,  залишають все інше доступним лише в середині замикання.

Завдяки замиканню, оголошенні всередині модуля змінні і методи доступні лише з середини цього модуля. Змінні і методи, оголошені всередині, які повертаються модулем, будуть доступні для всіх.

Розглянемо реалізацію модуля на чистому JS:

var basketModule = (function() {
  var basket = []; // приватна змінна
    return { // методи доступні на зовні
        addItem: function(values) {
            basket.push(values);
        },
        getItemCount: function() {
            return basket.length;
        },
        getTotal: function() {
           var q = this.getItemCount(),p=0;
            while(q--){
                p+= basket[q].price; 
            }
            return p;
        }
    }
}());

Всередині модуля, як ви помітили, ми повертаємо об’єкт. Цей об’єкт автоматично присвоюється змінній basketModule, так що з ним можна взаємодіяти таким чином:

// basketModule - це об'єк з властивастями, які можуть бути також і методами:
basketModule.addItem({item:'bread', price:0.5});
basketModule.addItem({item:'butter', price:0.3});

console.log(basketModule.getItemCount());
console.log(basketModule.getTotal());

// А цей код працювати не буде:
console.log(basketModule.basket); // undefined тому що не входить в повертаємий об'єкт
console.log(basket); // масив доступний лише з замикання

Літературна нотація об’єктів

Літерал об’єкта не вимагає використання оператора new для створення екземпляра, але він не повинен стояти на початку виразу, так як відкрита { може бути сприйнята як початок блоку. Нижче ви можете побачити приклад модуля, визначеного за допомогою літеральної нотації об’єкта. Нові члени об’єкта можуть бути додані за допомогою конструкції myModule.property = 'someValue';

Шаблон «модуль» може бути корисний для багатьох речей. Але якщо ви вважаєте, що вам не потрібно робити приватними деякі методи або властивості, то літерал об’єкта – більш ніж підходящий вибір.

var myModule = {
    myProperty: 'someValue',
    myConfig: {
        useCaching: true,
        language: 'en'
    },
    myMethod: function() {
        console.log('I can haz functionality?');
    },
    myMethod2: function() {
        console.log('Caching is: ' + ((this.myConfig.useCaching) ? 'enabled' : 'disabled'));
    },
    myMethod3: function(newConfig) {
        if (typeof newConfig == 'object') {
            this.myConfig = newConfig;
            console.log(this.myConfig.language); 
        }
    }
};

myModule.myMethod();  // 'I can haz functionality'
myModule.myMethod2(); // Вивід 'enabled'
myModule.myMethod3({language:'fr',useCaching:false}); // 'fr'

Модулі CommonJS

Можливо, ви щось чули про CommonJS за останні пару років. CommonJS – це добровільна робоча група, яка проектує, прототипує і стандартизує різні JavaScript API. На сьогоднішній день вони ратифікували стандарти для модулів і пакетів – CommonJS визначають простий API для написання модулів, які можуть бути використані в браузері за допомогою тега <script>, як з синхронним, так і з асинхронним завантаженням.

У структурному плані, CommonJS-модуль являє собою готовий до повторного використання фрагмент JavaScript-коду, який експортує спеціальні об’єкти, доступні для використання в будь-якому залежному коді.

Шаблон “Фасад”

Ключову роль в даній архітектурі грає шаблон проектування під назвою «фасад».

Як правило, фасад використовується для створення деякої абстракції, що приховує за собою зовсім іншу реальність. Шаблон «фасад» забезпечує зручний високорівневий інтерфейс для великих блоків коду, приховуючи за собою їхню справжню складність. Ставтеся до фасаду, як до спрощеного API, який ви віддаєте в користування іншим розробникам.

Фасад – структурний патерн. Часто його можна виявити в JavaScript-бібліотеках і фреймворках, де користувачам доступний тільки фасад – обмежена абстракція широкого діапазону поведінки реалізованої всередині.

Причина, по якій нам цікавий фасад – можливість приховати деталі реалізації конкретної функціональності, що зберігаються в модулях. Це дозволить нам вносити зміни в реалізацію, не повідомляючи про це користувачам.

var module = (function() {
  var _private = {
    i: 5,
    get: function() {
      console.log('Поточне значення:' + this.i);
    },
    set: function(val) {
      this.i = val;
    },
    run: function() {
      console.log('процес запущений');
    },
    jump: function() {
      console.log('різка зміна');
    }
  };
  return {
    facade: function(args) {
      _private.set(args.val);
      _private.get();
      if (args.run) {
        _private.run();
      }
    }
  }
}());

module.facade({run:true, val:10}); // Поточне значення: 10, процес запущений

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

Шаблон “Медіатор”

Пояснити, що являє собою патерн «медіатор» досить просто на прикладі наступної аналогії – уявіть собі контроль трафіку в аеропорту: всі рішення про те, які літаки можуть злітати або сідати, приймає диспетчер. Для цього, всі повідомлення, які виходять від літаків, надходять в вежу управління, замість того, щоб пересилатися між літаками безпосередньо. Такий централізований контролер – це і є ключ до успіху даної системи. Це і є «медіатор».

Медіатор застосовується в системах, де взаємодія між модулями може бути досить складною, але, в той же час, добре визначеною. Якщо ви вважаєте, що зв’язок між модулями вашої системи буде постійно рости і ускладнюватися, то, можливо, вам варто додати центральний елемент управління. Патерн «медіатор» відмінно підходить для цієї ролі.

Медіатор виступає в якості посередника в спілкуванні між різними модулями, інкапсулюючи їхню взаємодію. Крім того, цей шаблон проектування, запобігає прямій взаємодії різних компонентів системи, сприяє ослабленню зв’язків в коді. У даній системі він також допомагає у вирішенні проблем, пов’язаних з залежностями модулів.

Ми можемо розглядати модулі, як «видавців», що публікують події. Медіатор ж є і «видавцем» і «підписником» одночасно. У прикладі, Module 1 посилає повідомлення, що очікуює деяку реакцію медіатора. Потім, медіатор, отримавши повідомлення, повідомляє інші модулі про певні дії, які необхідно виконати для завершення завдання. Module 2 виконує необхідні Module 1 дії, і повідомляє про результат назад, в медіатор. В цей же час, медіатор запускає Module 3 для логування вхідних повідомлень.

Переваги:

  • Зменшує зв’язування модулів, додаючи посередника – центральний елемент управління;
  • Завдяки слабкій зв’язаності коду, впровадження нової функціональності відбувається значно легше.

Недоліки:

  • Модулі більше не можуть взаємодіяти безпосередньо. Використання медіатора призводить до невеликого падіння продуктивності – така природа слабкої зв’язаність.

Приклад:

var mediator = (function() {
    var subscribe = function(channel, fn) {
        if (!mediator.channels[channel]) mediator.channels[channel] = [];
        mediator.channels[channel].push({ context: this, callback: fn });
        return this;
    },
 
    publish = function(channel) {
        if (!mediator.channels[channel]) return false;
        var args = Array.prototype.slice.call(arguments, 1);
        for (var i = 0, l = mediator.channels[channel].length; i < l; i++) {
            var subscription = mediator.channels[channel][i];
            subscription.callback.apply(subscription.context, args);
        }
        return this;
    };
 
    return {
        channels: {},
        publish: publish,
        subscribe: subscribe,
        installTo: function(obj) {
            obj.subscribe = subscribe;
            obj.publish = publish;
        }
    };

}());

Приклади використання:

//Pub/sub on a centralized mediator

mediator.name = "tim";
mediator.subscribe('nameChange', function(arg) {
    console.log(this.name);
    this.name = arg;
    console.log(this.name);
});

mediator.publish('nameChange', 'david'); //tim, david


//Pub/sub via third party mediator

var obj = {name: 'sam'};
mediator.installTo(obj);
obj.subscribe('nameChange', function(arg) {
    console.log(this.name);
    this.name = arg;
    console.log(this.name);
});

obj.publish('nameChange', 'john'); //sam, john

Закругляємось

Фасад, в даній архітектурі, виконує роль абстракції ядра програми. Фасад знаходиться між медіатором і модулями. Модулі в ідеальній ситуації повинні взаємодіяти виключно з фасадом і не повинні знати абсолютно нічого про інші компоненти системи.

Фасад також забезпечує послідовний і доступний в будь-який момент інтерфейс для модулів.

Медіатор є інтерпретацією патерну «підписник / видавець (pub/sub)», але він отримує тільки ті повідомлення, які нас дійсно цікавлять. За фільтрацію же всіх повідомлень відповідає фасад.

Основне завдання ядра (медіатора) – управляти життєвим циклом модулів. Коли ядро ​​отримує цікаве повідомлення від модулів, воно повинно визначити, як додаток має на це відреагувати, таким чином ядро ​​визначає момент запуску або зупинки певного модуля або набору модулів.

В ідеальній ситуації, одного разу запущений модуль, повинен функціонувати самостійно. До завдань ядра не входить прийняття рішень про те, як реагувати, наприклад, на подію DOM ready – в даній архітектурі у модулів є досить можливостей для того, щоб приймати такі рішення самостійно.

В доповнення, ядро має бути в змозі додавати або видаляти модулі не ламаючи нічого.

Схожі статті