Lab / JavaScript / Основы

Паттерны проектирования в JavaScript c примерами

Паттерны - это такие подходы если угодно шаблоны, которые придумали для того, чтобы решать часто встречающиеся задачи при написании кода. Они помогают написать код таким образом, чтобы его было проще понимать, изменять, а главное дополнять в будущем. Короче говоря, паттерны — это договорённости о стандартном стиле и подходах.

Зачем нужны паттерны проектирования?

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

Более того, паттерны дают программистам общую терминологию, которая помогает понимать, что делает код в проекте. Если вы попадете на код, написанный другим разработчиком, то знание паттернов поможет вам быстрее разобраться в том, что он делает. Работы команды над общим проектом так-же становится более эффективной за счёт соблюдения договоронностей по общему стилю.

Паттерн Модуль

Паттерн модуля — это наиболее часто используемый шаблон проектирования в JavaScript, который позволяет инкапсулировать код в единый, самодостаточный модуль с закрытыми и открытыми методами. Ключевая концепция паттерна модуля заключается в создании закрытого модуля, который скрывает детали реализации (логики) и предоставляет внешнему приложению только четко определенный API.

Реализация модуля на прмере корзины:

var ShoppingCart = (function() {
  var items = [];

  function addItem(item) {
    items.push(item);
  }

  function removeItem(item) {
    var index = items.indexOf(item);
    if (index !== -1) {
      items.splice(index, 1);
    }
  }

  function getItems() {
    return items;
  }

  function getTotal() {
    return items.reduce(function(total, item) {
      return total + item.price;
    }, 0);
   }

	// Раскрытие публичных методов
  return {
    addItem: addItem,
    removeItem: removeItem,
    getItems: getItems,
    getTotal: getTotal
  };
})();

// Использование:
ShoppingCart.addItem({ name: 'Product 1', price: 10 });
ShoppingCart.addItem({ name: 'Product 2', price: 20 });
console.log(ShoppingCart.getItems()); // возвращает [{ name: 'Product 1', price: 10 }, { name: 'Product 2', price: 20 }]
console.log(ShoppingCart.getTotal()); // возвращает 30

У этого модуля есть приватные данные (items) и публичные методы API модуля addItem(), removeItem(), getItems(), getTotal().

Методы addItem() и removeItem() добавляют и удаляют элементы из массива items соответственно. Метод getItems() возвращает товары в корзине, а метод getTotal() вычисляет общую стоимость товаров.

Это базовый пример, в рельной реализации может быть гораздо больше методов и логики, например хранение данных в LocalStorage или базе типа Redis, работа с опциями или вариантами товара итд.

Паттерн Открытый модуль:

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

Вот пример:

var myModule = (function() {
  var privateVariable = "Hello, world!";

  function privateMethod() {
    console.log(privateVariable);
  }

  function publicMethod() {
    privateMethod();
  }

  return {
    publicMethod: publicMethod
  };
})();

myModule.publicMethod();

Паттерн Конструктор

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

В шаблоне конструктора функция конструктора используется для определения свойств и методов объекта. Затем ключевое слово new используется для создания новых экземпляров объекта.

Вот пример шаблона "Конструктор":

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name);
};

var john = new Person("John", 30);
var jane = new Person("Jane", 25);

john.sayHello(); // возвращает "Hello, my name is John"
jane.sayHello(); // возвращает "Hello, my name is Jane"

Паттерн Фабрика

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

Пример реализации шаблона "Фабрика":

function createPerson(name, age, gender) {
  var person = {};

  person.name = name;
  person.age = age;
  person.gender = gender;

  person.sayHello = function() {
    console.log("Hello, my name is " + this.name);
  };

  if (gender === "male") {
    person.favoriteColor = "blue";
  } else if (gender === "female") {
    person.favoriteColor = "pink";
  } else {
    person.favoriteColor = "green";
  }

  return person;
}

var john = createPerson("John", 30, "male");
var jane = createPerson("Jane", 25, "female");

john.sayHello(); // возвращает "Hello, my name is John"
console.log(john.favoriteColor); // возвращает "blue"
jane.sayHello(); // возвращает "Hello, my name is Jane"
console.log(jane.favoriteColor); // возвращает "pink"

Может показатся, не явным когда нужно использовать каждый из паттернов, так как оба применяются для создания объектов. В чём же разница? Шаблон фабрики обеспечивает большую гибкость при создании объектов и часто используется, когда точный тип создаваемого объекта заранее не известен. Шаблон "Конструктор" более жесткий и часто используется, когда тип объекта четко определен и последователен.

Паттерн Синглтон

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

В паттерне "Синглтон" объект-синглтон создается с помощью функции конструктора, а ключевое слово new используется для создания нового экземпляра объекта. Однако вместо того, чтобы создавать несколько экземпляров объекта, паттерн Singleton обеспечивает создание только одного экземпляра объекта и предоставляет способ глобального доступа к этому экземпляру.

Такой паттерн идеально подходит для хранения глобальных настроек и параметров вашего приложения. Ещё одно частое использование Синглтона это подключение к базе данных.

Пример Синглтона для работы с конфигами

var Config = (function() {
  var instance;

  function init() {
    // private properties and methods
    var config = {
      apiUrl: "http://api.example.com",
      maxRetries: 3
    };

    // public properties and methods
    return {
      getApiUrl: function() {
        return config.apiUrl;
      },
      getMaxRetries: function() {
        return config.maxRetries;
      }
    };
  }

  return {
    getInstance: function() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

// Usage:
var config1 = Config.getInstance();
var config2 = Config.getInstance();

console.log(config1 === config2); // возвращает true
console.log(config1.getApiUrl()); // возвращает "http://api.example.com"
console.log(config1.getMaxRetries()); // возвращает 3

Функция init() определяет частные и общедоступные свойства и методы объекта-одиночки.

Методы getApiUrl() и getMaxRetries() используются для получения значений конфига. Используя шаблон Singleton, приложение может обеспечить согласованность параметров конфига во всём коде.

Паттерн декоратор

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

// оригинальный объект
function Coffee() {
  this.cost = function() {
    return 2.0;
  };
}

// декоратор
function Milk(coffee) {
  this.cost = function() {
    return coffee.cost() + 0.5;
  };
}

// декоратор
function Sugar(coffee) {
  this.cost = function() {
    return coffee.cost() + 0.2;
  };
}

// использование
var coffee = new Coffee();
coffee = new Milk(coffee);
coffee = new Sugar(coffee);

console.log(coffee.cost()); // возвращает 2.7

Декораторы Milk и Sugar оборачиваются вокруг ориганального объекта Coffee и добавляют свою собственную функциональность, вызывая исходный метод cost и добавляют стоимость к ориганальному объекту. В результате получается новый объект, который обладает объединенной функциональностью исходного объекта Coffee и декораторов Milk и Sugar.

Выводы

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