Использование паттернов проектирования в 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[name || PetConstructor.name] = PetConstructor;
};

PetFactory.create = function(petName) {
  var PetConstructor = this[petName];
  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([buttonOk, buttonCancel, title]);
}


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

var newPopup = UI.createPopup(BluePopupFactory);


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

Ну. И что?
Реклама
Комментарии 30
  • 0
    Будет здорово, если в будущих статьях список реализаций пополнится. Спасибо.
    • 0
      Мне не хотелось загромождать описание паттернов большим количеством кода, особенно с учетом того, что среди нескольких реализаций как правило есть более приемлемая, чем все остальные. Но если на это есть спрос, то подумаю как лучше структурировать реализации так, чтобы не запутаться в них и прояснить плюсы минусы и подводные камни.
      • 0
        С описанием паттернов большинство на хабре уже худо-бедно знакомо, а вот различные варианты их реализации — уже более интересный материал. Чтобы не загромождать статью используйте спойлер.
        • 0
          Под описанием я имел ввиду именно основную на мой взгляд реализацию, как можете заметить собственно текстового описания на каждый паттерн приходится по несколько предложений. Но в ообщем я согласен, что чем больше реализаций, тем лучше.
      • +5
        • 0
          Спасибо! Самое смешное, что первый линк даже есть в закладках, но был погребен под тоннами ссылок на бесполезне вещи.
      • +13
        На мой взгляд, многие классические паттерны в javascript просто не имеют смысл, так как этот язык гибче тех, на которые они в первую очередь рассчитаны.

        Если бы стояла задача описать этот паттерн одной фразой, то она получилась бы примерно следующей: Singleton — это класс, который может иметь только один экземпляр.


        Но в javascript нет классов! Проблема высосана из пальца.

        Но у него есть и существенный недостаток: основная цель паттерна singleton — обеспечить доступ к объекту без использования глобальных переменных, а данный способ предоставляет доступ к переменной app только в текущей области видимости.


        Facepalm. А если сделать функцию она магическим образом не будет глобальной? В requirejs можно возвращать и объект, вы не поверите.

        Суть фабричного метода — не в том, что вы описали, а в том, чтобы предоставить возможность подклассам переопределить класс (или в общем случае — способ создания) объектов, которые создаются в процессе работы этого класса. В javascript такая задача тоже может появиться, но она решается просто добавление конструктора в объект и переопределением при необходимости:
        var obj = { createAnimal: function(){ return new this.animal(); } }; obj.animal = Dog; obj.animal = Cat;

        То что вы описали тоже решается без каких то проблем и мысле о «паттернах»:
        var animal = { cat: Cat, dog: Dog }; new animal['cat'];
        • +1
          Хотел написать то же, вы меня опередили.
          Т.к. в JS класс — такой же объект языка, как и переменная, то совершенно нет никакого смысла прятать обращение к переменной за вызов метода статического класса. Всё равно нужно держать где-то в области видимости некую переменную для доступа к синглтону, только лишний вызов функции добавится.
          • 0
            дополню:
            обращение к синглтону подобным образом нелогично и противоречит здравому смыслу
            var s = new Singleton();
            
            Что делает оператор new? Создает новый объект. А тут всегда будет возвращатся один и тот же.
            • 0
              Еще код с использованием синглтонов сложнее покрывать тестами.
            • –2
              На мой взгляд, многие классические паттерны в javascript просто не имеют смысл, так как этот язык гибче тех, на которые они в первую очередь рассчитаны.

              Дело Ваше, на мой взгляд имеют.

              Но в javascript нет классов!

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

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

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

              Суть фабричного метода — не в том, что вы описали, а в том, чтобы предоставить возможность подклассам переопределить класс (или в общем случае — способ создания) объектов, которые создаются в процессе работы этого класса

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


              var obj = { createAnimal: function(){ return new this.animal(); } }; obj.animal = Dog; obj.animal = Cat;

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

              То что вы описали тоже решается без каких то проблем и мысле о «паттернах»:
              var animal = { cat: Cat, dog: Dog }; new animal['cat'];

              Мыслей о паттернах может и не появляться, но этот код также является примитивной реализацией фабрики.
              • 0
                можно возвращать объект, но это будет менее гибко: мы теряем возможность безболезненного избавления от дублирования кода путем наследования.

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

                var singleton = new Parent();
                singleton.overloadedMethod = function(){};
                return singleton;
                

                Зачем создавать лишние сущности?
                • 0
                  Отнаследованный можно, но что будете делать, если понадобится отнаследоваться от объекта singleton? Создавать обертку, которая будет содержать все методы объекта singleton, для переопределенных реализовывать новую логику, для сохранившихся вызывать метод сохраненного в обертке singleton? Это и называется менее гибко, какой объем работы надо будет проделать чтобы переименовать пару методов, а если наследников будет несколько? Можно еще конечно склонировать объект и переопределить нужные методы, но тут можно нарваться на грабли полного копирования объекта, т.к. поверхностное тут не подойдет, на которое можно потратить немало времени и между прочим по вычислительным ресурсам это тоже выйдет не самая легкая операция, вместо того чтобы ипользовать функции-констркуторы и прототипное наследование.
                  • 0
                    JavaScript очень гибкий, можно и от объекта наследоватся:

                    var child = Object.create(singleton);
                    
                    • 0
                      в чисто прототипном ООП только так и делается. А джаваскриптовые функции-конструкторы созданы для совместимости с Java-оператором new.
                    • 0
                      Прототипное наследование вызывает те же проблемы, что и shallow copy. Даже больше — при клонировании хотя бы простые значения можно изменять в унаследованном объекте независимо.

                      Наследование объекта от объекта:

                      var parent = {},
                          child;
                      
                      function Surrogate(){}
                      Surrogate.prototype = parent;
                      child = new Surrogate();
                      


                      или

                      var parent = {},
                          child = Object.create(parent,{});
                      


                      или

                      var parent = {},
                          child = {}, 
                          key;
                      
                      for (key in parent){
                          if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key)){
                              child[key] = parent[key];
                          }
                      }
                      


                      Наследование от синглтона сомнительно само по себе.
                  • 0
                    Раз уж вы ссылаетесь на Стефанова, приведу пару цитат из него:
                    Про синглтоны:
                    Usefulness warning: The following discussion is not so useful as a practical pattern but more as a theoretical exercise in imitating the workarounds for issues related to the designs of some (statically, strongly typed) class-based languages in which functions are not first-class
                    objects.

                    Про фабрики (цитату вы прервали на самом интересном месте)
                    The second point is more important in static class languages in which it may be nontrivial to create instances of classes, which are not known in advance (in compile time). In javaScript, this part of the implementation is quite easy.
                • 0
                  Синглтон — дурацкий антипаттерн, убивающий читабельность и понимабельность кода чуть более, чем совсем.
                  Тот факт, что мы получаем ссылку не должне скрываться за декларацией создания нового объекта, это просто ложное утверждение, типа true = false # Happy debugging!.

                  Правильно использовать #clone() или фабрику объектов.
                  • 0
                    Чаще всего он применяется когда конечному пользователю класса не принципиально ссылка это или новый объект: объект для доступа к БД, объект текущего пользователя и пр. Память и вычисления экономятся, объект как-будто бы новый
                    • 0
                      Эмм… как это не важно? Объект он на то и объект, что обладает собственной «уникальностью» (ну, в грубом приближении). И когда мы создаем новый — он должен быть новым.
                      А то потом получаем всякое, что у Пети пол женским стал, потому что кто-то новую запись сделал, взяв как конструктор объект пользователя.
                      • 0
                        Текущий пользователь всегда один, и если описан класс именно для текущего пользователя, то создание объекта каждый раз при необходимости считать его данные, подтягивание данных из бд, и т.д. просто расточительно. В то же время при сохранении единого объекта и экономии ресурсов мы сохраняем страрый привычный интерфейс создания объекта. Повторю: речь именно о текущем пользователе, а Вы пытаетесь его применить к какому-то абстрактному пользователю.
                        • 0
                          Так яж про семантику.
                          user = new User # as singleton — плохо
                          user = Registry.get 'current_user' — хорошо

                          Использование синглтона как раз и отражает первую проблему в программировании — называние вещей.
                          • 0
                            Ну, что-то в этом есть, но опять же дело вкуса. Называете класс CurrentUserSingleton и проблем точно не будет
                            • +1
                              Называете класс CurrentUserSingleton и проблем точно не будет

                              Вы меня явно недооцениваете :)

                              А кроме шуток — вот уже не первый раз в итоге скатываюсь к тому, что DI + Registry наше все.
                              Тут нам и ре-юзабельность малой кровью, и возможность моки подпихивать и черт в ступе, если нужен.

                              Получается, к сожалению, такой если не god-object, конечно, но просветленный — это точно, но ничего не поделать, где-то в системе должны же зависимости хранится.
                    • 0
                      любимые слова: понимабельность и мониторирование, да?
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • –1
                        Прецеде́нт(случай)… Заебали!
                        • НЛО прилетело и опубликовало эту надпись здесь
                      • –1
                        github.com/nooks/singleton.js думаю более лучший метод реализации singleton
                        • 0
                          По моему такая реализация singleton по книге Стефанова более приемлемая:
                          var Universe; (function () { var instance; Universe = function Universe () { if (instance) { return instance; } instance = this; this.a = 10; } }()); Universe.prototype.b = 1;

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

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