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

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

Зачем нужны паттерны проектирования?

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

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

Например, если вы используете паттерн «Декоратор», это тут же сообщит новому программисту, пришедшему в проект, о том, какие именно задачи решает некий фрагмент кода и зачем он нужен. Благодаря этому такой программист сможет больше времени уделить практическим задачам, которые решает программа, а не попыткам понять её внутреннее устройство.

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

Паттерн «Модуль»

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

Модули - это составная часть любого современного JavaScript-приложения. Они помогают поддерживать чистоту кода, способствуют разделению кода на осмысленные фрагменты и помогают его организовывать. В JavaScript существует множество способов создания модулей, одним из которых является паттерн «Модуль» (Module).

В отличие от других языков программирования, JavaScript не имеет модификаторов доступа. То есть, переменные нельзя объявлять как приватные или публичные. В результате паттерн «Модуль» используется ещё и для эмуляции концепции инкапсуляции.

Этот паттерн использует IIFE (Immediately-Invoked Functional Expression, немедленно вызываемое функциональное выражение), замыкания и области видимости функций для имитации этой концепции. Например:

Const myModule = (function() { const privateVariable = "Hello World"; function privateMethod() { console.log(privateVariable); } return { publicMethod: function() { privateMethod(); } } })(); myModule.publicMethod();

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

В результате переменные и функции, объявленные внутри IIFE, скрыты от механизмов, находящихся во внешней по отношению к ним области видимости. Они оказываются приватными сущностями константы myModule .

После того, как этот код будет выполнен, myModule будет выглядеть следующим образом:

Const myModule = { publicMethod: function() { privateMethod(); }};

То есть, обращаясь к этой константе, можно вызвать общедоступный метод объекта publicMethod() , который, в свою очередь, вызовет приватный метод privateMethod() . Например:

// Выводит "Hello World" module.publicMethod();

Паттерн «Открытый модуль»

Паттерн «Открытый модуль» (Revealing Module) представляет собой немного улучшенную версию паттерна «Модуль», которую предложил Кристиан Хейльманн. Проблема паттерна «Модуль» заключается в том, что нам приходится создавать публичные функции только для того, чтобы обращаться к приватным функциям и переменным.

В рассматриваемом паттерне мы назначаем свойствам возвращаемого объекта приватные функции, которые хотим сделать общедоступными. Именно поэтому данный паттерн и называют «Открытый модуль». Рассмотрим пример:

Const myRevealingModule = (function() { let privateVar = "Peter"; const publicVar = "Hello World"; function privateFunction() { console.log("Name: "+ privateVar); } function publicSetName(name) { privateVar = name; } function publicGetName() { privateFunction(); } /** открываем функции и переменные, назначая их свойствам объекта */ return { setName: publicSetName, greeting: publicVar, getName: publicGetName }; })(); myRevealingModule.setName("Mark"); // Выводит Name: Mark myRevealingModule.getName();

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

После выполнения IIFE myRevealingModule выглядит так:

Const myRevealingModule = { setName: publicSetName, greeting: publicVar, getName: publicGetName };

Мы можем, например, вызвать метод myRevealingModule.setName("Mark") , который представляет собой ссылку на внутреннюю функцию publicSetName . Метод myRevealingModule.getName() ссылается на внутреннюю функцию publicGetName . Например:

MyRevealingModule.setName("Mark"); // выводит Name: Mark myRevealingModule.getName();

Рассмотрим преимущества паттерна «Открытый модуль» перед паттерном «Модуль»:

  • «Открытый модуль» позволяет делать общедоступными скрытые сущности модуля (и снова скрывать их, если нужно), модифицируя, для каждой из них, лишь одну строку в объекте, возвращаемом после выполнения IIFE.
  • Возвращаемый объект не содержит определения функций. Всё, что находится справа от имён его свойств, определено в IIFE. Это способствует чистоте кода и упрощает его чтение.

Модули в ES6

До выхода стандарта ES6 в JavaScript не было стандартного средства для работы с модулями, в результате разработчикам приходилось использовать сторонние библиотеки или паттерн «Модуль» для реализации соответствующих механизмов. Но с приходом ES6 в JavaScript появилась стандартная система модулей.

Модули ES6 хранятся в файлах. Один файл может содержать лишь один модуль. Всё, что находится внутри модуля, по умолчанию является приватным. Функции, переменные и классы можно делать публичными с использованием ключевого слова export . Код внутри модуля всегда выполняется в строгом режиме.

Экспорт модуля

Есть два способа экспорта функции или переменной, объявленной в модуле:

  • Экспорт выполняется путём добавления ключевого слова export перед объявлением функции или переменной. Например:
// utils.js export const greeting = "Hello World"; export function sum(num1, num2) { console.log("Sum:", num1, num2); return num1 + num2; } export function subtract(num1, num2) { console.log("Subtract:", num1, num2); return num1 - num2; } // Это - приватная функция function privateLog() { console.log("Private Function"); }
  • Экспорт выполняется путём добавления ключевого слова export в конец кода с перечислением имён функций и переменных, которые нужно экспортировать. Например:
// utils.js function multiply(num1, num2) { console.log("Multiply:", num1, num2); return num1 * num2; } function divide(num1, num2) { console.log("Divide:", num1, num2); return num1 / num2; } // Это приватная функция function privateLog() { console.log("Private Function"); } export {multiply, divide};

Импорт модуля

Так же, как существуют два способа экспорта, есть и два способа импорта модулей. Делается это с использованием ключевого слова import:

  • Импорт нескольких избранных элементов. Например:
// main.js // Импорт нескольких избранных элементов import { sum, multiply } from "./utils.js"; console.log(sum(3, 7)); console.log(multiply(3, 7));
  • Импорт всего, что экспортирует модуль. Например:
// main.js // импорт всего, что экспортирует модуль import * as utils from "./utils.js"; console.log(utils.sum(3, 7)); console.log(utils.multiply(3, 7));

Псевдонимы для экспортируемых и импортируемых сущностей

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

Для переименования сущностей при экспорте можно поступить так:

// utils.js function sum(num1, num2) { console.log("Sum:", num1, num2); return num1 + num2; } function multiply(num1, num2) { console.log("Multiply:", num1, num2); return num1 * num2; } export {sum as add, multiply};

Для переименования сущностей при импорте используется такая конструкция:

// main.js import { add, multiply as mult } from "./utils.js"; console.log(add(3, 7)); console.log(mult(3, 7));

Паттерн «Синглтон»

Паттерн «Синглтон» или «Одиночка» (Singleton) представляет собой объект, который может существовать лишь в единственном экземпляре. В рамках применения этого паттерна новый экземпляр некоего класса создаётся в том случае, если он пока не создан. Если же экземпляр класса уже существует, то, при попытке обращения к конструктору, возвращается ссылка на соответствующий объект. Последующие вызовы конструктора всегда будут возвращать тот же самый объект.

Фактически, то, что мы называем паттерном «Синглтон», имелось в JavaScript всегда, но называют это не «Синглтоном», а «объектным литералом». Рассмотрим пример:

Const user = { name: "Peter", age: 25, job: "Teacher", greet: function() { console.log("Hello!"); } };

Так как каждый объект в JavaScript занимает собственную область памяти и не делит её с другими объектами, всякий раз, когда мы обращаемся к переменной user , мы получаем ссылку на один и тот же объект.

Паттерн «Синглтон» можно реализовать с использованием функции-конструктора. Выглядит это так:

Let instance = null; function User(name, age) { if(instance) { return instance; } instance = this; this.name = name; this.age = age; return instance; } const user1 = new User("Peter", 25); const user2 = new User("Mark", 24); // выводит true console.log(user1 === user2);

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

Паттерн «Синглтон» можно реализовать с использованием паттерна «Модуль». Например:

Const singleton = (function() { let instance; function User(name, age) { this.name = name; this.age = age; } return { getInstance: function(name, age) { if(!instance) { instance = new User(name, age); } return instance; } } })(); const user1 = singleton.getInstance("Peter", 24); const user2 = singleton.getInstance("Mark", 26); // prints true console.log(user1 === user2);

Здесь мы создаём новый экземпляр user , вызывая метод singleton.getInstance() . Если экземпляр объекта уже существует, то этот метод просто возвратит его. Если же такого объекта пока нет, метод создаёт его новый экземпляр, вызывая функцию-конструктор User .

Паттерн «Фабрика»

Паттерн «Фабрика» (Factory) использует для создания объектов так называемые «фабричные методы». При этом не требуется указывать классы или функции-конструкторы, которые применяются для создания объектов.

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

Class Car{ constructor(options) { this.doors = options.doors || 4; this.state = options.state || "brand new"; this.color = options.color || "white"; } } class Truck { constructor(options) { this.doors = options.doors || 4; this.state = options.state || "used"; this.color = options.color || "black"; } } class VehicleFactory { createVehicle(options) { if(options.vehicleType === "car") { return new Car(options); } else if(options.vehicleType === "truck") { return new Truck(options); } } }

Здесь созданы классы Car и Truck , которые предусматривают использование неких стандартных значений. Они применяются для создания объектов car и truck . Также здесь объявлен класс VehicleFactory , который используется для создания новых объектов на основе анализа свойства vehicleType , передаваемого соответствующему методу возвращаемого им объекта в объекте с параметрами options . Вот как со всем этим работать:

Const factory = new VehicleFactory(); const car = factory.createVehicle({ vehicleType: "car", doors: 4, color: "silver", state: "Brand New" }); const truck= factory.createVehicle({ vehicleType: "truck", doors: 2, color: "white", state: "used" }); // Выводит Car {doors: 4, state: "Brand New", color: "silver"} console.log(car); // Выводит Truck {doors: 2, state: "used", color: "white"} console.log(truck);

Здесь создан объект factory класса VehicleFactory . После этого можно создавать объекты классов Car или Truck , вызывая метод factory.createVehicle() и передавая ему объект options со свойством vehicleType , установленным в значение car или truck .

Паттерн «Декоратор»

Паттерн «Декоратор» (Decorator) используется для расширения функционала объектов без модификации существующих классов или функций-конструкторов. Этот паттерн можно использовать для добавления к объектам неких возможностей без модификации кода, который ответственен за их создание.

Вот простой пример использования этого паттерна:

Function Car(name) { this.name = name; // Значение по умолчанию this.color = "White"; } // Создание нового объекта, который планируется декорировать const tesla= new Car("Tesla Model 3"); // Декорирование объекта - добавление нового функционала tesla.setColor = function(color) { this.color = color; } tesla.setPrice = function(price) { this.price = price; } tesla.setColor("black"); tesla.setPrice(49000); // Выводит black console.log(tesla.color);

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

Class Car() { } class CarWithAC() { } class CarWithAutoTransmission { } class CarWithPowerLocks { } class CarWithACandPowerLocks { }

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

Class Car { constructor() { // Базовая стоимость this.cost = function() { return 20000; } } } // Функция-декоратор function carWithAC(car) { car.hasAC = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } } // Функция-декоратор function carWithAutoTransmission(car) { car.hasAutoTransmission = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 2000; } } // Функция-декоратор function carWithPowerLocks(car) { car.hasPowerLocks = true; const prevCost = car.cost(); car.cost = function() { return prevCost + 500; } }

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

This UML describes how a prototype interface is used to clone concrete implementations.

To clone an object, a constructor must exist to instantiate the first object. Next, by using the keyword prototype variables and methods bind to the object"s structure. Let"s look at a basic example:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype.go = function() { // Rotate wheels } TeslaModelS.prototype.stop = function() { // Apply brake pads }

The constructor allows the creation of a single TeslaModelS object. When a creating new TeslaModelS object, it will retain the states initialized in the constructor. Additionally, maintaining the function go and stop is easy since we declared them with prototype . A synonymous way to extend functions on the prototype as described below:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype = { go: function() { // Rotate wheels }, stop: function() { // Apply brake pads } }

Revealing Prototype Pattern

Similar to Module pattern, the Prototype pattern also has a revealing variation. The Revealing Prototype Pattern provides encapsulation with public and private members since it returns an object literal.

Since we are returning an object, we will prefix the prototype object with a function . By extending our example above, we can choose what we want to expose in the current prototype to preserve their access levels:

Var TeslaModelS = function() { this.numWheels = 4; this.manufacturer = "Tesla"; this.make = "Model S"; } TeslaModelS.prototype = function() { var go = function() { // Rotate wheels }; var stop = function() { // Apply brake pads }; return { pressBrakePedal: stop, pressGasPedal: go } }();

Note how the functions stop and go will be shielded from the returning object due to being outside of returned object"s scope. Since JavaScript natively supports prototypical inheritance, there is no need to rewrite underlying features.

There are many times when one part of the application changes, other parts needs to be updated. In AngularJS, if the $scope object updates, an event can be triggered to notify another component. The observer pattern incorporates just that - if an object is modified it broadcasts to dependent objects that a change has occurred.

Another prime example is the model-view-controller (MVC) architecture; The view updates when the model changes. One benefit is decoupling the view from the model to reduce dependencies.

Wikipedia

As shown in the UML diagram, the necessary objects are the subject , observer , and concrete objects. The subject contains references to the concrete observers to notify for any changes. The Observer object is an abstract class that allows for the concrete observers to implements the notify method.

Let"s take a look at an AngularJS example that encompasses the observer pattern through event management.

// Controller 1 $scope.$on("nameChanged", function(event, args) { $scope.name = args.name; }); ... // Controller 2 $scope.userNameChanged = function(name) { $scope.$emit("nameChanged", {name: name}); };

With the observer pattern, it is important to distinguish the independent object or the subject .

It is important to note that although the observer pattern does offer many advantages, one of the disadvantages is a significant drop in performance as the number of observers increased. One of the most notorious observers is watchers . In AngularJS, we can watch variables, functions, and objects. The $$digest cycle runs and notifies each of the watchers with the new values whenever a scope object is modified.

We can create our own Subjects and Observers in JavaScript. Let"s see how this is implemented:

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.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

The Publish/Subscribe pattern, however, uses a topic/event channel that sits between the objects wishing to receive notifications (subscribers) and the object firing the event (the publisher). This event system allows code to define application-specific events that can pass custom arguments containing values needed by the subscriber. The idea here is to avoid dependencies between the subscriber and publisher.

This differs from the Observer pattern since any subscriber implementing an appropriate event handler to register for and receive topic notifications broadcast by the publisher.

Many developers choose to aggregate the publish/subscribe design pattern with the observer though there is a distinction. Subscribers in the publish/subscribe pattern are notified through some messaging medium, but observers are notified by implementing a handler similar to the subject.

In AngularJS, a subscriber "subscribes" to an event using $on("event", callback), and a publisher "publishes" an event using $emit("event", args) or $broadcast("event", args).

Singleton

A Singleton only allows for a single instantiation, but many instances of the same object. The Singleton restricts clients from creating multiple objects, after the first object created, it will return instances of itself.

Finding use cases for Singletons is difficult for most who have not yet used it prior. One example is using an office printer. If there are ten people in an office, and they all use one printer, ten computers share one printer (instance). By sharing one printer, they share the same resources.

Var printer = (function () { var printerInstance; function create () { function print() { // underlying printer mechanics } function turnOn() { // warm up // check for paper } return { // public + private states and behaviors print: print, turnOn: turnOn }; } return { getInstance: function() { if(!printerInstance) { printerInstance = create(); } return printerInstance; } }; function Singleton () { if(!printerInstance) { printerInstance = intialize(); } }; })();

The create method is private because we do not want the client to access this, however, notice that the getInstance method is public. Each officer worker can generate a printer instance by interacting with the getInstance method, like so:

Var officePrinter = printer.getInstance();

In AngularJS, Singletons are prevalent, the most notable being services, factories, and providers. Since they maintain state and provides resource accessing, creating two instances defeats the point of a shared service/factory/provider.

Race conditions occur in multi-threaded applications when more than one thread tries to access the same resource. Singletons are susceptible to race conditions, such that if no instance were initialized first, two threads could then create two objects instead of returning and instance. This defeats the purpose of a singleton. Therefore, developers must be privy to synchronization when implementing singletons in multithreaded applications.

Conclusion

Design patterns are frequently used in larger applications, though to understand where one might be advantageous over another, comes with practice.

Before building any application, you should thoroughly think about each actor and how they interact with one another. After reviewing the Module , Prototype , Observer , and Singleton design patterns, you should be able to identify these patterns and use them in the wild.

Возможно, вы уже знакомы с такими фреймворками/библиотеками как ReactJS, Angular или jQuery, но знаете ли вы, зачем они были созданы? Какую проблему решают? Какой шаблон проектирования используют? Шаблоны проектирования очень важно знать, и, могу предположить, некоторые из разработчиков-самоучек могли их пропустить. Так что же такое шаблоны проектирования? Что они делают? Как мы можем их использовать? Почему их следует использовать?

Что такое шаблоны проектирования и зачем их использовать?

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

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

Некоторые из плюсов использования шаблонов проектирования:

  • Не нужно изобретать велосипед (ленивый программист => хороший программист)
  • Находишь общий язык с разработчиками
  • Выглядишь круто и профессионально
  • Если ты самоучка, то это поможет тебе изрядно выделиться среди конкурентов
  • Эти шаблоны проходят сквозь все языки программирования

Типы шаблонов и примеры некоторых из них

Порождающие шаблоны (Creational): создание новых объектов.

  1. Конструктор (Constructor)
  2. Модульный (Module)
  3. Фабричный метод (Factory)
  4. Одиночка (Singletion)

Структурные шаблоны(Structural): упорядочивают объекты.

  1. Декоратор(Decorator)
  2. Фасад (Façade)

Поведенческие (Behavioral): как объекты соотносятся друг с другом.

  1. Наблюдатель(Observer)
  2. Посредник(Mediator)
  3. Команда(Command)

Порождающее шаблоны

Эти шаблоны используются для создания новых объектов.

Constructor

Создает новые объекты в их собственной области видимости.

// Constructor Pattern //
// Используйте для создания новых объектов в их собственной области видимости. var Person = function(name, age, favFood) {
this.name = name;
this.age = age;
this.favFood = favFood;
}; // Прототип позволяет всем экземплярам Person ссылаться на него без повторения функции.
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}, I"m ${this.age} years old, and my favorite food is ${this.favFood}`); // new создает объект {} и передает "this" в конструктор
// Конструктор устанавливает значение для этого объекта и возвращает его.
var bob = new Person("Bob", 22, "Chicken");
bob.greet();
// Hello, my name is Bob, I"m 22 years old, and my favorite food is Chicken // ES6 / ES2015 Классы
class Vehicle {
constructor(type, color) {
this.type = type;
this.color = color;
} getSpecs() {
console.log(`Type: ${this.type}, Color: ${this.color}`);
}
}; var someTruck = new Vehicle("Truck", "red");
someTruck.getSpecs();

Module

Используйте для инкапсуляции методов.

// Module Pattern //
// Используется для инкапсуляции кода var myModule = (function() {
// Приватная переменная
var memes = ["cats", "doge", "harambe"];

Var getMemes = function() {
return memes;
}; // возвращает то, к чему вы хотите разрешить доступ в объекте
// то, как он это возвращает действительно делает его показателем модульного шаблона проектирования return {
getMemes: getMemes
};
})(); console.log(myModule.getMemes()); // массив мемов
console.log(myModule.memes); // undefined

Factory

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

// Factory Pattern //
// Несколько конструкторов для нашей фабрики function Cat(options) {
this.sound = "Meow";
this.name = options.name;
} function Dog(options) {
this.sound = "Rawr";
this.name = options.name;
} // Animal Factory
function AnimalFactory() {} // Тип Cat по умолчанию
AnimalFactory.prototype.animalType = Cat; // метод для создания новых животных
AnimalFactory.prototype.createAnimal = function(options) {
switch(options.animalType) {
case "cat":
this.animalType = Cat;
break;
case "dog":
this.animalType = Dog;
break;
default:
this.animalType = Cat;
break;
} return new this.animalType(options);
} var animalFactory = new AnimalFactory();
var doge = animalFactory.createAnimal({
animalType: "dog",
name: "Doge"
}); var snowball = animalFactory.createAnimal({name: "Snowball"}); console.log(doge instanceof Dog); // true
console.log(doge); // выводит doge как cat объект
console.log(snowball instanceof Cat); // true
console.log(snowball); // выводит snowball как cat объект

Singleton

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

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

Структурные шаблоны

Как создаются объекты и какие взаимоотношения между ними. Расширяет или упрощает функциональность.

Decorator

Используйте, чтоб добавлять новую функциональность объектам (Расширяет функциональность ).

// Decorator Pattern //
// Простой конструктор var Person = function (name) {
this.name = name;
} Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
} var uniqueBob = new Person("Bob"); // может быть добавлено к нему без изменения конструктора Person
uniqueBob.hobbies = ["Cooking", "Running"]; uniqueBob.greet = function() {
console.log("My hobbies are: ", this.hobbies);
}; uniqueBob.greet(); // Другой способ
var CoolPerson = function(name, catchPhrase) {
Person.call(this, name);
this.catchPhrase = catchPhrase;
}; // включает в себя prototypes от Person
CoolPerson.prototype = Object.create(Person.prototype); // изменяет прототип
CoolPerson.prototype.greet = function() {
Person.prototype.greet.call(this);
console.log(this.catchPhrase);
}; var coolDude = new CoolPerson("Jeff", "Aaaayyy");
console.log(coolDude);
coolDude.greet();

Facade

Используйте для создания простого интерфейса (упрощает функциональность , как например jQuery).

// Facade Pattern //
// абстрагирует он некоторых сложных/неряшлевых вещей var $ = function (target) {
return new MemeQuery(target);
}; function MemeQuery (target) {
this.target = document.querySelector(target);
} MemeQuery.prototype.html = function(html) {
this.target.innerHTML = html;
return this;
}; // теперь, все, что мы будем видеть и использовать это $
$("#myParagraph").html("Meeemee").html("Some JS design patterns"); //окей, возможно это и не лучший пример..
// просто посмотрите в исходный код jQuery, там полно примеров фасада
// он нужен просто для того, чтоб увести нас от того, чтоб заострять внимание на сложностях проектирования и сделать проектирование быстрее и проще.

Поведенческие шаблоны

Распределяют обязанности между объектами и тем, как они сообщаются.

Observer

Позволяет объектам наблюдать за объектами и быть оповещенными об изменениях.

// Observer Pattern //
// https://github.com/CodeDraken/emtr/blob/master/emitter.js

Mediator

Один объект контролирует сообщение между объектами, поэтому объекты не сообщаются друг с другом на прямую.

// Mediator Pattern //
// Извините, я очень устал для того, чтоб придумывать примеры:P

Command

Инкапсулирует вызов метода в один объект.

// Command Pattern //
// Пример из:
(function(){
var carManager = { // Запросить информацию
requestInfo: function(model, id){
return "The information for " + model + " with ID " + id + " is foobar";
},
// Купить машину
buyVehicle: function(model, id){
return "You have successfully purchased Item " + id + ", a " + model;
}, // Организвать прсмотр
arrangeViewing: function(model, id){
return "You have successfully booked a viewing of " + model + " (" + id + ") ";
}
};
})(); carManager.execute = function (name) {
return carManager && carManager.apply(carManager, .slice.call(arguments, 1));
}; carManager.execute("arrangeViewing", "Ferrari", "14523");
carManager.execute("requestInfo", "Ford Mondeo", "54323");

Фуух, сколько этих шаблонов… Я надеюсь, вы еще здесь, потому что у нас осталось еще 22 шаблона для рассмотрения. Шучу, это все! Но есть еще шаблоны, которые следует изучить, поэтому я добавлю ссылки для тех, кто хочет узнать об этом побольше.

Some Design Patterns in JavaScript

P.S. Это мой первый перевод для medium. Спасибо всем, что прочитали! Надеюсь, вы действительно узнали что-то новое. Буду рада услышать ваши комментарии!

Шаблоны проектирования помогут сделать разработку комфортной и понятной. Давайте начнем изучать паттерны JavaScript вместе.

Модуль и Синглтон

Автор на примерах показывает паттерны JavaScript – Module и Singleton. Очень полезно инкапсулировать состояние, структуру и любую приватную информацию объекта. Для решения этой задачи используют шаблон “Модуль”, который реализует все задуманное через замыкания. Другим полезным и часто используемым шаблоном, является “Синглтон”. Этот шаблон предоставляет одну глобальную точку доступа к экземпляру и гарантирует наличие только одного такого объекта в приложении.

Observer

В этой лекции рассматривается самый простой для понимания паттерн – наблюдатель. Он основан на зависимости один ко многим: при изменении состояния одного объекта (observer), все зависимые объекты (observable) оповещаются об этом. Самое главное в общении этих двух объектов, что не нужна прямая связь между объектом и субъектом. Это позволяет добиться модульности клиентской части кода.

Strategy

Допустим, есть объект, поведение которого в разный момент времени и при разных обстоятельствах должно быть различным. Для того чтобы описать и определить его поведение, можно использовать условный оператор, если вариантов поведения немного. В ином случае нужно использовать шаблон “Стратегия”.

Decorator или обертка

В этом видео разбираемся с очень популярным паттерном в JS – Decorator. Если простым языком, то в этом паттерне используется наследование, т. е. происходит изменение или дополнение поведения существующей функции. Объект получает функцию, а возвращает обертку, в которой происходят все события.

А теперь начнем изучать паттерны JavaScript вместе с углубленными знаниями проектирования.

Контекст и шаблоны вызова функций, call/apply

Здесь автор начинает серию уроков по расширенному JS, а точнее по углубленной базе для новичков. Рассмотрению подвергается поведение this в зависимости от способа вызова функции. Также объясняется на примерах ценность паттерна apply, относительно привязки this к obj и паттерна call, который получает не параметры, а список аргументов при вызове.

Атрибуты свойств, get/set

Для более гибкого управления абсолютно всеми свойствами объекта и даже видимостью в цикле, изменяемостью и прочими, есть специальные возможности. Define property – метод, позволяющий настроить деликатный доступ к свойствам объекта, используя флаги configurable, enumerable и т. д.

Асинхронность, стек вызовов, промисы

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

В этой статье мы расскажем об общих шаблонах проектирования в JS. Эти шаблоны предлагают разработчикам способы решения технических проблем многоразовыми и элегантными способами. Хотите улучшить ваши JavaScript навыки? Тогда читайте дальше.

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

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

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

Разновидности шаблонов проектирования

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

Вкратце о них:

  1. Порождающие шаблоны (creational patterns) сосредоточены на способах создания объектов или классов. Это может показаться простым (и это в некоторых случаях оно так и есть), но большие приложения должны контролировать процесс создания объекта.
  2. Структурные шаблоны (structural design patterns) сосредоточены на том, чтобы управлять отношениями между объектами так, чтобы ваше приложение было построено масштабируемым способом. Ключевым аспектом структурной модели является обеспечение того, что изменение в одной части приложения не влияет на все другие части.
  3. Поведенческие шаблоны (behavioral patterns) сосредоточены на связи между объектами

Примечание о классах в JavaScript

Читая о дизайн шаблонах, вы часто будете видеть ссылки на классы и объекты. Это может привести к путанице, поскольку JavaScript на самом деле не использует “class” (класс), более правильным является термин “data type” (тип данных).

Типы данных в JavaScript

JavaScript является объектно-ориентированным языком, где объекты наследуют от других объектов, в концепции известной как прототипное наследство. Типы данных (data types) могут быть созданы путем определения того, что называется “функцией конструктора”, например:

Function Person(config) { this.name = config.name; this.age = config.age; } Person.prototype.getAge = function() { return this.age; }; var tilo = new Person({name:"Tilo", age:23 }); console.log(tilo.getAge());

Обратите внимание на использование prototype при определении методов на Person типе данных. Так как несколько Person объектов будут ссылаться на тот же прототип, это позволит getAge() методу быть разделенным всеми экземплярами Person типа данных, нежели его переопределения для каждого экземпляра. Кроме того, любой тип данных, который наследует от Person, будет иметь доступ к методу getAge().

Работа с конфиденциальностью

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

Var retinaMacbook = (function() { //Приватные переменные var RAM, addRAM; RAM = 4; //Приватные методы addRAM = function (additionalRAM) { RAM += additionalRAM; }; return { //Публичные переменные и методы USB: undefined, insertUSB: function (device) { this.USB = device; }, removeUSB: function () { var device = this.USB; this.USB = undefined; return device; } }; })();

В приведенном выше примере, мы создали retinaMacbook объект, с публичными и приватными переменными и методами. Вот, как мы будем это использовать:

RetinaMacbook.insertUSB("myUSB"); console.log(retinaMacbook.USB); //logs out "myUSB" console.log(retinaMacbook.RAM) //logs out undefined

Порождающие шаблоны

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

Строительный шаблон (Builder pattern)

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

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

Например, вы, наверняка, делали это бесчисленное количество раз в jQuery:

Var myDiv = $("

This is a div.
"); //myDiv теперь представляет jQuery объект ссылающейся на DOM узел. var someText = $("

"); //someText это jQuery объект ссылающейся на HTMLParagraphElement var input = $("");

Взгляните на три примера выше. В первом из них, мы прошли в

элемент с некоторым контентом. Во втором, мы прошли в пустой тег

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

Переменная $ адаптирует строительный шаблон в jQuery. В каждом примере, нам был возвращен jQuery DOM объекти и имелся доступ ко всем методам, предоставляемых библиотекой jQuery, и не в одном из моментов, мы не вызывали document.createElement. JS library обработала все это за закрытыми дверями.

Представьте себе, как много было бы работы, если нам пришлось бы создавать элемент DOM и вставлять содержимое в него! Используя строительный шаблон, мы можем сосредоточить свое внимание на типе и содержание объекта, а не на его явном создании.

Прототипный шаблон (Prototype pattern)

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

Прототипный шаблон - это шаблон, в котором объекты создаются на основе шаблона существующего объекта путем клонирования.

Это простой и естественный способ реализации наследования в JavaScript. Например:

Var Person = { numFeet: 2, numHeads: 1, numHands:2 }; //Object.create берет свой ​​первый аргумент и применяет его к прототипу нового объекта. var tilo = Object.create(Person); console.log(tilo.numHeads); //результат 1 tilo.numHeads = 2; console.log(tilo.numHeads) //результат 2

Свойства (и методы) в Person объекте, применяются к прототипу объекта tilo. Мы можем переопределить свойства объекта tilo, если хотим, чтобы они были разными.

В приведенном выше примере, мы использовали Object.create (). Однако, Internet Explorer 8 не поддерживает новый метод. В этих случаях мы можем имитировать его поведение:

Var vehiclePrototype = { init: function (carModel) { this.model = carModel; }, getModel: function () { console.log("The model of this vehicle is " + this.model); } }; function vehicle (model) { function F() {}; F.prototype = vehiclePrototype; var f = new F(); f.init(model); return f; } var car = vehicle("Ford Escort"); car.getModel();

Структурные шаблоны

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

Компоновщик (шаблон проектирования)

  • Шаблон Компоновщик, - это ещё один вид шаблона, который вы, вероятно, использовали без осознания того.

Итак, что же это значит? Давайте, рассмотрим следующий пример в jQuery (у большинства JS библиотек будут эквиваленты этому):

$(".myList").addClass("selected"); $("#myItem").addClass("selected"); //Не делайте этого на больших таблицах, это всего лишь пример. $("#dataTable tbody tr").on("click", function(event){ alert($(this).text()); }); $("#myButton").on("click", function(event) { alert("Clicked."); });

Большинство библиотек JavaScript обеспечивают последовательное API, независимо от того, имеем мы дело с одним элементом DOM или массивом DOM элементов. В первом примере, мы можем добавить selected класс ко все элементам подобраных селектором.myList, но мы также можем использовать этот же метод, когда речь идет об еденичном DOM элементе, #myItem. Точно так же можно приложить обработчик событий с помощью on() метода на нескольких узлах, или на одном узле через тот же API.

Благодаря использованию композитных макетов, jQuery (и многие другие библиотеки) предоставляют нам упрощенный API.

Composite шаблон иногда может вызывать проблемы. В слабо расписанном языке, таком как JavaScript, полезным будет знать, имеем ли мы дело с одним элементом или несколькими элементами. Так как компоновщик шаблон использует одинаковый API для обоих, мы зачастую можем принять одно за другое и в конечном итоге столкнуться с неожиданной ошибкой. Некоторым библиотекам, таким как YUI3, предлагают два отдельных метода получения элементов (Y.one() vs Y.all()).

Фасад (шаблон проектирования)

Вот еще один паттерн, который мы воспринимаем как должное. Объект, который абстрагирует работу с несколькими классами, объединяя их в единое целое.

Фасад паттерн предоставляет пользователю простой интерфейс, скрыв его основные сложности.

Фасад шаблон, почти всегда улучшает удобство использования части программного обеспечения. Использование jQuery в качестве примера, одним из наиболее распространенных методов библиотеки, является ready() метод:

$(document).ready(function() { //весь ваш код идет сюда... });

Метод ready() фактически реализует фасад. Если взглянуть на источник, вот что вы найдете:

Ready: (function() { ... //Mozilla, Opera, и Webkit if (document.addEventListener) { document.addEventListener("DOMContentLoaded", idempotent_fn, false); ... } //IE модель событий else if (document.attachEvent) { // обеспечьте firing до onload; может быть поздно, но безопасно для iframes document.attachEvent("onreadystatechange", idempotent_fn); // Резерв для window.onload, который всегда работает window.attachEvent("onload", idempotent_fn); ... } })

Метод ready() не такой уж простой. jQuery нормализует непостоянство браузера, чтобы ready() сработал в нужный момент. Однако, как разработчик, вы будете представлены с простым интерфейсом.

Большинство примеров фасад шаблонов, следует этому принципу. Для его реализации, мы обычно полагаемся на условные операторы, но представляем его в виде простого интерфейса для пользователя. Другие методы реализации этого паттерна включают в себя animate() и css().

Поведенческие шаблоны

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

Шаблон Наблюдатель (Observer)

Вот что говорится о Наблюдателе:

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

Нам нужно три метода для описания этого паттерна:

publish(data): вызывается объектом, когда у него есть уведомление. Некоторые данные могут быть переданы с помощью этого метода.
subscribe(observer): вызывается объектом, для добавления наблюдателя в свой список наблюдателей.
unsubscribe(observer): вызывается объектом, чтобы удалить наблюдателя из списка наблюдателей.
Большинство современных JavaScript библиотек поддерживают эти три метода, как часть своей инфраструктуры событий. Обычно есть on() или attach() метод, trigger() или fire() метод, и off() или detach() метод. Рассмотрим следующий сниппет:

//Мы просто создаем связь между методами jQuery событий var o = $({}); $.subscribe = o.on.bind(o); $.unsubscribe = o.off.bind(o); $.publish = o.trigger.bind(o); // Usage document.on("tweetsReceived", function(tweets) { //perform some actions, then fire an event $.publish("tweetsShow", tweets); }); //Мы можем subscribe к этому событию, а затем fire наше собственное событие. $.subscribe("tweetsShow", function() { //display the tweets somehow .. //publish после того, как оно показано $.publish("tweetsDisplayed); }); $.subscribe("tweetsDisplayed, function() { ... });

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

Шаблон Посредник (Mediator)

Последний паттерн, который мы рассмотрим, это Посредник. Он похож на паттерн Наблюдатель, но с некоторыми заметными отличиями.

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

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

$("#album").on("click", function(e) { e.preventDefault(); var albumId = $(this).id(); mediator.publish("playAlbum", albumId); }); var playAlbum = function(id) { … mediator.publish("albumStartedPlaying", {songList: [..], currentSong: "Without You"}); }; var logAlbumPlayed = function(id) { //Логин альбом на бенэнде }; var updateUserInterface = function(album) { //Апдейт UI для отображения того, что играет }; //Посредник subscriptions mediator.subscribe("playAlbum", playAlbum); mediator.subscribe("playAlbum", logAlbumPlayed); mediator.subscribe("albumStartedPlaying", updateUserInterface);

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

Заключение

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

Перевод ()
Источник фото - Fotolia.ru



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: