Статьи

Добавление микроконструкторов в шаблон модульного дизайна

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

Пример использования

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

  1. Существует два интерфейса, и на любой данной странице может быть любое количество экземпляров одного или другого (но не обоих).
  2. Каждый экземпляр интерфейса будет нуждаться в своих собственных открытых методах — таких как load и save для взаимодействия с этим экземпляром.
  3. Также потребуется функциональность управления для управления всеми экземплярами любого интерфейса и обмена данными между ними.

Таким образом, чтобы удовлетворить все эти требования, я пришел с этой идеей; но это было не так просто!

Проблема с публичными конструкторами

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

Экземпляр сконструированного объекта внутренне называется так, поэтому свойства сконструированного объекта определяются с использованием синтаксиса this.property . Если мы хотим, чтобы методы-прототипы имели доступ к данным конструктора, мы должны определить эти свойства с помощью открытого синтаксиса. Закрытые переменные, определенные в конструкторе, доступны только в конструкторе. И есть проблема: если конструктор является общедоступным, то так же, как и его свойства.

Итак, как мы реализуем специфичные для экземпляра свойства сконструированного открытого объекта, скрывая все эти данные в закрытой области видимости? Это на самом деле проще, чем кажется!

Модульная структура виджета

Давайте начнем с рассмотрения структуры модуля виджета, которая разбивает код на два отдельных скрипта. Первый скрипт — это Widget.js , который создает корневой объект и определяет все общие функции, аналогично примеру с модулем Master из предыдущей статьи . Помимо ожидаемого объекта config и служебных функций, есть еще один защищенный объект под названием instances , о котором мы поговорим чуть позже. Чтобы сохранить пример кода коротким, объекты и функции — это просто пустые оболочки, но вы можете получить полный код в конце этой статьи.

 var Widget = (function() { var instances = {}, config = {}, utils = { extend : function(root, props){ ... }, privatise : function(root, prop){ ... } }; this.define = function(key, value){ ... }; return utils.extend(this, { instances : instances, config : config, utils : utils }); })(); 

Второй сценарий — DeveloperInterface.js или RespondentInterface.js , он похож на пример модуля времени выполнения из предыдущей статьи . Его первая работа — запечатать защищенных членов. Именно здесь определяется конструктор открытого интерфейса, открытый объект, который также имеет свои собственные открытые методы. На любой странице требуется только один из сценариев интерфейса, и для этого примера я использую интерфейс разработчика.

 Widget = (function() { var instances = this.utils.privatise(this, 'instances'), config = this.utils.privatise(this, 'config'), utils = this.utils.privatise(this, 'utils'); this.DeveloperInterface = function() { }; this.DeveloperInterface.prototype = { load : function(){ ... }, save : function(){ ... } }; return this; }).apply(Widget); 

Внутри конструктора

Открытый конструктор используется для создания экземпляра интерфейса и передает ссылочный ключ (частичный id ) в статическую разметку, которую он улучшает.

 var example = new Widget.DeveloperInterface("A1"); 

Ключ используется для получения ссылки DOM на разметку. Оба эти значения должны быть доступны из методов load и save . При прочих равных условиях мы определяем их как публичные свойства:

 this.DeveloperInterface = function(key) { this.key = key; this.question = document.getElementById('Question-' + this.key); }; 

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

Так что это неизбежно — где-то вдоль линии у нас нет выбора, кроме как создавать открытые свойства. Однако мы можем ограничить эти данные одним ссылочным значением, а затем использовать это значение для ссылки на личные данные. Для этого предназначен объект instances .

Использование объекта Instances

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

 this.DeveloperInterface = function(key) { this.key = key; instances[this.key] = { question : document.getElementById('Question-' + this.key) }; }; 

key является ссылочным значением и единственным общедоступным свойством. Свойство question теперь экранировано внутри защищенного объекта, но все еще доступно для методов интерфейса как instances[this.key].question . Затем объект instances может быть расширен с любым количеством свойств, и все они будут частными для виджета, но доступны для методов экземпляра.

Держаться за ключ

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

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

Поддержание глобального контроля

Защита данных, безусловно, важна, но не менее важен тот факт, что теперь у нас есть централизованная ссылка на все экземпляры интерфейса. Это позволяет реализовать избыточную функциональность. Функции в сценарии интерфейса могут выполнять итерацию по всем экземплярам, ​​считывать данные из них, записывать данные обратно в них или что-либо еще необходимое для управления и контроля. А поскольку объект instances защищен, он также доступен для основного модуля Widget . Оттуда мы можем реализовать общую функциональность, которая применяется к экземплярам любого интерфейса.

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

 this.DeveloperInterface = function(key) { this.key = key; instances[this.key] = { question : document.getElementById('Question-' + this.key) }; instances[this.key].question.instance = this; }; 

Вот простой пример использования глобального события click . Прослушиватель событий будет определен в главном модуле Widget , а затем активирован щелчками внутри элемента question любого экземпляра интерфейса:

 document.addEventListener('click', function(e) { var target = e.target; do { if(typeof(target.instance) !== 'undefined') { break; } } while(target = target.parentNode); if(target) { alert(target.instance.key); alert(target === instances[target.instance.key].question); } }, false); 

Из этих примеров предупреждений вы можете видеть, как мы можем использовать ссылку на instance для ссылки на key экземпляра, а вместе с этим — на циклическую ссылку на target .

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

Я подготовил файл для загрузки, который включает в себя все функции, описанные в этой статье. Он разделен на два отдельных файла, Widget.js и DeveloperInterface.js , как описано в этой статье: