Каждый разработчик стремится писать удобный и легко читаемый код. Как только приложения становятся больше, структурирование кода становится важной частью.
Шаблоны проектирования играют важную роль в решении этой задачи, обеспечивая организационную структуру распространенных вопросов в конкретных обстоятельствах.
При создании приложений веб-разработчики JavaScript часто взаимодействуют с шаблонами проектирования неосознанно.
Как правило, они используют некоторые шаблоны проектирования больше, чем другие, хотя есть обширный перечень шаблонов проектирования на разные случаи.
В этом посте я хотел бы обсудить распространённые шаблоны чтобы улучшить ваши знания в программировании и погрузиться глубже в JavaScript.
Шаблоны проектирования включают в себя следующее:
— Модуль
— Прототип
— Наблюдатель
— Одиночка
Каждый шаблон состоит из множества свойств, но я выделяю следующие ключевые моменты:
1.Контекст: Где/при каких обстоятельствах используется тот или иной шаблон?
2.Проблема: Какую проблему мы пытаемся решить?
3.Решение: Как использовать это шаблон для решения этой проблемы?
4.Реализация: Как выглядит реализация?
# Шаблон Модуль (Module)
В JavaScript модули являются наиболее распространенными шаблонами проектирования для обеспечения независимости каких-то частей кода от других компонентов. Это обеспечивает слабую связь для поддержания хорошо структурированного кода.
Для тех, кто знаком с объектно-ориентированными языками, модули — это «классы» в JavaScript. Одно из многих преимуществ классов — инкапсуляция – защита состояния и поведения от доступа из других классов.
Шаблон модуля дает доступ публичным и частным уровням (плюс менее защищенным и привилегированным).
Модули должны быть немедленно вызываемыми функциями (IIFE) , чтобы позволить частным областям видимости, то есть замыканию, защитить переменные и методы (однако, вернется объект, а не функция). Вот как это выглядит:
1
2
3
4
5
6
|
(function() {
// объявляем приватные переменные и/или функции
return {
// объявляем публичные переменные и/или функции
}
})();
|
Здесь мы создаем приватные переменные и/или функции до возвращения нашего объекта, который мы хотим вернуть.
Код за пределами нашего замыкания не может получить доступ к этим частным переменным, так как он не в той же области видимости. Давайте рассмотрим более конкретную реализацию:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var HTMLChanger = (function() {
var contents = ‘contents’
var changeHTML = function() {
var element = document.getElementById(‘attribute-to-change’);
element.innerHTML = contents;
}
return {
callChangeHTML: function() {
changeHTML();
console.log(contents);
}
};
})();
HTMLChanger.callChangeHTML(); // Результат: ‘contents’
console.log(HTMLChanger.contents); // undefined
|
Обратите внимание, что callChangeHTML связан с возвращенным объектом и может ссылаться в рамках неймспейс (namespace) HTMLChanger. Однако, когда содержимое снаружи модуля, то такое невозможно.
REVEALING MODULE PATTERN
Цель — поддержание инкапсуляции и открытие определенных переменных и методов, возвращаемых в литерале объекта. Непосредственная реализация выглядит следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
var Exposer = (function() {
var privateVariable = 10;
var privateMethod = function() {
console.log(‘Inside a private method!’);
privateVariable++;
}
var methodToExpose = function() {
console.log(‘This is a method I want to expose!’);
}
var otherMethodIWantToExpose = function() {
privateMethod();
}
return {
first: methodToExpose,
second: otherMethodIWantToExpose
};
})();
Exposer.first(); // Результат: Этот вот метод я хочу раскрыть!
Exposer.second(); // Результат: Внутри приватного метода!
Exposer.methodToExpose; // undefined
|
Здесь есть есть явный недостаток – невозможность ссылаться на приватные методы. Это может создавать проблемы в модульном тестировании. Аналогичным образом, публичное поведение не является переопределяемым.
# Шаблон Прототип (Prototype)
Любой Разработчик JavaScript либо видел ключевое слово prototype, озадаченный прототипным наследованием, либо реализовывал прототипы в своем коде. Шаблон прототип основывается на прототипном наследовании JavaScript. Модель прототипа используется в основном для создания объектов в ситуациях, требующих высокой производительности. Созданные объекты являются пустыми клонами исходного объекта.
Один из случаев использования паттерна «прототип» – проведение обширных операций с базой данных для создания объекта, используемого для других частей приложения. Если другой процесс необходим для использования этого объекта, то вместо того, чтобы выполнять эти операции с базой данных, целесообразнее клонировать ранее созданный объект.
Пояснение в схеме: Prototype Design Pattern on Wikipedia
Этот язык UML описывает интерфейс прототипа используется для клонирования конкретных реализаций.
Для клонирования объекта должен существовать конструктор для того чтобы создать экземпляр первого объекта. Далее, с помощью ключевого слова prototype переменные и методы привязываются к структуре объекта. Давайте рассмотрим простой пример:
1
2
3
4
5
6
7
8
9
10
11
|
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = ‘Tesla’;
this.make = ‘Model S’;
}
TeslaModelS.prototype.go = function() {
// Вращаются колеса
}
TeslaModelS.prototype.stop = function() {
// Применяются тормозные колодки
}
|
Конструктор позволяет создавать один объект TeslaModelS. При создании нового объекта TeslaModelS, он сохранит состояние, инициализированное в конструкторе. Кроме того, поддержание функции go и stop несложно, так как мы объявили их при помощи прототипов. Такой же способ расширения функции с использованием прототипа описан ниже:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = ‘Tesla’;
this.make = ‘Model S’;
}
TeslaModelS.prototype = {
go: function() {
// Вращаются колеса
},
stop: function() {
// Применяются тормозные колодки
}
}
|
REVEALING PROTOTYPE PATTERN
Так же как и шаблон модуль шаблон прототип имеет вариацию Revealing . Revealing паттерн обеспечивает инкапсуляцию с публичными и приватными членами.
Поскольку мы возвращаем объект, мы добавим объекту-прототипу префикс функции. Дополнив наш пример, мы можем выбрать что мы хотим показать в текущем прототипе, чтобы сохранить свои уровни доступа:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = ‘Tesla’;
this.make = ‘Model S’;
}
TeslaModelS.prototype = function() {
var go = function() {
// Вращаются колеса
};
var stop = function() {
// Применяются тормозные колодки
};
return {
pressBrakePedal: stop,
pressGasPedal: go
}
}();
|
Обратите внимание, как функции Stop и Go будут защищены от возвращенного объекта в связи с нахождением за пределами области видимости возвращаемого объекта. Поскольку JavaScript изначально поддерживает прототипное наследование, нет необходимости переписывать базовые элементы(или особенности или черты).
# Шаблон Наблюдатель (Observer)
Бывает так, что одна часть приложения изменяется, а другие части нуждаются в обновлении. В Angular js, если $scope объекта обновляется, событие может быть запущено для уведомления другого компонента. Шаблон Observer включает в себя то, что, если объект изменен, то он передает (broadcasts) зависимым объектам, что изменение произошло.
Другой яркий пример архитектура модель-представление-контроллер (MVC); представление обновляется когда изменяется модель. Одним из преимуществ является разрыв связи представления от модели для уменьшения зависимостей.
Пояснение в схеме: Observer Design Pattern on Wikipedia
Как показано на схеме UML, необходимые объекты это subject, observer, и concrete. Объект subject содержит ссылки на concrete observers для уведомления любых изменениях. Объект observer является абстрактным классом, позволяющий concrete observers реализовывать метод уведомления.
Давайте взглянем на пример AngularJS, который включает в себя шаблон Observer через управление событиями.
1
2
3
4
5
6
7
8
9
|
// Controller 1
$scope.$on(‘nameChanged’, function(event, args) {
$scope.name = args.name;
});
...
// Controller 2
$scope.userNameChanged = function(name) {
$scope.$emit(‘nameChanged’, {name: name});
};
|
С шаблоном Observer важно различать независимый это объект или subject.
Важно отметить, что, хотя шаблон Observer и предоставляет много преимуществ, но одним из недостатков является значительное падение производительности, так как количество «наблюдателей» (observers) увеличено. Один из самых пользующихся дурной славой наблюдателей являются watchers. В AngularJS мы можем наблюдать (watch) переменные, функции и объекты. Цикл $$digest работает и уведомляет каждого из watchers новыми значениями всякий раз, когда область объекта изменяется.
Мы можем создать наши собственные Subjects и Observers в JavaScript. Давайте посмотрим, как это реализуется:
1
2
3
4
5
6
7
8
9
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
|
var Subject = function() {
this.observers = [];
return {
subscribeObserver: function(observer) {
this.observers.push(observer);
},
unsubscribeObserver: function(observer) {
var index = this.observers.indexOf(observer);
if(index > —1) {
this.observers.splice(index, 1);
}
},
notifyObserver: function(observer) {
var index = this.observers.indexOf(observer);
if(index > —1) {
this.observers[index].notify(index);
}
},
notifyAllObservers: function() {
for(var i = 0; i < this.observers.length; i++){
this.observers[i].notify(i);
};
}
};
};
var Observer = function() {
return {
notify: function(index) {
console.log(«Observer « + index + » is notified!»);
}
}
}
var subject = new Subject();
var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();
subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);
subject.notifyObserver(observer2); // Observer 2 is notified!
subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!
|
PUBLISH/SUBSCRIBE
Паттерн Publish/Subscribe использует канал topic/event, который находится между объектами, желающими получать уведомления (Subscribers) и объектом генерации события (Publisher). Эта система событий позволяет коду определить применение конкретных событий, которые могут передать пользовательские аргументы, содержащие значения, необходимые subscriber. Цель – избежание зависимости между подписчиком Subscriber и Publisher .
В этом состоит отличие от шаблона Observer так как любой subscriber реализует соответствующий обработчик события чтобы зарегистрировать и получить topic уведомления, передаваемые publisher.
Многие разработчики выбирают агрегирование (объединение) паттерна publish/subscribe и шаблона «наблюдателя» несмотря на различия. Subscriber в паттерне publish/subscribe уведомляются посредством сообщений, а «наблюдатели» уведомляются с помощью обработчика похожего на subject.
В AngularJS subscriber ‘subscribes’ на событие, используя $on (‘event’, callback) , а publisher ‘publishes’ событие, используя $emit(‘event’, args) или $broadcast(‘event’, args).
# Шаблон Синглтон (Singleton)
Синглтон позволяет наличие только одного экземпляра, а также много экземпляров одного и того же объекта. Синглтон ограничивает клиентов от создания множества объектов, после первого созданного объекта, он возвращает экземпляры самого себя.
Нахождение вариантов использования Синглтонов сложно для не использовавших их раньше. Одним из примеров является использование в офисном принтере. Допустим в офисе десять человек и все они используют один принтер, десять компьютеров подключены к одному принтеру (экземпляр). Путем совместного использования одного принтера, они делятся одними и те же ресурсами.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
var printer = (function () {
var printerInstance;
function create () {
function print() {
// основной принцип действия принтера
}
function turnOn() {
// проверка на бумаге
}
return {
// публичные + приватные состояния и поведения
print: print,
turnOn: turnOn
};
}
return {
getInstance: function() {
if(!printerInstance) {
printerInstance = create();
}
return printerInstance;
}
};
function Singleton () {
if(!printerInstance) {
printerInstance = intialize();
}
};
})();
|
Создание метода — приватность, так как мы не хотим, чтобы клиент получил доступ к нему, однако заметьте, что метод getInstance является публичным. Каждый офисный работник может создать экземпляр принтера взаимодействуя с методом getInstance, вот так:
1
|
var officePrinter = printer.getInstance();
|
В AngularJS, синглтоны являются наиболее распространенными, наиболее примечательные из них это services, factories, и providers.
# Заключение
Шаблоны проектирования часто используются в крупных приложениях, хотя понять, где лучше применять тот или иной шаблон приходит с практикой.
Прежде чем создавать какие-либо приложения, вы должны тщательно обдумать каждое действие и как все будет взаимодействовать друг с другом. Изучив Module, Prototype, Observer, и Singleton, вам следует различать эти паттерны и понимать как использовать их.
Источник: scotch.io