Изучите такие паттерны, как Facade, Adapter, Singleton, Prototype, Builder, Proxy и Factory для проектирования современного программного обеспечения.

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

  • Поддержание соединений с базами данных
  • Создание и управление объектами
  • Уведомление множества пользователей, подписанных на определенную сущность.

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

Но это совсем не обязательно!

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

Итак, давайте рассмотрим наиболее часто встречающиеся паттерны проектирования, которые могут понадобиться вам при работе с Node.js.

1. Фасад (Facade)

image.png

Прежде всего, важно понять паттерн Facade. Он очень важен для приложений на Node.js.

Проще говоря, паттерн Facade упрощает сложные подсистемы, предоставляя единый интерфейс.

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

Например, процесс входа на сайт с помощью аккаунта Google можно рассматривать как реальный пример фасада. Вам нужно только нажать на кнопку "Войти с помощью Google", и она действует как единый вариант входа.

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

Преимущества:

  • Упрощенный интерфейс: Это снижает когнитивную нагрузку на разработчиков, упрощая взаимодействие со сложными системами.
  • Уменьшение связанности: Отделение клиентского кода от деталей внутренней реализации позволяет улучшить сопровождаемость и гибкость кода.
  • Улучшенная читаемость: Инкапсулирует сложную логику внутри фасада, делая код более организованным и понятным.
  • Контролируемый доступ: Включение определенных правил или проверок перед доступом к базовым функциям.

Рассмотрим этот фрагмент:

// Комплексный модуль
class ComplexModule {
  initialize() {
    // Логика инициализации комплекса
  }

  operation1() {
    // Сложная операция 1
  }

  operation2() {
    // Сложная операция 2
  }
}

// Код клиента
const complexModule = new ComplexModule();
complexModule.initialize();
complexModule.operation1();
complexModule.operation2();

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

Однако посмотрите на этот фрагмент:

// Фасад для сложного модуля
class ModuleFacade {
  constructor() {
    this.complexModule = new ComplexModule();
  }

  performOperations() {
    this.complexModule.initialize();
    this.complexModule.operation1();
    this.complexModule.operation2();
  }
}

// Код клиента
const moduleFacade = new ModuleFacade();
moduleFacade.performOperations();

Теперь, как вы можете видеть, инициализация подмодулей больше не находится вне модуля, а инкапсулирована в функцию performOperations, которая обрабатывает все внутренние коммуникации сложной подсистемы.

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

Полную реализацию можно посмотреть здесь.

2. Синглтон

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

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

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

Преимущества:

  • Глобальный доступ: Удобный способ доступа к общим данным или функциональности из любой точки вашего приложения.
  • Управление ресурсами: Обеспечивает эффективное использование ресурсов, таких как соединения с базой данных, регистраторы или дескрипторы файлов, благодаря наличию только одного экземпляра.
  • Согласованность: Обеспечивает согласованное поведение, поскольку изменения затрагивают только один экземпляр.
  • Контролируемое состояние: Упрощает управление состоянием за счет наличия центральной точки для манипулирования данными.

Вот как будет выглядеть реализация синглтона в Node.js:

class ConfigManager {
  constructor() {
    this.databaseConfig = { /* конфигурация базы данных */ };
    this.apiKey = "your_api_key";
    // Другие конфигурации для всего приложения
  }

  static getInstance() {
    if (!this.instance) {
      this.instance = new ConfigManager();
    }
    return this.instance;
  }

  getDatabaseConfig() {
    return this.databaseConfig;
  }

  getApiKey() {
    { return this.apiKey;
  }

  // Дополнительные методы для получения других конфигураций
}

// Использование
const configManager = ConfigManager.getInstance();

// Доступ к конфигурациям
const databaseConfig = configManager.getDatabaseConfig();
const apiKey = configManager.getApiKey();

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

Полную реализацию этого паттерна можно посмотреть здесь.

3. Адаптер

image.png

Далее вам нужно представить сценарий, в котором API, который вы используете, и клиент, над которым вы работаете, имеют несовместимые API.

Например, у вас может быть компонент React, который принимает два реквизита:

  • Имя
  • Фамилия

Но ваш API возвращает переменную:

  • Полное Имя

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

Именно здесь на помощь приходит паттерн адаптера.

Паттерн адаптера устраняет разрыв между несовместимыми интерфейсами, позволяя им работать вместе без проблем.

Преимущества:

  • Интероперабельность: Обеспечивает связь между компонентами с различными интерфейсами, способствуя интеграции и повторному использованию системы.
  • Свободное соединение: Отделяет клиентский код от конкретной реализации адаптируемого компонента, повышая гибкость и удобство сопровождения.
  • Гибкость: Позволяет адаптировать новые компоненты без изменения существующего кода путем создания новых адаптеров.
  • Возможность повторного использования: Реализацию адаптера можно повторно использовать для аналогичных задач совместимости, сокращая дублирование кода.

Пример:

Ниже приведен простой пример кодирования паттерна проектирования Adapter. Полную реализацию можно посмотреть здесь.

Старая система:

class OldSystem {
  request() {
    return "Запрос старой системы";
  }
}

Новая система и адаптер:

class NewSystem {
  newRequest() {
    return "Запрос новой системы";
  }
}

class Adapter {
  constructor(newSystem) {
    this.newSystem = newSystem;
  }

  request() {
    return this.newSystem.newRequest();
  }
}

Использование клиента

// Использование OldSystem
const oldSystem = new OldSystem();
console.log(oldSystem.request()); // Выходные данные: Запрос старой системы

// Использование адаптера с NewSystem
const newSystem = new NewSystem();
const adapter = new Adapter(newSystem);
console.log(adapter.request()); // Выводы: Запрос новой системы

4. Строитель (Builder)

image.png

Далее мы рассмотрим паттерн, который можно использовать для построения объектов и упрощения управления ими.

Паттерн Builder отделяет создание сложного объекта от его представления.

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

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

Преимущества:

  • Улучшенная читаемость: Более четкий код за счет явного задания каждого свойства с осмысленными именами методов.
  • Гибкость: Конструирование объектов только с необходимыми свойствами, избегая неожиданных значений в неиспользуемых полях.
  • Неизменность: Метод build() часто создает новый экземпляр вместо того, чтобы изменять конструктор, что способствует неизменяемости и упрощает рассуждения.
  • Обработка ошибок: Проще проверять значения свойств и выбрасывать ошибки в методах конструктора, чем в сложном конструкторе.

Пример:

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

class UserBuilder {
  constructor(name) {
    this.name = name;
    this.email = null;
    this.address = null;
  }

  withEmail(email) {
    this.email = email;
    return this; // Цепочка методов
  }

  withAddress(address) {
    this.address = address;
    return this;
  }

  build() {
    // Проверяем и создаем объект User
    const user = new User({
      name: this.name,
      email: this.email,
      address: this.address,
    });
    return user;
  }
}

// Код клиента
const user1 = new UserBuilder('John')
  .withEmail('john@example.com')
  .withAddress('123 Main St.')
  .build();

console.log(user1); // Выводит полный объект User со значениями

5. Фабрика (Factory)

image.png

Паттерн Factory предоставляет интерфейс для создания объектов, но позволяет подклассам изменять тип создаваемых объектов.

Представьте это как производственный завод с различными сборочными линиями для производства разных продуктов. В Node.js паттерн "Фабрика" позволяет создавать объекты без указания их конкретных классов, что способствует гибкости и расширяемости.

Преимущества:

  • Развязка: Клиентский код отделен от конкретной логики создания объектов, что способствует гибкости и удобству сопровождения.
  • Централизованное управление: Вы можете легко добавлять новые типы объектов или изменять существующие, не затрагивая клиентский код, пока Фабрика обрабатывает изменения.
  • Гибкость: Фабрика может выбирать подходящий объект в зависимости от условий выполнения или конфигурации, что делает ваш код более адаптируемым.
  • Инкапсуляция: Детали создания объектов скрыты в Фабрике, что улучшает читаемость и сопровождаемость кода.

Пример:

Ниже приведен простой пример кодирования паттерна Factory. Другие примеры кодирования можно посмотреть здесь.

Интерфейс формы

// Интерфейс Shape
class Shape {
  draw() {}
}

Конкретные фигуры

// Конкретные реализации интерфейса Shape
class Circle extends Shape {
  draw() {
    console.log("Рисуем круг");
  }
}

class Square extends Shape {
  draw() {
    console.log("Рисуем квадрат");
  }
}

class Triangle extends Shape {
  draw() {
    console.log("Рисуем треугольник");
  }
}

Shape factory

// Класс ShapeFactory отвечает за создание экземпляров фигур
class ShapeFactory {
  createShape(type) {
    switch (type) {
      case 'circle':
        return new Circle();
      case 'square':
        return new Square();
      case 'triangle':
        return new Triangle();
      default:
        throw new Error('Invalid shape type');
    }
  }
}

Код клиента

// Код клиента, использующий ShapeFactory для создания фигур
const shapeFactory = new ShapeFactory();

const circle = shapeFactory.createShape('circle');
circle.draw(); // Выходные данные: Рисование круга

const square = shapeFactory.createShape('square');
square.draw(); // Выводы: Рисуем квадрат

const triangle = shapeFactory.createShape('triangle');
triangle.draw(); // Выводы: Рисуем треугольник

6. Прототип

image.png

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

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

Концепция:

  • Прототип: Определите базовый объект с желаемыми свойствами и методами. Он служит образцом для последующих объектов.
  • Клонирование: Вы создаете новые объекты, копируя прототип, часто используя встроенные методы типа Object.create или собственную логику клонирования.
  • Персонализация: Вновь созданные объекты могут изменять свои индивидуальные свойства, не затрагивая исходный прототип.

Преимущества:

  • Производительность: Клонирование существующих объектов обычно быстрее, чем создание новых с нуля, особенно для сложных объектов.
  • Эффективность использования памяти: Совместное использование свойств и методов через прототип позволяет избежать избыточного хранения, что сокращает использование памяти.
  • Динамические изменения: Вы можете легко расширить прототип, чтобы добавить новые функциональные возможности всем существующим и будущим экземплярам.

Пример:

Ниже приведен простой пример кодирования паттерна проектирования Prototype. Другие примеры кодирования можно посмотреть здесь.

Объект прототипа

// Объект прототипа
const animalPrototype = {
  type: 'неизвестный',
  makeSound: function () {
    console.log('Какой-то общий звук');
  },
  clone: function () {
    return Object.create(this); // Использование Object.create() для клонирования
  },
};

Настройка экземпляра

// Пользовательские экземпляры на основе прототипа
const dog = animalPrototype.clone();
dog.type = 'Dog';
dog.makeSound = function () {
  console.log('Гав!');
};

const cat = animalPrototype.clone();
cat.type = 'Cat';
cat.makeSound = function () {
  console.log('Мяу!');
};

Код клиента

// Код клиента, использующий настроенные экземпляры
dog.makeSound(); // Выходные данные: Гав!
cat.makeSound(); // Выходные данные: Мяу!

7. Прокси

image.png

Шаблон Proxy служит суррогатом или заменителем другого объекта, контролируя доступ к нему.

Таким образом, создается объект-посредник ("прокси"), который стоит между клиентом и реальным объектом. Этот прокси контролирует доступ к реальному объекту, потенциально перехватывая и изменяя операции до или после того, как они достигли цели.

Это позволяет добавлять дополнительные функции, не изменяя непосредственно реализацию реального объекта. Паттерн Proxy полезен для ленивой загрузки, контроля доступа или добавления функций протоколирования/отладки.

Преимущества:

  • Контролируемый доступ: Обеспечение прав доступа или проверка перед взаимодействием с реальным объектом.
  • Дополнительная функциональность: Добавление таких функций, как ведение журнала, кэширование или безопасность, без изменения самого объекта.
  • Абстракция: Упрощение клиентского кода за счет сокрытия деталей реализации реального объекта.
  • Гибкость: Динамическое изменение целевого объекта или поведения обработчика во время выполнения.

Пример:

Ниже приведен простой пример кодирования паттерна Proxy, а больше примеров кодирования можно найти здесь. Во всех этих примерах я использовал объект JavaScript Proxy для создания прокси для других объектов.

const target = {
  name: 'Алиса',
  sayHello() {
    console.log(Здравствуйте, меня зовут ${this.name} )
  },
};

const handler = {
  get(target, prop, receiver) {
    console.log(Свойство ${prop} получено );
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(Свойство ${prop} установлено в ${value} );
    return Reflect.set(target, prop, value, receiver);
  },
};

const proxy = new Proxy(target, handler);

proxy.name; // Вывод: Получено имя свойства
proxy.sayHello(); // Вывод: Доступ к свойству sayHello
                  // Здравствуйте, меня зовут Алиса
proxy.name = 'Bob'; // Вывод: Имя свойства установлено в Bob

Заключительные мысли

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

Спасибо за чтение.


Автор: Danusha Navod