Шаблоны проектирования JavaScript

Материал из support.qbpro.ru

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

Что такое шаблон проектирования?

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

И так, почему важно знать и понимать шаблоны проектирования? Шаблоны проектирования имеют три основных преимущества:

  1. Шаблоны представляют собой проверенные решения. Они обеспечивают твердые подходы к решению вопросов в разработке программного обеспечения, используя проверенные методы, которые отражают опыт и знания разработчиков, которые помогли определить их и довести до шаблона.
  2. Шаблоны могут быть легко использованы повторно: обычно отражает модель из коробки решения, которые могут быть адаптированы к нашим собственным потребностям. Эта особенность делает их весьма надежными.
  3. Шаблоны могут быть выразительным: Когда мы смотрим на шаблон в целом, то присутствует определенная структура, и готовые пути решения, с помощь которых можно представить решение сложной задачи простым способом.

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

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


Шаблоны проектирования, предлагают решение наиболее типичных задач, связанных с архитектурой объектно-ориентированного программного обеспечения. Они достаточно давно используются на практике и доказали свою полезность во многих ситуациях. Именно поэтому и вам будет полезно познакомиться с ними и обсудить их.

Хотя эти шаблоны проектирования могут применяться в любом языке программирования, тем не менее многие годы они изучаются с позиций языков со строгим контролем типов и со статическими классами, таких как C++ и Java.

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

Нами будут рассмотрены следующие шаблоны проектирования:

  • Пространство имен
  • Частные свойства и методы
  • Модуль
  • Шаблон модуль выявление
  • Одиночка
  • Наблюдатель
  • Посредник
  • Прототип
  • Команда
  • Фасад
  • Фабричный метод
  • Смешанный шаблон
  • Декоратор
  • Приспособленец

Мы уже используем шаблоны в повседневной жизни

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

Представьте себе, что у нас есть сценарий, где нужно найти на странице элементы с классом "Foo" Каким будет самый эффективный способ решения? Вот несколько различных способов, решения это проблемы:

  1. Выбрать все элементы на странице, и путем ручного перебора проверять каждый элемент, соответствует ли его класс тому, который мы ищем.
  2. Использовать современную строенную функцию браузера querySelectorAll() для выбора всех элементов с классом “Foo”.
  3. Использование встроенной функцией такой как getElementsByClassName() и получить нужную коллекцию.

Итак, какой из этих вариантов является самым быстрым? Это на самом деле вариант под номером 3, С коэффициентом в 8-10 раз. Но в реальных приложениях, 3-й вариант может и не работать, так как в версии Internet Explorer ниже 9 поддерживают его не полностью и в таком случае необходимо использовать другие способы .

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

Категории шаблонов проектирования

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

Порождающие (Creational) шаблоны проектирования

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

Некоторые из шаблонов которые попадают в эту категорию:

  • Абстрактная фабрика (Abstract Factory).
  • Прототип (Prototype).
  • Одиночка (Singleton).
  • Строитель (Builder).

Структурные (Structural) шаблоны проектирования

Это шаблоны проектирования, в котором рассматривается вопрос о том, как из классов и объектов образуются более крупные структуры.

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

Некоторые из шаблонов которые попадают в эту категорию:

  • Декоратор (Decorator).
  • Фасад (Facade).
  • Приспособленец (Flyweight).
  • Адаптер (Adapter).
  • Заместитель (Proxy).

Поведенческие (Behavioral) шаблоны проектирования

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

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

Некоторые из шаблонов которые попадают в эту категорию:

  • Итератор (Iterator).
  • Посредник (Mediator).
  • Наблюдатель (Observer).
  • Посетитель (Vistor).

Общая таблица категорий шаблонов проектирования

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


Creational (Порождающие) На основе концепции создания объекта
Class
Factory Method (Фабричный метод) Создает экземпляры нескольких производных классов на основе данных интерфейса или событий
Object
Abstract Factory (Абстрактная фабрика) Предоставляет интерфейс для создания семейств, связанных между собой, или независимых объектов, конкретные классы которых неизвестны
Builder (Строитель) Отделяет конструирование сложного объекта от его представления, позволяя использовать один и тот же процесс конструирования для создания различных представлений
Prototype (Прототип) Описывает виды создаваемых объектов с помощью прототипа и создает новые объекты путем его копирования
Singleton (Одиночка) Гарантирует, что некоторый класс может иметь только один экземпляр, и предоставляет
Structural (Структурные) Базируется на построении блоков из объектов
Class
Adapter (Адаптер) Преобразует интерфейс класса в некоторый другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного шаблона из-за несовместимости интерфейсов
Object
Adapter (Адаптер) Преобразует интерфейс класса в некоторый другой интерфейс, ожидаемый клиентами. Обеспечивает совместную работу классов, которая была бы невозможна без данного шаблона из-за несовместимости интерфейсов
Bridge (Мост) Отделяет абстракцию от реализации, благодаря чему появляется возможность независимо изменять и то и другое
Composite (Компоновщик) Группирует объекты в древовидные структуры для представления иерархий типа “часть-целое”. Позволяет клиентам работать с единичным объектами так же, как с группами объектов
Decorator (Декоратор) Динамически возлагает на объект новые функции. Декораторы применяются для расширения имеющейся функциональности и являются гибкой альтернативой порождения подклассов
Facade (Фасад) Представляет унифицированный интерфейс к множеству интерфейсов в некоторой подсистеме. Определяет интерфейс более высокого уровня, облегчающий работу с подсистемой
Flyweight (Приспособленец) Использует разедление для эффективной поддержки большого числа мелких объектов
Proxy (Заместитель) Подменяет другой объект для контроля доступа к нему
Behavioral (Поведенческие) На основе концепции создания объекта
Class
Interpreter (Интерпретатор) Для заданного языка определяется представление его грамматики, а также интерпретатор предложений языка, использующий это представление
Template Method (Шаблонный метод) Определяет скелет алгоритма, перекладывая ответственность за некоторые его шаги на подклассы. Позволяет подклассам переопределять шаги алгоритма, не меняя его общей структуры
Object
Chain of Responsibility (Цепочка обязаностей) Можно избежать жесткой зависимости отправителя запроса от его получателя, при этом запросом начинает обрабатываться один из несколько объектов. Объекты получатели связываются в цепочку, и запрос передается по цепочке, пока какой-то объект его не обработает
Command (Команда) Инкапсулирует запрос в виде объекта, позволяя тем самым параметризировать клиентов типом запроса, устанавливать очередность запросов, протоколировать их и поддерживать отмену выполнения операций
Iterator (Итератор) Дает возможность последовательно обойти все элементы составного объекта, не расскрывая его внутреннего представления
Mediator (Посредник) Определяет объект, в котором инкапсулировано знание о том, как взаимодействуют объекты из некоторого множества. Способствует уменьшению числа связей между объектами, позволяя им работать без явных ссылок друг на друга. Это, в свою очередь, дает возможность независимо изменять схему взаимодействия
Memento (Хранитель) Позволяет, не нарушая инкапсуляции, получить и сохранить во внешней памяти внутренее состояние объекта, чтобы позже объект можно было восстановить точно в таком же состоянии
Observer (Наблюдатель) Определяет между объектами зависимость типа один - ко - многим, так что при изменении состоянии одного объекта все зависящие от него получают извещение и автоматически обновляются
State (Состояние) Позволяет объекту варьировать свое поведение при изменении внутреннего состояния. При этом создается впечатление, что поменялся класс объекта
Strategy (Стратегия) Определяет семейство алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. Можно менять алгоритм независимо от клиента, который им пользуется
Visitor (Посетитель) Представляет операцию, которую надо выполнять над элементами объекта. Позволяет определить новую операцию, не меняя классы элементов, к которым он применяется

Шаблон “Пространство имен”

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

Взгляните на следующий пример:

// ДО: 5 глобальных переменных
function Parent() {}
function Child() {}
var some_var = 1;
var module1 = {};
module1.data = {a: 1, b: 2};
va module2 = {};

Такой программный код легко переписать иным способом, создав единственный глобальный объект, назовем его MYAPP, и превратив все функции и переменные в свойства этого глобального объекта:

// ПОСЛЕ: 1 глобальная переменная 
// глобальный объект
var MYAPP = {}; 
// конструкторы 
MYAPP.Parent = function () {}; 
MYAPP.Child = function () {}; 
// переменная 
MYAPP.some_var = 1; 
// обьект-контейнер 
MYAPP.modules = {}; 
// вложенные обьекты 
MYAPP.modules.modulel = {}; 
MYAPP.modules.modulel.data = {a: 1, b: 2}; 
MYAPP.modules.module2 = {}; 

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

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

// применение функции пространства имен 
MYAPP.namespace('MYAPP.modules.module2'); 
// этот вызов эквивалентен следующей конструкции: 
//  var MYAPP = { 
//      modules: { 
//          module2: {} 
//      } 
//  }; 

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

var MYAPP = MYAPP || {}; 
MYAPP.namespace = function (ns_string) { 
          var parts = ns_string.split('.'), 
          parent = MYAPP, 
          i; 
         // отбросить начальный префикс - имя глобального объекта 
        if (partsf[0] === "MYAPP") { 
        parts = parts.slice(1); 
        } 
        for (i = 0; i < parts.length; i += 1) { 
            // создать свойство, если оно отсутствует 
           if (typeof parent[parts[i]] === "undefined") { 
               parent[parts[i]] = {}; 
            } 
           parent = parent[parts[i]]; 
       } 
       return parent; 
}; 

Такая реализация делает допустимыми все следующие варианты использования функции:

// присваивать возвращаемое значение локальной переменной 
var module2 = MYAPP.namespace(‘MYAPP.modules.module2’); 
module2 === MYAPP.modules.module2; // true 
// опускать начальный префикс 'MYAPP'
MYAPP.namespace('modules.module51'); 
// создавать глубоко вложенные пространства имен 
MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property' ); 

Шаблон “Частные свойства и методы”

В языке JavaScript нет специальных средств объявления частных (private), защищенных (protected) или общедоступных (public) свойств и методов, как в языке Java или в других языках. Все члены объектов в этом языке являются общедоступными:

var myobj = { 
                 myprop: 1, 
                getProp: function () { 
                         return this.myprop; 
                 } 
}; 
console.log(myobj.myprop); // 'myprop' - общедоступный член 
console.log(myobj.getPropO); // getPropO - также общедоступный член 

To же справедливо и при использовании функций-конструкторов для создания объектов - все члены являются общедоступными:

function Gadget() { 
         this.name = 'iPod'; 
         this.stretch = function () { 
                   return 'iPad'; 
        }; 
} 
var toy = new GadgetO; 
console.log(toy.name); // 'name' - общедоступный член 
console.log(toy.stretch()); // stretch() - общедоступный член 

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

function Gadget() { 
        // частный член 
         var name = ‘iPod’; 
       // общедоступная функция 
        this.getName = function () { 
               return name; 
        }; 
}
var toy = new Gadget(); 
// имя 'name' не определено, частный член 
console.log(toy.папе); // undefined 
// общедоступный метод может обратиться к частному члену 'name'
console.log(toy.getName()); // "iPod"

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

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

var myobj; // это будет объект 
(function () { 
         // частные члены 
         var name = "my, oh my"; 
         // реализация общедоступных членов 
         // обратите внимание на отсутствие инструкции 'var'
          myobj = { 
               // привилегированный метод 
              getName: function () { 
                     return name; 
               } 
         }; 
}()); 
myobj.getName(); // "my, oh my"

Та же идея положена в основу следующего примера, имеющего несколько иную реализацию:

var myobj = (function () { 
          // частные члены 
        var name = "my, oh my"; 
         // реализация общедоступных членов 
        return { 
               getName: function () { 
                       return name; 
              } 
}; 
}()); 
myobj.getName(); // "my, oh my"

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

Один из недостатков создания частных членов с применением конструкторов заключается в том, что они создаются всякий раз, когда вызывается конструктор для создания нового объекта.

Фактически эта проблема относится ко всем членам, добавляемым внутри конструкторов. Чтобы сэкономить свои усилия и память, общие для всех экземпляров свойства и методы можно добавить в свойство prototype конструктора. При таком подходе общие члены будут совместно использоваться всеми экземплярами, созданными с помощью одного и того же конструктора. Аналогичным образом можно определять частные свойства, совместно используемые всеми экземплярами. Для этого необходимо применить комбинацию из двух шаблонов: частные свойства внутри конструкторов и частные свойства в литералах объектов. Так как свойство prototype является обычным объектом, его можно определить в виде литерала.

Как это сделать, показано в следующем примере:

function Gadget() { 
         // частный член 
         var name = 'iPod'; 
         // общедоступная функция 
          this.getName = function () { 
                return name; 
         }; 
} 
Gadget.prototype = (function () { 
          // частный член 
         var browser = "Mobile Webkit"; 
         // общедоступные члены прототипа 
         return { 
               getBrowser: function () { 
                     return browser; 
              } 
        }; 
}()); 
var toy = new Gadget(); 
console.log(toy.getName()); // "собственный" привилегированный метод 
console.log(toy.getBrowser()); // привилегированный метод прототипа