JavaScript
Perfect code
April 2013 4

Использование паттернов проектирования в javaScript: Порождающие паттерны

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

В условиях когда клиентская часть веб-приложений становится все более толстой, бизнес-логика неумолимо переползает на клиент, а на суверенитет серверных технологий все более смело посягает 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
70.3k 647
Comments 30