Using design patterns in javaScript: Generating patterns

Hi, Habr!
I was surprised to find that there was no detailed article on the subject on the hub, which immediately prompted me to correct this flagrant injustice.

In conditions when the client part of web applications is getting thicker, business logic is inexorably creeping on the client, and node.js is more and more boldly encroaching on the sovereignty of server technologies. One can not help but think about javaScript architecture design techniques. And in this matter design patterns should undoubtedly help us - template techniques for solving frequently encountered problems. Patterns help build an architecture that will require the least effort from you to make changes if necessary. But do not take them as a panacea, that is, roughly speaking, if the quality of the code is “not a fountain”, it is teeming with hardcode and a rigid connection between logically independent modules, then no patterns will save it. But if the task is to design a scalable architecture, then patterns can be a good help.
But by the way, this article is not about design patterns as such, but about their application in javaScript. In the first part of this article, I will write about the application of generative patterns.


Singleton


If there was a task to describe this pattern with one phrase, then it would turn out approximately the following: Singleton is a class which can have only one copy.
The simplest and most obvious solution in javaScript to implement this pattern is to use objects:

var app = {
  property1: 'value',
  property2: 'value',
  ...
  method1: function () {
    ...
  },
  ...
}


This method has both its advantages and disadvantages. It is easy to describe, many people use it without realizing the existence of any patterns, and any javaScript developer will understand this form of writing. But it also has a significant drawback: the main purpose of the singleton pattern is to provide access to the object without using global variables, and this method provides access to the app variable only in the current scope. This means that we can access the app object from anywhere in the application only if it is global. Most often this is extremely unacceptable, a good style of development in javaScript is to use a maximum of one global variable, in which everything you need is encapsulated. And this means that we can use the above approach at most once in the application.
The second method is a bit more complicated, but more universal:

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 () {
}


Now, using any modular system (for example, requirejs), we can connect a file with a description of this constructor function anywhere in our application and get access to our object by doing:

var someObj = new SomeFunction ();


But this method also has its drawback: the instance is stored simply as a static property of the constructor, which allows anyone to overwrite it. But we want that under any circumstances we can get access from any corner of our application to the desired object. This means that the variable in which we save the instance must be made private, and will help us with this closure.

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


It would seem that this is the solution to all problems, but new problems come to replace the old ones. Namely: all the properties listed in the prototype constructor after creating the instance will not be available, because in fact, they will be written to the old constructor, and not to the newly defined one. But there is a worthy way out of this situation:

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;
}


This way of describing a loner is devoid of all the above disadvantages and is quite suitable for universal use, however, methods for describing a loner using a closure will not work with requirejs, but if you modify them a bit and remove the variable from the closure created by the function itself to the function used in define, then the problem will be solved:

define([], function () {
  var instance = null;
  function SomeFunction() {
    if (instance) {
      return instance;
    }
    this.property1 = 'value';
    this.property2 = 'value';
    instance = this;
  };
  return SomeFunction;
}); 


Factory method


The factory method has two main goals:
1) Do not use explicitly specific classes
2) Combine commonly used methods for initializing objects together
The simplest implementation of the factory method is this example:

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


Accordingly, the creation of objects will look like this:

foo = factory('foo');
bar = factory('bar');


You can use a more elegant solution:

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();
};


In this case, we do not limit ourselves to the number of classes that the factory can generate, we can add them as many as we like in this way:

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


Well or like that:

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


Abstract factory


An abstract factory is used to create a group of interconnected or interdependent objects.
Suppose we have several pop-ups that consist of the same elements, but these elements look different and react differently to user actions. Each of these elements will be created by the factory method, which means that each type of pop-up window needs its own factory of objects.
For example, we describe the BluePopupFactory factory, it has exactly the same structure as PetFactory, so we omit the details and just use it.

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);


Probably we should have a certain class that is responsible for the interface elements.

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


And we will add the createPopup method to it:

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]);
}


As you can see createPopup takes the factory as an argument, creates the popup itself and the buttons with a title for it, and then attaches them to the window.
After that, you can use this method like this:

var newPopup = UI.createPopup(BluePopupFactory);


Accordingly, it is possible to describe an unlimited number of factories and transfer the necessary one when creating the next pop-up window.

Also popular now: