Огляд шаблонів проектування у JavaScript

12 хв. читання
18 листопада 2018

Коли ви починаєте роботу над новим проектом, ви не переходите одразу до написання коду. Спочатку треба визначити мету й обсяг, функціонал і технічні характеристики проекту. Для більш комплексних проектів слід обрати правильний шаблон проектування. Й тільки потім можна переходити до написання коду.

Що таке Шаблон Проектування?

Шаблон проектування — рішення для поширених проблем при розробці програмного забезпечення. Це найкращі практики від найбільш досвічених розробників. Шаблон проектування можна також розглядати як макет для програмування.

Навіщо використовувати шаблони проектування?

Багато розробників вважають шаблони проектування марною витратою часу або просто не знають як їх правильно застосовувати. З шаблонами проектування можна писати кращий та більш зрозумілий код. Такий код згодом легше підтримувати.

Шаблони проектування дають розробникам загальну термінологію, якою можна пояснити суть вашого коду без детального ознайомлення з ним.

Наприклад, якщо ви використовуєте у своєму проекті шаблон Декоратор, інший програміст одразу зрозуміє що робить та чи інша частина коду та зможе зосередитись на вирішенні більш суттєвих проблем.

Розглянемо декілька шаблонів проектування у JavaScript.

Шаблон Модуль (Module)

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

Модулі — незамінна частина сучасного застосунку на JavaScript. Вони забезпечують чистоту коду, роблять його організованим та розділеним. Існує багато способів реалізації модулів у JavaScript, один з них — шаблон Модуль.

На відміну від інших мов програмування, JavaScript не має модифікаторів доступу, тому ми не можемо визначити public або private змінні. З шаблоном Модуль можна імітувати інкапсуляцію. Для цього у шаблоні використовуються IIFE (функції, що викликаються негайно), замикання та області видимості функцій.

Наприклад:

const myModule = (function() {
  
  const privateVariable = 'Hello World';
  
  function privateMethod() {
    console.log(privateVariable);
  }
  return {
    publicMethod: function() {
      privateMethod();
    }
  }
})();
myModule.publicMethod();

Оскільки тут використано IIFE, код виконується негайно та повертає об'єкт, який присвоюється змінній myModule. Через замикання повернений об'єкт досі може отримати доступ до функцій та змінних у IIFE (навіть після її завершення).

Таким чином, змінні та функції, визначені всередині IIFE, стають прихованими від зовнішньої області або private для змінної myModule.

Після виконання коду змінна myModule виглядатиме так:

const myModule = {
  publicMethod: function() {
    privateMethod();
  }};

Тому ми можемо викликати publicMethod(), який здійснить виклик privateMethod(). Наприклад:

// Виведе 'Hello World'
module.publicMethod();

Шаблон Виявлення Модулів (Revealing Module)

Цей шаблон — трохи покращена версія шаблону Модуль Крістіана Хейлмана. Основна проблема шаблону Модуль у тому, що треба створювати нові public функції лише для виклику private функцій та змінних.

У шаблоні Виявлення Модулів ми відображаємо властивості поверненого об'єкта на приватні функції, які хочемо виявляти як public. Саме тому шаблон Виявлення Модулів має таку назву.

Наприклад:

const myRevealingModule = (function() {
  
  let privateVar = 'Peter';
  const publicVar = 'Hello World';
  function privateFunction() {
    console.log('Name: '+ privateVar);
  }
  
  function publicSetName(name) {
    privateVar = name;
  }
  function publicGetName() {
    privateFunction();
  }
  /** виявляємо методи та змінні, присвоївши їх властивостям об'єкта */
return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
  };
})();
myRevealingModule.setName('Mark');
// Виведе Name: Mark
myRevealingModule.getName();

З цим шаблоном легше зрозуміти яка функція та змінна має публічний доступ. Тож код стає більш читабельним.

Після виконання коду, змінна myRevealingModule виглядає так:

const myRevealingModule = {
  setName: publicSetName,
  greeting: publicVar,
  getName: publicGetName
};

Ми можемо викликати метод myRevealingModule.setName('Mark'), який є посиланням на внутрішні методи publicSetName та myRevealingModule.getName().Останні посилаються на publicGetName.

Наприклад:

myRevealingModule.setName('Mark');
// Виведе Name: Mark
myRevealingModule.getName();

Переваги шаблону Виявлення модулів над шаблоном Модуль:

  • Можна змінювати доступ до елементів з public на private і навпаки лише одним рядком у return.
  • Повернений об'єкт не визначає функцій. Ці вирази визначені всередині IIFE, що робить код більш доступним для сприйняття.

ES6 Модулі

До ES6 у JavaScript не було вбудованих модулів, тому розробники були вимушені покладатися на сторонні бібліотеки чи шаблон Модуль для їх реалізації. Але з ES6 у JavaScript з'явилися власні модулі.

Модулі ES6 збережені у файлах. В одному файлі може знаходитись лише один модуль. Увесь вміст модуля приватний за замовчуванням. Функції, змінні та класи супроводжуються ключовим словом export. Код модуля завжди виконується у суворому режимі.

Експорт Модуля

Існує два способи експорту оголошення функції та змінної:

  • Додати ключове слово export перед оголошенням змінної чи функції.

Наприклад:

// utils.js
export const greeting = 'Hello World';
export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// Це приватна функція
function privateLog() {
  console.log('Private Function');
}  
  • Додати в кінець коду ключове слово export з назвами функцій на змінних, які ми хочемо експортувати.

Наприклад:

// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// Це приватна функція
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

Імпорт модуля

Тут також два варіанти реалізації, але з ключовим словом import. Наприклад:

  • Імпорт декількох елементів одночасно
// main.js
// імпортуємо декілька елементів
import { sum, multiply } from './utils.js';
console.log(sum(3, 7));
console.log(multiply(3, 7));
  • Імпорт всього модуля
// main.js
// Імпорт всього модуля
import * as utils from './utils.js';
console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

Для імпортів та експортів можна створити аліаси

Якщо ви хочете уникнути конфлікту імен, ви можете змінити назву експорту як під час експорту, так і під час імпорту. Наприклад:

  • Зміна назви для експорту
// utils.js
function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
export {sum as add, multiply};
  • Зміна назви для імпорту
// main.js
import { add, multiply as mult } from './utils.js';
console.log(add(3, 7));
console.log(mult(3, 7));

Шаблон Одинак (Singleton)

Одинак — об'єкт, екземпляр якого можна створити лише один раз. З цим шаблоном новий екземпляр класу створюється лише якщо він ще не існує. В іншому випадку повертається посилання на вже створений екземпляр. Будь-які повторні виклики конструктора завжди повертатимуть один об'єкт.

У JavaScript вбудовані синглтони називаються літералами об'єкта. Наприклад:

const user = {
  name: 'Peter',
  age: 25,
  job: 'Teacher',
  greet: function() {
    console.log('Hello!');
  }
};

Оскільки кожен об'єкт у JavaScript займає унікальне місце в пам'яті, коли ми викликаємо об'єкт user, повертається посилання на цей об'єкт.

Якщо ми спробуємо присвоїти змінну user іншій змінній і змінити її. Наприклад:

const user1 = user;
user1.name = 'Mark';

Ми побачимо, що обидва об'єкти змінилися, тому що об'єкти у JavaScript передаються за посиланням, а не за значенням. У нашому випадку у пам'яті є лише один об'єкт. Наприклад:

// виведе 'Mark'
console.log(user.name);
// виведе 'Mark'
console.log(user1.name);
// виведе true
console.log(user === user1);

Шаблон Одинак можна реалізувати через конструктор. Наприклад:

let instance = null;
function User() {
  if(instance) {
    return instance;
  }
  instance = this;
  this.name = 'Peter';
  this.age = 25;
  
  return instance;
}
const user1 = new User();
const user2 = new User();
// Виводить true
console.log(user1 === user2);

Коли викликається конструктор, він перевіряє чи існує екземпляр об'єкта. Якщо об'єкт не існує, змінна this присвоюється instance. Якщо об'єкт існує, конструктор повертає його.

Singleton можна також реалізувати через шаблон Модуль. Наприклад:

const singleton = (function() {
  let instance;
  
  function init() {
    return {
      name: 'Peter',
      age: 24,
    };
  }
  return {
    getInstance: function() {
      if(!instance) {
        instance = init();
      }
      
      return instance;
    }
  }
})();
const instanceA = singleton.getInstance();
const instanceB = singleton.getInstance();
// виведе true
console.log(instanceA === instanceB);

У коді ми створюємо новий екземпляр, викликаючи метод singleton.getInstance. Якщо екземпляр вже існує, метод його повертає. В іншому випадку метод створює новий екземпляр через виклик init().

Шаблон Фабрика (Factory)

Шаблон Фабрика — шаблон, який використовує фабричні методи для створення об'єктів без точно визначеного класу або конструктора.

Цей шаблон використовується, коли необхідно створити різні об'єкти в залежності від умови. Логіка створення об'єктів передається дочірнім класам.

Наприклад:

class Car{
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'brand new';
    this.color = options.color || 'white';
  }
}
class Truck {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || 'used';
    this.color = options.color || 'black';
  }
}
class VehicleFactory {
  createVehicle(options) {
    if(options.vehicleType === 'car') {
      return new Car(options);
    } else if(options.vehicleType === 'truck') {
      return new Truck(options);
      }
  }
}

Ми створили класи Car та Truck (з деякими значеннями за замовчуванням). Також визначаємо клас VehicleFactory, який буде створювати та повертати новий об'єкт в залежності від значення властивості vehicleType об'єкта options.

const factory = new VehicleFactory();
const car = factory.createVehicle({
  vehicleType: 'car',
  doors: 4,
  color: 'silver',
  state: 'Brand New'
});
const truck= factory.createVehicle({
  vehicleType: 'truck',
  doors: 2,
  color: 'white',
  state: 'used'
});
// Виведе Car {doors: 4, state: "Brand New", color: "silver"}
console.log(car);
// Виведе Truck {doors: 2, state: "used", color: "white"}
console.log(truck);

Створюється новий об'єкт factory класу VehicleFactory. Після цього ми можемо створити об'єкти класів Car чи Truck, викликавши factory.createVehicle та передати об'єкт options з властивістю vehicleType. Ця властивість і визначає клас.

Шаблон Декоратор (Decorator)

Шаблон Декоратор використовується для розширення функціоналу об'єкта без зміни наявного класу та конструктора. З цим шаблоном можна додавати до об'єктів нові фічі без зміни їх базового коду.

Простий приклад Декоратора:

function Car(name) {
  this.name = name;
  // Значення за замовчуванням
  this.color = 'White';
}
// Створення нового об'єкта
const tesla= new Car('Tesla Model 3');
// Декорування об'єкта новою функціональністю
tesla.setColor = function(color) {
  this.color = color;
}
tesla.setPrice = function(price) {
  this.price = price;
}
tesla.setColor('black');
tesla.setPrice(49000);
// виведе black
console.log(tesla.color);

Більш практичний приклад шаблону:

Скажімо, вартість автомобіля залежить від його функціоналу. Без шаблону Декоратор, нам необхідно створити різні класи для можливих комбінацій функціоналу. У кожному класі буде метод для розрахунку вартості. Наприклад:

class Car() {
}
class CarWithAC() {
}
class CarWithAutoTransmission {
}
class CarWithPowerLocks {
}
class CarWithACandPowerLocks {
}

Але з шаблоном Декоратор ми можемо створити базовий клас Car і обчислити вартість різних комплектацій з функціями-декораторами. Наприклад:

class Car {
  constructor() {
  // Вартість за замовчуванням
  this.cost = function() {
  return 20000;
  }
}
}
// Функція-декоратор
function carWithAC(car) {
  car.hasAC = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}
// Функція-декоратор
function carWithAutoTransmission(car) {
  car.hasAutoTransmission = true;
   const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 2000;
  }
}
// Функція-декоратор
function carWithPowerLocks(car) {
  car.hasPowerLocks = true;
  const prevCost = car.cost();
  car.cost = function() {
    return prevCost + 500;
  }
}

Спочатку ми створили базовий клас Car. Далі створюємо декоратор для фічі, яку хочемо додати та передаємо об'єкт класу Car як параметр. Потім ми перевизначаємо функцію cost. Вона повертатиме оновлену вартість автомобіля та додаватиме нову властивість об'єкту.

Щоб додати нову фічу, треба зробити щось на зразок:

const car = new Car();
console.log(car.cost());
carWithAC(car);
carWithAutoTransmission(car);
carWithPowerLocks(car);

Наприкінці ми зможемо підрахувати вартість автомобіля так:

// Підраховуємо загальну вартість авто
console.log(car.cost());
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація