Статьи

Шаблоны модульного дизайна: частные, привилегированные и защищенные участники в JavaScript

В этой статье я опишу структуру и преимущества расширенного шаблона модульного проектирования, который включает в себя четыре основных типа элементов :

  • public : участники, к которым можно получить доступ из любого места
  • private : члены, к которым можно получить доступ только изнутри объекта
  • привилегированный : члены, к которым можно получить прямой доступ только изнутри объекта, но к которым можно получить косвенный доступ извне через открытый метод
  • protected : члены, к которым можно получить доступ только изнутри объекта или любого из его модулей.

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

Для получения дополнительной информации о разнице между объектно-ориентированным и объектно-ориентированным программированием и введении в объектно-ориентированное программирование на JavaScript я бы порекомендовал статью Райана Фришберга: объектно-ориентированное программирование на JavaScript .

Модульные шаблоны дизайна

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

Базовая модель модуля

Классический шаблон модуля оборачивает весь код сценария в литерал анонимной функции, который при возврате присваивается одной переменной. Функциональный литерал — это просто функция, которая вызывает себя, поэтому конечное назначение функционального литерала — это не функция, а любое значение, которое возвращает функция.

В качестве простого примера, функциональный литерал включает в себя вычисление, и поэтому окончательное значение, назначенное sum является результатом этого вычисления:

 var sum = (function() { return 6 * 7; })(); 

Чтобы проиллюстрировать, что означает этот синтаксис, вот тот же код, абстрагированный в именованную функцию:

 function calculate() { return 6 * 7; } var sum = (calculate)(); 

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

Публичные и частные члены

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

Вот пример:

 var MyModule = (function() { var myPrivateData = 303; function myPrivateFunction() { alert('private'); } return { myPublicData : 42, myPublicFunction : function() { alert('public'); } }; })(); 

Так как мы возвратили объект свойств, и он назначен MyModule , к свойствам можно обращаться извне объекта как MyModule.myPublicData и MyModule.myPublicFunction . Но мы не можем получить доступ к myPrivateData или myPrivateFunction вообще, потому что переменные доступны только в их исходной области видимости. Область действия переменной — это контекст, в котором она определена, определяется с помощью оператора var . В этом примере область действия приватных переменных — это объект MyModule , и поэтому к ним можно получить доступ только из этого объекта.

Выявление шаблона модуля

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

Давайте еще раз посмотрим на этот объект, но на этот раз, используя шаблон модуля:

 var MyModule = (function() { var myPrivateData = 303; function myPrivateFunction() { alert('private'); } var myPublicData = 42; function myPublicFunction() { alert('public'); } return { myPublicData : myPublicData, myPublicFunction : myPublicFunction }; })(); 

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

Этот паттерн был обновлен Кристианом Хейлманном, который дает превосходное объяснение этого и паттерна модуля, на котором он основан, в своей статье: « Снова с паттерном модуля — открой что-то миру» .

Преимущества различного синтаксиса

Иногда, однако, может помочь разница в синтаксисе и стиле кодирования, просто чтобы напомнить себе и другим, какие члены являются публичными или частными. Синтаксис, который вы используете, может быть своего рода самодокументированием, чтобы указать, например, что функции, объявленные с использованием синтаксиса function foo , всегда будут частными.

Тип синтаксиса, который вы используете для функции, также влияет на тип синтаксиса, который вы можете использовать внутри нее. В приведенном ниже примере использование синтаксиса this.foo для публичной функции означает, что он может использовать тот же синтаксис для ссылки на другие общедоступные свойства (если они также определены с этим синтаксисом). И если все открытые члены определены с тем же синтаксисом, то все, что вам в конечном итоге нужно будет вернуть, this :

 var MyModule = (function() { var myPrivateData = 303; function myPrivateFunction() { alert('private'); } this.myPublicData = 42; this.myPublicFunction = function() { alert(this.myPublicData); } return this; })(); 

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

Добавление привилегированных членов

Хотя к закрытым членам можно получить доступ только изнутри объекта, публичные члены, которые ссылаются на них, также могут сделать их публичными. Например, открытая функция может вернуть значение закрытой переменной, например так:

 var MyModule = (function() { var myPrivateData = 303; this.myPublicFunction = function() { return myPrivateData; } return this; })(); alert(MyModule.myPublicFunction()); //alerts 303 

Когда открытая функция разрешает доступ к закрытым данным извне объекта, это называется привилегированным . На языке строгого программирования привилегированна сама функция, но лично я считаю гораздо более полезным думать о переменных, к которым они обращаются, как о привилегированных; они разные.

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

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

 var MyLibrary = (function() { var config = { resolution : 10 }; this.define = function(key, value) { if(typeof config[key] == 'undefined') { alert('There is no config option "' + key + '"'); } else { if(isNaN(value = parseInt(value, 10))) { alert('The value defined for "' + key + '" is not a number'); } else { config[key] = value; } } }; return this; })(); MyLibrary.define('fail', 20); //alerts the first failure MyLibrary.define('resolution', 'fail'); //alerts the second failure MyLibrary.define('resolution', 20); //resolution is now 20 

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

Создание дополнительных модулей

Теперь мы создали шаблон дизайна с публичными, частными и привилегированными участниками. Но помните, что мы называем его шаблоном модуля , поэтому он должен иметь возможность иметь дополнительные модули в одном и том же контексте объекта (т. Е. Совместно использовать одни и те же общедоступные данные и иметь одинаковую концепцию this ).

Есть несколько способов достижения этого, но я предпочитаю использовать apply() . Метод apply позволяет вам указать контекст объекта, в котором оценивается функция, эффективно переопределяя значение this . Поэтому, чтобы связать дополнительные модули с контекстом MyModule , мы просто модифицируем синтаксис функции-литерала, чтобы передать его через apply :

 var MyModule = (function() { this.version = '1.0'; return this; })(); var MyModule = (function() { this.getVersion = function() { return this.version; }; return this; }).apply(MyModule); alert(MyModule.getVersion()); //alerts "1.0" 

Связывание дополнительных модулей таким способом иногда называют дополнением . Вы также можете услышать, что это описывается как строгое увеличение или произвольное увеличение — где строгое увеличение означает, что модули должны загружаться в синхронном порядке , в отличие от произвольного увеличения, когда они могут загружаться в любом порядке . (Обычные теги <script> загружают свое содержимое в синхронном порядке источников, тогда как динамически генерируемые сценарии, которые добавляются позже, загружаются асинхронно.)

По моему опыту, приложение редко может работать с произвольным дополнением. Модули неизбежно зависят друг от друга и поэтому должны загружаться в определенном порядке. Наши примеры все такие.

Добавление защищенных участников

Разделение скрипта на несколько модулей является обычной и удобной практикой. Это значительно облегчает управление большой кодовой базой и позволяет экономить полосу пропускания, когда модули не всегда требуются.

Но что, если мы хотим обмениваться данными между различными модулями? Если мы сделаем эти данные общедоступными, мы потеряем преимущества конфиденциальности, но если мы сделаем их конфиденциальными, они будут доступны только одному модулю. Что нам действительно нужно, так это общие частные пользователи , и они известны как защищенные .

JavaScript не имеет защищенных членов как таковых, но мы можем эффективно создавать их, временно делая данные общедоступными . Чтобы достичь этого, позвольте мне сначала познакомить вас с двумя ключевыми функциями — extend и privatise — которые мы определим как часть объекта служебных функций:

 var utils = { extend : function(root, props) { for(var key in props) { if(props.hasOwnProperty(key)) { root[key] = props[key]; } } return root; }, privatise : function(root, prop) { var data = root[prop]; try { delete root[prop]; } catch(ex) { root[prop] = null; } return data; } }; 

Функция расширения просто добавляет новые свойства к объекту, а функция приватизации копирует свойство, а затем удаляет оригинал. Мы можем использовать extend в одном модуле, чтобы создать общедоступную ссылку на частную переменную, а затем использовать privatise в другом модуле, чтобы скопировать ее обратно в частную переменную и удалить общедоступную ссылку.

Итак, вот пример первого модуля, который имеет два защищенных члена (включая сам объект utils ) и один открытый член. Для краткости примера кода служебные функции — это просто пустые оболочки, но они будут идентичны функциям, которые я показывал вам недавно:

 var MyModule = (function() { var myProtectedData = 909; var utils = { extend : function(root, props) { }, privatise : function(root, prop) { } }; this.myPublicData = 42; return utils.extend(this, { myProtectedData : myProtectedData, utils : utils }); })(); 

Вы можете увидеть, как мы используем вариант шаблона раскрывающегося модуля, чтобы возвращать не только общедоступные, но и защищенные элементы. Итак, на данный момент у нас есть три открытых члена: MyModule.myProtectedData , MyModule.utils и MyModule.myPublicData .

Теперь вот пример последнего модуля, который использует функцию privatise для копирования указанных открытых членов обратно в частные переменные, а затем удаляет их публичные ссылки:

 var MyModule = (function() { var myProtectedData = this.utils.privatise(this, 'myProtectedData'); var utils = this.utils.privatise(this, 'utils'); return this; }).apply(MyModule); 

И как только это будет сделано, защищенные элементы будут заблокированы внутри своих объектов, доступны для обоих модулей, но больше не будут доступны извне.

Обратите внимание, что функция privatise полагается на наличие отдельных аргументов для объекта и ключа свойства, поскольку объекты в JavaScript передаются по ссылке . Таким образом, root является ссылкой на MyModule , и когда мы удаляем из него свойство, указанное key , мы удаляем это свойство из ссылочного объекта.

Но если бы это было так:

 privatise : function(root) { var data = root; try { delete root; } catch(ex) { root = null; } return data; } 

И называется так:

 var myProtectedData = this.utils.privatise(this.myProtectedData); 

Тогда публичные члены не будут удалены — функция просто удалит ссылку , а не свойство, на которое она ссылается.

Конструкция try ... catch также необходима для более старых версий IE , в которых delete не поддерживается. В этом случае мы аннулируем публичное свойство, а не удаляем его, что, очевидно, не то же самое, но имеет эквивалентный конечный результат отрицания публичной ссылки члена.

Расширение защищенных членов

Имея эту платформу, мы можем добавить любое количество дополнительных модулей и сделать так, чтобы все они разделяли защищенные элементы, просто добавляя их между первым и последним модулями . В этих промежуточных модулях участники не приватизированы, они просто проходят через:

 var MyModule = (function() { var myProtectedData = this.myProtectedData; var utils = this.utils; return this; }).apply(MyModule); 

На самом деле совсем не обязательно копировать защищенные элементы в закрытые переменные, но это означает, что у нас есть единообразная форма ссылок в каждом модуле.

Мы также можем расширять защищенные объекты в зависимости от модуля, например, для определения дополнительных служебных функций, которые нужны только определенным модулям. Такие расширения, очевидно, также будут доступны для последующих модулей:

 var MyModule = (function() { var myProtectedData = this.myProtectedData; var utils = this.utils.extend(this.utils, { extraStuff : function() { } }); return this; }).apply(MyModule); 

И последнее, что следует отметить, это то, что защищенные участники также могут быть привилегированными . Пример, который я показал вам ранее, для привилегированного объекта config , является основным кандидатом на данные, которые могут быть полезны для защиты. Конечным результатом будут настройки конфигурации, которые могут использоваться всеми модулями, но которые пользователь по-прежнему не может изменить, не пройдя функцию public define .

Окончательный расширенный шаблон модуля

Я подготовил файл для загрузки, который включает в себя все функции, Master.js в этой статье, и разделен на три отдельных файла : Master.js — это корневой объект, который объявляет первоначальных членов, Extension.js — необязательный промежуточный модуль (из которых любой количество экземпляров может быть использовано), тогда Runtime.js — последний модуль, который запечатывает защищенные элементы: