Синглтон

Намерение

Убедитесь, что у класса есть только один экземпляр, и предоставьте ему глобальную точку доступа.

Мотивация

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

Состав

Как мы можем применить эту идею к JavaScript? Давайте сначала посмотрим, как выглядит ожидаемое поведение этого шаблона:

    var person = new Person();
    var person2 = new Person();
    person === person2 // true

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

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

Этого можно добиться разными способами:

  1. Мы можем использовать глобальные переменные для хранения экземпляра, но не являются ли глобальные переменные плохой практикой? Да, они. Итак, давайте посмотрим другие возможные решения.
  2. Кэширование экземпляра в статическом свойстве конструктора. Единственный недостаток заключается в том, что свойство экземпляра будет общедоступным, а это означает, что внешний код может его изменить, а мы этого не хотим.
  3. Обертывание экземпляра закрытием. Это делает экземпляр закрытым и недоступным для изменений.

Давайте посмотрим, как второй и третий подходы выглядят в коде:

Экземпляр в статическом свойстве

    function Person(name, lastName) {
        // Do we have an existent instance of the constructor? if  so, please return the instance
        if ( typeof Person.instance === 'object') {
            return Person.instance;
        }

        // Define some properties as usual
        this.name = name;
        this.lastName = lastName;

        Person.instance = this;
    }

    var person = new Person('John', 'Doe');
    var person2 = new Person('Alex', 'Foo');
    person === person2 // true

Экземпляр в закрытии

    function Person(name, lastName) {
        var instance = this;
        this.name = name;
        this.lastName = lastName;

        Person = function(name, lastName) {
            return instance;
        }
    }

В первый раз, когда пользователь вызывает конструктор с помощью new, он сохраняет ссылку на экземпляр (this) в переменной с именем instance, переопределяет конструктор и возвращает экземпляр.

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

Важно: instance доступен только из лексической области видимости человека, это также включает тело функции Person, объявленной позже.

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

  Person.prototype.prop1 = 'property 1';

  var person = new Person();
  
  Person.prototype.prop2 = 'property 2';

  var person2 = new Person();
  
  console.log(person.prop1); // logs 'property 1'
  console.log(person2.prop1); // logs 'property 1'
  console.log(person.prop2); // logs undefined
  console.log(person2.prop2); // logs undefined
 
  person.constructor.name; // Person
  person.constructor === Person; // false

Причина, по которой person.constructor === Person регистрирует false, заключается в том, что конструктор экземпляра Person (созданного в первый раз) указывает на первую функцию Person. Во втором вызове конструктора Person мы теряем ссылку на внешнюю функцию, потому что мы перезаписываем ее второй (внутренней) функцией.

Возможное решение этой проблемы:

    function Person() {
        var instance;

        Person = function() {
            return instance;
        }
        // We set the constructor of instance to always be the              function that overwrites Person.
        instance = new Person;
        instance.name = 'Bob';
        return instance;
    }

Другое возможное решение:

    // Alternative Solution 1    
    var Person;
    (function(){
        var instance;

        Person = function Person() {
            if (instance) {
                return instance;
            }
            instance = this;
            instance.name = 'Bob';
        }
    })();
  

Фабрика

Намерение

Определите интерфейс для создания объекта, но позвольте подклассам решать, какой класс создавать. Заводской метод позволяет классу отложить создание экземпляра до подклассов.

Мотивация

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

Состав

Объекты, созданные методом createFactory, наследуются от одного и того же родительского объекта, они являются подклассами, реализующими специальные функции.

Давайте создадим фабрику животных, которая занимается созданием разных видов животных:

  1. У нас будет общий конструктор AnimalFactory.
  2. Статический метод класса AnimalFactory с именем createFactory (). Этот метод будет обрабатывать создание разных видов животных.
  3. Специализированные конструкторы, такие как AnimalFactory.Monkey, AnimalFactory.Snake, AnimalFactory.Dog, унаследованные от AnimalFactory. Мы определим все эти специализированные конструкторы как статические свойства родительского конструктора AnimalFac.

Посмотрим, как выглядит завершенная реализация этого паттерна:

Реализация

    var monkey = AnimalFactory.createFactory('Monkey');
    var snake = AnimalFactory.createFactory('Snake');
    var dogo = AnimalFactory.createFactory('Dog');

    monkey.getLegs(); // I have 2 legs
    snake.getLegs(); // I have 0 legs
    dogo.getLegs(); // I have 4 legs

Один из возможных способов реализации решения был бы следующим;

function AnimalFactory () {};

// We extend the animal factory functionality by adding a getLegs function that returns the amount of legs of an specific animal.
AnimalFactory.prototype.getLegs = function() {
        return  'I have ' + this.legs + ' legs';
}

AnimalFactory.createFactory = function (type) {
    var constr = type, instance;
    
    if (typeof AnimalFactory[constr] !=== 'function') {
        throw new Error('The constructor ' + constr +  ' does not exist');
    }
 // at this point the constructor is known to exist so we have it inherit from the parent
    if (typeof AnimalFactory[constr].prototype.getLegs !== 'function') {
            AnimalFactory[constr].prototype = new AnimalFactory();
    }
    
    instance = new AnimalFactory[constr]();
    return instance;
};

// Specific constructor for the different types of animal. The only singularity about this constructors is that they are assigned to a static property of the AnimalFactory class, apart from that everything looks normal. We can call any of this constructors like this: var monkey = AnimalFactory['Monkey'];
AnimalF.Monctoryakey = function() {
    this.legs = 2;
}
AnimalFactory.Snake = function() {
    this.legs = 0;
}
AnimalFactory.Dogo = function() {
    this.legs = 4;
}

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

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

Итератор

Намерение

Шаблон итератора предоставляет клиенту возможность перебирать коллекцию объектов в цикле.

Мотивация

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

Состав

В шаблоне итератора объект должен предоставить метод next (). Вызов next () должен возвращать следующий последовательный элемент. Нам решать, что означает «следующий».

Агрегированный объект обычно предоставляет другой метод, называемый hasNext (), чтобы пользователи объекта знали, достигли ли они конца данных.

Реализация такого агрегатного объекта выглядит так:

Реализация

var aggObj = (function() {
  var index = 0,
        data = [1, 2, 3, 4, 5],
        length = data.length;

        return {
            next: function() {
                var element;
                if (this.hasNext()) {
                    element = data[index];
                    index = index + 2;
                    return element;
                }
                return null;
            },
            hasNext: function() {
                return index < length;
            }
        }
} )()
console.log(aggObj.next()); // 1
console.log(aggObj.next()); // 3
console.log(aggObj.next()); // 5
console.log(aggObj.next()); // null

Декоратор

Намерение

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

Мотивация

Представим, что мы являемся владельцами магазина MacBook. Мы продаем MacBook с разными характеристиками, и цена MacBook зависит от этих характеристик. Допустим, начальная цена MacBook составляет 1000 долларов. Если мы добавим, например, SSD-диск, цена возрастет, если пользователь хочет MacBook с дисплеем Retina, цена возрастет и так далее, и тому подобное. Эти надстройки украшают наши MacBook. Предположим также, что гарантия на MacBook зависит от дополнений.

Реализация

Вот простая реализация шаблона декоратора для нашего примера:

function Macbook() {
  this.price = 1000;
  this.warranty = 0.5;
  this.decorators_list = {};
}

Macbook.decorators = {};

Macbook.decorators.memory = {
  getPrice: function(price) {
    return price + 400;
  }
};

Macbook.decorators.ssd = {
  getPrice: function(price) {
    return price + 500;
  },
  getWarranty: function(warranty) {
    return warranty + 1;
  }
}

Macbook.decorators.retinaDisplay = {
  getPrice: function(price) {
    return price + 700;
  },
   getWarranty: function(warranty) {
    return warranty + 1.5;
  }
}

Macbook.prototype.decorate = function(method, decorator) {
  this.decorators_list[method] =   this.decorators_list[method] || [];
  this.decorators_list[method].push(decorator);
}

Macbook.prototype.getPrice = function() {  
  return this.price;
}

Macbook.prototype.getWarranty = function() {
  return this.warranty;
}
// Just a helper method that takes a name method and a property, and converts it to a decorated method
function makeDecoratedMethod(method, prop) {
  Macbook.prototype[method] = function() {
  var i = 0, 
      max = this.decorators_list[method].length,
      name,
      res = this[prop];
  
  for (i; i < max; i ++) {
    name = this.decorators_list[method][i];
    res = Macbook.decorators[name][method](res);
  }
  return res;
}

}

makeDecoratedMethod('getPrice', 'price');
makeDecoratedMethod('getWarranty', 'warranty');

var mb = new Macbook();
mb.decorate('getPrice','memory');
mb.decorate('getPrice','ssd');
mb.decorate('getPrice','retinaDisplay');
mb.decorate('getWarranty', 'retinaDisplay');
console.log(mb.getPrice()); // 2600
console.log(mb.getWarranty()); // 2

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

Как видите, это гибкий способ добавления функциональности нашему объекту по запросу.

Заключение

Мы рассмотрели некоторые популярные шаблоны проектирования и способы их применения в javascript. Имейте в виду, что большинство шаблонов проектирования происходят из языков со статической типизацией, таких как C #, Java. Однако JavaScript - это динамический язык, и некоторые из обсуждаемых здесь шаблонов мало используются в javascript. Тем не менее, не помешает знать, как применять их в javascript.