Шаблоны проектирования в JavaScript простыми словами. Базовые шаблоны проектирования в JavaScript JavaScript использует прототипное наследование




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

Простая фабрика

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

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

Пример реализации

Создадим неявный интерфейс для всех дверей:

/* Door getWidth() getHeight() */ class WoodenDoor { constructor(width, height){ this.width = width this.height = height } getWidth(){ return this.width } getHeight(){ return this.height } }

Организуем фабрику, которая будет их производить:

Const DoorFactory = { makeDoor: (width, height) => new WoodenDoor(width, height) }

Все, можно работать:

Const door = DoorFactory.makeDoor(100, 200) console.log("Width:", door.getWidth()) console.log("Height:", door.getHeight())

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

Фабричный метод

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

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

Пример реализации

Начнем с самого гамбургера:

Class Burger { constructor(builder) { this.size = builder.size this.cheeze = builder.cheeze || false this.pepperoni = builder.pepperoni || false this.lettuce = builder.lettuce || false this.tomato = builder.tomato || false } }

А вот и Строитель:

Class BurgerBuilder { constructor(size) { this.size = size } addPepperoni() { this.pepperoni = true return this } addLettuce() { this.lettuce = true return this } addCheeze() { this.cheeze = true return this } addTomato() { this.tomato = true return this } build() { return new Burger(this) } }

Вуаля! Вот наш бургер:

Const burger = (new BurgerBuilder(14)) .addPepperoni() .addLettuce() .addTomato() .build()

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

Синглтон

У страны должен быть единственный президент, иначе начнется беспорядок.

Этот паттерн оборачивает объект и динамически изменяет его поведение.

Пример реализации

Возьмем для примера кофе. Самый простой кофе, реализующий соответствующий интерфейс:

/* Coffee interface: getCost() getDescription() */ class SimpleCoffee{ getCost() { return 10 } getDescription() { return "Simple coffee" } }

Мы хотим иметь возможность добавлять в кофе различные добавки, для этого создадим некоторые декораторы:

Class MilkCoffee { constructor(coffee) { this.coffee = coffee } getCost() { return this.coffee.getCost() + 2 } getDescription() { return this.coffee.getDescription() + ", milk" } } class WhipCoffee { constructor(coffee) { this.coffee = coffee } getCost() { return this.coffee.getCost() + 5 } getDescription() { return this.coffee.getDescription() + ", whip" } } class VanillaCoffee { constructor(coffee) { this.coffee = coffee } getCost() { return this.coffee.getCost() + 3 } getDescription() { return this.coffee.getDescription() + ", vanilla" } }

Теперь вы можете сделать кофе на свой вкус:

Let someCoffee someCoffee = new SimpleCoffee() console.log(someCoffee.getCost())// 10 console.log(someCoffee.getDescription())// Простой кофе someCoffee = new MilkCoffee(someCoffee) console.log(someCoffee.getCost())// 12 console.log(someCoffee.getDescription())// Простой кофе, молоко someCoffee = new WhipCoffee(someCoffee) console.log(someCoffee.getCost())// 17 console.log(someCoffee.getDescription())// Простой кофе, молоко, сливки someCoffee = new VanillaCoffee(someCoffee) console.log(someCoffee.getCost())// 20 console.log(someCoffee.getDescription())// Простой кофе, молоко, сливки, ваниль

Фасад

Чтобы включить компьютер достаточно нажать кнопку. Это очень просто, но внутри включающегося компьютера происходит много сложных действий. Простой интерфейс к сложной системе – это и есть Фасад .

Пример реализации

Создадим класс компьютера:

Class Computer { getElectricShock() { console.log("Ouch!") } makeSound() { console.log("Beep beep!") } showLoadingScreen() { console.log("Loading..") } bam() { console.log("Ready to be used!") } closeEverything() { console.log("Bup bup bup buzzzz!") } sooth() { console.log("Zzzzz") } pullCurrent() { console.log("Haaah!") } }

и простой Фасад для его сложных функций:

Class ComputerFacade { constructor(computer) { this.computer = computer } turnOn() { this.computer.getElectricShock() this.computer.makeSound() this.computer.showLoadingScreen() this.computer.bam() } turnOff() { this.computer.closeEverything() this.computer.pullCurrent() this.computer.sooth() } }

Так работать с компьютером намного проще:

Const computer = new ComputerFacade(new Computer()) computer.turnOn() // Ouch! Beep beep! Loading.. Ready to be used! computer.turnOff() // Bup bup buzzz! Haah! Zzzzz

Приспособленец

В поездах дальнего следования воду для горячих напитков кипятят в больших емкостях – сразу для всех. Это позволяет экономить электричество (или газ).

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

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

Пример реализации

Соискатели хотят получать уведомления:

Const JobPost = title => ({ title: title }) class JobSeeker { constructor(name) { this._name = name } notify(jobPost) { console.log(this._name, "has been notified of a new posting:", jobPost.title) } }

А Доска объявлений может эти уведомления рассылать:

Class JobBoard { constructor() { this._subscribers = } subscribe(jobSeeker) { this._subscribers.push(jobSeeker) } addJob(jobPosting) { this._subscribers.forEach(subscriber => { subscriber.notify(jobPosting) }) } }

// создаем подписчиков const jonDoe = new JobSeeker("John Doe") const janeDoe = new JobSeeker("Jane Doe") const kaneDoe = new JobSeeker("Kane Doe") // создаем доску объявлений // подписываем соискателей const jobBoard = new JobBoard() jobBoard.subscribe(jonDoe) jobBoard.subscribe(janeDoe) // оповещаем подписчиков о новой вакансии jobBoard.addJob(JobPost("Software Engineer")) // John Doe has been notified of a new posting: Software Engineer // Jane Doe has been notified of a new posting: Software Engineer

Посетитель

Чтобы отправиться за границу, нужно получить разрешение (визу). Но оказавшись в стране, вы можете спокойно посещать самые разные места, не спрашивая дополнительного разрешения. Достаточно лишь узнать о них.

Паттерн Посетитель позволяет добавлять объектам дополнительные операции, не изменяя их исходный код.

Пример реализации

Смоделируем зоопарк с разными видами животных:

Class Monkey { shout() { console.log("Ooh oo aa aa!") } accept(operation) { operation.visitMonkey(this) } } class Lion { roar() { console.log("Roaaar!") } accept(operation) { operation.visitLion(this) } } class Dolphin { speak() { console.log("Tuut tuttu tuutt!") } accept(operation) { operation.visitDolphin(this) } }

Теперь мы хотим послушать, какие звуки они издают. Для этого создадим Посетителя:

Const speak = { visitMonkey(monkey){ monkey.shout() }, visitLion(lion){ lion.roar() }, visitDolphin(dolphin){ dolphin.speak() } }

Он просто обращается к каждому классу и вызывает нужный метод:

Const monkey = new Monkey() const lion = new Lion() const dolphin = new Dolphin() monkey.accept(speak) // Ooh oo aa aa! lion.accept(speak) // Roaaar! dolphin.accept(speak) // Tuut tutt tuutt!

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

Const jump = { visitMonkey(monkey) { console.log("Jumped 20 feet high! on to the tree!") }, visitLion(lion) { console.log("Jumped 7 feet! Back on the ground!") }, visitDolphin(dolphin) { console.log("Walked on water a little and disappeared") } }

Monkey.accept(speak) // Ooh oo aa aa! monkey.accept(jump) // Jumped 20 feet high! on to the tree! lion.accept(speak) // Roaaar! lion.accept(jump) // Jumped 7 feet! Back on the ground! dolphin.accept(speak) // Tuut tutt tuutt! dolphin.accept(jump) // Walked on water a little and disappeared

Стратегия

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

Шаблон Стратегия позволяет переключать используемый алгоритм в зависимости от ситуации.

Пример реализации

Воплотить Стратегию в JavaScript помогут функции первого класса.

Const bubbleSort = dataset => { console.log("Sorting with bubble sort") // ... // ... return dataset } const quickSort = dataset => { console.log("Sorting with quick sort") // ... // ... return dataset }

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

Const sorter = dataset => { if(dataset.length > 5){ return quickSort } else { return bubbleSort } }

Теперь можно сортировать массивы:

Const longDataSet = const shortDataSet = const sorter1 = sorter(longDataSet) const sorter2 = sorter(shortDataSet) sorter1(longDataSet) // Sorting with quick sort sorter2(shortDataSet) // Sorting with bubble sort

Состояние

Вы рисуете в Paint. В зависимости от вашего выбора кисть меняет свое состояние: рисует красным, синим или любым другим цветом.

Паттерн Состояние позволяет изменять поведение класса при изменении состояния.

Пример реализации

Создадим текстовый редактор, в котором можно менять состояние текста – жирный, курсив и т. д.

Это функции преобразования:

Const upperCase = inputString => inputString.toUpperCase() const lowerCase = inputString => inputString.toLowerCase() const defaultTransform = inputString => inputString

А вот и сам редактор:

Class TextEditor { constructor(transform) { this._transform = transform } setTransform(transform) { this._transform = transform } type(words) { console.log(this._transform(words)) } }

Можно работать:

Const editor = new TextEditor(defaultTransform) editor.type("First line") editor.setTransform(upperCase) editor.type("Second line") editor.type("Third line") editor.setTransform(lowerCase) editor.type("Fourth line") editor.type("Fifth line") // First line // SECOND LINE // THIRD LINE // fourth line // fifth line

Шаблонный метод

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

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

Пример реализации

Создадим инструмент для тестирования, сборки и разворачивания приложения.

Базовый класс определяет скелет алгоритма сборки:

Class Builder { // Template method build() { this.test() this.lint() this.assemble() this.deploy() } }

А дочерние классы – конкретную реализацию каждого шага:

Class AndroidBuilder extends Builder { test() { console.log("Running android tests") } lint() { console.log("Linting the android code") } assemble() { console.log("Assembling the android build") } deploy() { console.log("Deploying android build to server") } } class IosBuilder extends Builder { test() { console.log("Running ios tests") } lint() { console.log("Linting the ios code") } assemble() { console.log("Assembling the ios build") } deploy() { console.log("Deploying ios build to server") } }

Соберем проект:

Const androidBuilder = new AndroidBuilder() androidBuilder.build() // Running android tests // Linting the android code // Assembling the android build // Deploying android build to server const iosBuilder = new IosBuilder() iosBuilder.build() // Running ios tests // Linting the ios code // Assembling the ios build // Deploying ios build to server

До тех пор, пока вы разрабатываете простые приложения на языке Javascript, у вас не будет особых проблем с выводом данных на веб-страницу.

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

Например, нужно вывести на страницу список пользователей сайта со всеми их параметрами (например, id, имя. Могут быть также возраст, пол, и.т.д.). Предположим, что эти данные находятся внутри объекта usersData

Var usersData = [ { name: "Dima", id: 1 }, { name: "Katy", id: 2 }, { name: "Lena", id: 3 } ];

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

For(var i=0; i" + userData.name[i] + ""; }

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

Но, куда более наглядно будет вывод данных вот в такой форме:

  • :{{:name}}
  • С такой формой записи уже может разобраться и человек, который владеет только языком разметки HTML. Интуитивно можно разобраться, что в фигурных скобках будут подставляться соответствующие значения переменных.

    Такая форма записи становиться возможной при использовании так называемых шаблонов.

    Шаблон – это определенная заготовка в HTML-коде, которая написана по определенным правилам. Когда Javascript-код начинает взаимодействовать с такой заготовкой, код шаблона будет преобразовываться в HTML-код или узлы DOM-дерева на странице.

    Синтаксис шаблонов в Javascript может быть разным. Единого стандарта нет и все будет зависеть от того, какой библиотекой или плагином вы будете пользоваться.

    Вот шаблон, который создает меню на сайте:

    Здесь вместо фигурных скобок используется вот такая форма записи <% %>

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

    MyButton { <% if (boxShadow) { %> box-shadow:<%= boxShadowInset ? "inset" : "" %> <%= boxShadowOffsetX %> <%= boxShadowOffsetY %> <%= boxShadowBlurRadius %> <%= boxShadowSpreadRadius %> <%= boxShadowColor %>; <% } if (!solid && !transparent) { %> background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, <%= bgTopColor %>), color-stop(1, <%= bgBottomColor %>)); background:linear-gradient(to bottom, <%= bgTopColor %> 5%, <%= bgBottomColor %> 100%); filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="<%= bgTopColor %>", endColorstr="<%= bgBottomColor %>",GradientType=0); <% } %>

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

    Интуитивно понятный код, даже не для программиста;

    Более сокращенная форма записи;

    Данные, javascript-код и вывод данных на страницы отделены друг от друга, так ими намного проще управлять.

    В общем, использовать или не использовать шаблоны Javascript на веб-страницах, решать только вам. Я для себя уже решение принял и уже начинаю их использовать в некоторых своих разработках.

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

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

    Singleton

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

    Var app = { property1: "value", property2: "value", ... method1: function () { ... }, ... }

    Этот способ имеет как свои преимущества, так и недостатки. Его просто описать, многие его используют не догадываясь о существовании каких-либо паттернов и эта форма записи будет понятна любому javaScript разработчику. Но у него есть и существенный недостаток: основная цель паттерна singleton - обеспечить доступ к объекту без использования глобальных переменных, а данный способ предоставляет доступ к переменной app только в текущей области видимости. Это означает, что к объекту app мы сможем обратиться из любого места приложения только в том случае если он будет глобальным. Чаще всего это крайне неприемлемо, хорошим стилем разработки на javaScript является использование максимум одной глобальной переменной, в которой инкапсулируется все необходимое. А это означает, что приведенный выше подход мы сможем использовать максимум один раз в приложении.
    Второй способ чуть более сложен, но зато и более универсален:

    Function SomeFunction () { if (typeof (SomeFunction.instance) == "object") { return SomeFunction.instance; } this.property1 = "value"; this.property2 = "value"; SomeFunction.instance = this; return this; } SomeFunction.prototype.method1 = function () { }

    Теперь, используя любую модульную систему (например requirejs) мы в любом месте нашего приложения сможем подключить файл с описанием этой функции-конструктора и получим доступ к нашему объекту, выполнив:

    Var someObj = new SomeFunction ();

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

    Function SomeFunction () { var instance; SomeFunction = function () { return instance; } this.property1 = "value"; this.property2 = "value"; instance = this; }

    Казалось бы вот оно, решение всех проблем, но на место старых проблем приходят новые. А именно: все свойства, занесенные в прототип конструктора после создания экземпляра не будут доступны, т.к. по сути будут записаны в старый конструктор, а не в свежеопределенный. Но и из этой ситуации есть достойный выход:

    Function SomeFunction () { var instance; SomeFunction = function () { return instance; } SomeFunction.prototype = this; instance = new SomeFunction (); instance.constructor = SomeFunction; instance.property1 = "value"; instance.property2 = "value"; return instance; }

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

    Define(, function () { var instance = null; function SomeFunction() { if (instance) { return instance; } this.property1 = "value"; this.property2 = "value"; instance = this; }; return SomeFunction; });

    Factory method

    У фабричного метода две основных цели:
    1) Не использовать явно конкретные классы
    2) Объединить вместе часто используемые методы инициализации объектов
    Простейшей реализацией фабричного метода является такой пример:

    Function Foo () { //... } function Bar () { //... } function factory (type) { switch (type) { case "foo": return new Foo(); case "bar": return new Bar(); } }

    Соответственно создание объектов будет выглядеть так:

    Foo = factory("foo"); bar = factory("bar");

    Можно использовать более элегантное решение:

    Function PetFactory() { }; PetFactory.register = function(name, PetConstructor) { if (name instanceof Function) { PetConstructor = name; name = null; } if (!(PetConstructor instanceof Function)) { throw { name: "Error", message: "PetConstructor is not function" } } this = PetConstructor; }; PetFactory.create = function(petName) { var PetConstructor = this; if (!(PetConstructor instanceof Function)) { throw { name: "Error", message: "constructor "" + petName + "" undefined" } } return new PetConstructor(); };

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

    PetFactory.register("dog", function() { this.say = function () { console.log("gav"); } });

    Ну или таким:

    Function Cat() { } Cat.prototype.say = function () { console.log("meow"); } PetFactory.register(Cat);

    Abstract Factory

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

    Function BluePopup () { //создание всплывающего окна } BluePopup.prototype.attach = function (elemens) { //присоединение других ui-элементов к окну } BluePopupFactory.register("popup", BluePopup); function BluePopupButton () { //создание кнопки для синего всплывающего окна } BluePopupButton.prototype.setText = function (text) { //установка текста на кнопке } BluePopupFactory.register("button", BluePopupButton); function BluePopupTitle () { //создание заголовка для синего окна } BluePopupTitle.prototype.setText = function (text) { //установка текста заголовка } BluePopupFactory.register("title", BluePopupTitle);

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

    Function UI () { //класс, отвечающий за ui-элементы }

    И в него мы добавим метод createPopup:

    UI.createPopup = function (factory) { var popup = factory.create("popup"), buttonOk = factory.create("button"), buttonCancel = factory.create("button"), title = factory.create("title"); buttonOk.setText("OK"); buttonCancel.setText("Cancel"); title.setText("Untitled"); popup.attach(); }

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

    Var newPopup = UI.createPopup(BluePopupFactory);

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