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

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

Singleton (Одиночка)

Спрошу сначала: когда ты последний раз был в ресторане или кафе? Давай представим, что ты зашёл в одно из таких заведений. Там есть официанты, повара, и, конечно же, кассовый аппарат. Теперь давай поговорим о классическом программном паттерне — Singleton.

Принцип работы Singleton

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

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

Пример из жизни

Singleton — это как раз тот случай, когда у нас может быть только один экземпляр какого-то класса, и все обращения к этому классу будут использовать именно этот единственный экземпляр. Это прямо как с кассовым аппаратом в ресторане. Даже если в ресторане работает много официантов и клиентов одновременно, у них всегда есть доступ к одному и тому же кассовому аппарату для оформления заказов и оплаты.

Для лучшего понимания, представь себе, что у каждого официанта есть свой собственный кассовый аппарат. Это было бы неудобно, правда? Официанты могли бы сделать ошибки в счетах, и было бы сложно отслеживать всю выручку. Теперь, когда у них есть один общий кассовый аппарат (Singleton), все операции становятся гораздо проще и надежнее.

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

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

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

Более подробно про паттерн «Одиночка» можно почитать в этой статье.

Реализация на JavaScript

Паттерн одиночка (Singleton) позволяет создать только один экземпляр класса и предоставить глобальную точку доступа к этому экземпляру. Вот простая реализация паттерна одиночка на JavaScript на примере счетчика:

class Counter {
  constructor() {
    // Проверяем, существует ли уже экземпляр класса
    if (Counter.instance) {
      return Counter.instance; // Если существует, возвращаем существующий экземпляр
    }

    this.count = 0; // Инициализируем счетчик
    Counter.instance = this; // Сохраняем экземпляр в статической переменной

    return this; // Возвращаем новый экземпляр
  }

  increment() {
    this.count++;
  }

  decrement() {
    if (this.count > 0) {
      this.count--;
    }
  }

  getCount() {
    return this.count;
  }
}

// Использование счетчика как одиночки
const counter1 = new Counter();
counter1.increment();
console.log(counter1.getCount()); // Выводит: 1

const counter2 = new Counter(); // Получаем существующий экземпляр
console.log(counter2.getCount()); // Выводит: 1, так как это тот же экземпляр

counter2.decrement();
console.log(counter2.getCount()); // Выводит: 0, состояние счетчика сохраняется между экземплярами

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

Мы также добавили методы increment, decrement и getCount, чтобы управлять счетчиком и получать его текущее значение.

Factory Method (Фабричный метод)

Фабричный метод (Factory Method) — это порождающий паттерн проектирования, который предоставляет интерфейс для создания объектов в суперклассе, но позволяет подклассам выбирать класс создаваемого объекта. Таким образом, он делегирует ответственность за создание конкретных объектов подклассам, что способствует более гибкой и расширяемой архитектуре программы.

Принцип работы Фабричного метода

  1. Есть абстрактный суперкласс (или интерфейс), который объявляет метод (фабричный метод), отвечающий за создание объекта.
  2. Подклассы реализуют этот фабричный метод, возвращая конкретные экземпляры объектов, соответствующие их логике или требованиям.
  3. Клиентский код работает с абстрактным суперклассом и вызывает фабричный метод для создания объектов, не заботясь о конкретных классах.

Пример из жизни

Представьте, что вы создаете фабрику по производству мебели. У вас есть абстрактный суперкласс «Мебель» с фабричным методом «создать_мебель». Затем у вас есть подклассы, такие как «Стол», «Стул», «Шкаф», каждый из которых реализует фабричный метод и создает соответствующий объект (например, стол, стул или шкаф).

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

GUI библиотеки: В GUI библиотеках, таких как Swing для Java, фабричные методы используются для создания различных компонентов интерфейса, таких как кнопки, текстовые поля и окна.

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

Реализация на JavaScript

Вот простая реализация фабричного метода на JavaScript на примере создания разных типов мебели: столов и стульев.

// Абстрактная фабрика для создания мебели
class FurnitureFactory {
  createFurniture() {
    throw new Error('Метод createFurniture должен быть переопределен в подклассах');
  }
}

// Конкретная фабрика для создания столов
class TableFactory extends FurnitureFactory {
  createFurniture() {
    return new Table();
  }
}

// Конкретная фабрика для создания стульев
class ChairFactory extends FurnitureFactory {
  createFurniture() {
    return new Chair();
  }
}

// Абстрактный класс для мебели
class Furniture {
  constructor(name) {
    this.name = name;
  }
  describe() {
    console.log(`Это ${this.name}`);
  }
}

// Класс для столов
class Table extends Furniture {
  constructor() {
    super('стол');
  }
}

// Класс для стульев
class Chair extends Furniture {
  constructor() {
    super('стул');
  }
}

// Использование фабрик
const tableFactory = new TableFactory();
const chairFactory = new ChairFactory();

const table = tableFactory.createFurniture();
const chair = chairFactory.createFurniture();

table.describe(); // Выводит: Это стол
chair.describe(); // Выводит: Это стул

В этом примере мы создали абстрактную фабрику FurnitureFactory, от которой наследуются конкретные фабрики для столов и стульев. Каждая конкретная фабрика реализует метод createFurniture, который создает объекты соответствующего типа мебели (стол или стул). Каждый тип мебели представлен отдельным классом (Table и Chair), наследующимся от абстрактного класса Furniture.

Observer (Наблюдатель)

Принцип работы

Паттерн Observer используется для создания зависимости один-ко-многим между объектами, таким образом, что при изменении состояния одного объекта все зависимые от него объекты автоматически уведомляются и обновляются. Это позволяет обеспечить связь и согласованность между объектами без тесной зависимости между ними.

Пример из жизни

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

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

Паттерн Observer используется в различных областях программирования и приложений, включая:

  • Графические интерфейсы пользователя (GUI), где виджеты могут быть наблюдателями за моделью данных.
  • Реализация шаблона «издатель-подписчик» в системах сообщений и событий.
  • Очереди событий и обработчики событий.
  • Реактивное программирование и библиотеки, такие как RxJava и RxSwift.

Реализация на JavaScript

Пример реализации паттерна «Наблюдатель» (Observer) на JavaScript с комментариями.

// Наблюдатель
class Observer {
  // Метод, который будет переопределен в конкретных наблюдателях
  update(message) {
    // Здесь можно определить, как конкретный наблюдатель реагирует на обновления
  }
}

// Издатель (Субъект)
class WeatherStation {
  constructor() {
    this.observers = []; // Здесь будем хранить список наблюдателей
  }

  // Добавление наблюдателя в список подписчиков
  addObserver(observer) {
    this.observers.push(observer);
  }

  // Удаление наблюдателя из списка подписчиков
  removeObserver(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  // Уведомление всех наблюдателей об изменениях
  notifyObservers(message) {
    this.observers.forEach((observer) => {
      observer.update(message); // Вызываем метод update у каждого наблюдателя
    });
  }

  // Метод для установки новых погодных данных
  setWeather(temperature, humidity, pressure) {
    const message = `Температура: ${temperature}°C, Влажность: ${humidity}%, Давление: ${pressure} мм рт.ст.`;
    this.notifyObservers(message); // Уведомляем всех наблюдателей о новых данных
  }
}

// Конкретные наблюдатели
class PhoneDisplay extends Observer {
  update(message) {
    console.log(`Телефонный дисплей: ${message}`);
  }
}

class TVDisplay extends Observer {
  update(message) {
    console.log(`Телевизионный дисплей: ${message}`);
  }
}

// Пример использования
const weatherStation = new WeatherStation();
const phoneDisplay = new PhoneDisplay();
const tvDisplay = new TVDisplay();

weatherStation.addObserver(phoneDisplay);
weatherStation.addObserver(tvDisplay);

weatherStation.setWeather(25, 60, 1013);
weatherStation.setWeather(22, 55, 1010);

weatherStation.removeObserver(phoneDisplay);

weatherStation.setWeather(27, 70, 1015);

Этот код на JavaScript реализует паттерн «Наблюдатель», где WeatherStation служит издателем, а PhoneDisplay и TVDisplay — конкретными наблюдателями. При изменении погодных данных издатель уведомляет всех подписчиков (наблюдателей) о новых данных.

Паттерн Strategy (Стратегия)

Принцип работы

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

Пример из жизни

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

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

  • Системы сортировки: Вы можете использовать паттерн Стратегия для реализации разных алгоритмов сортировки (например, сортировка пузырьком, сортировка слиянием, быстрая сортировка) и переключать между ними в зависимости от требований приложения.
  • Парсинг данных: Если у вас есть разные форматы данных (например, JSON, XML, CSV), вы можете использовать Стратегию для выбора соответствующего парсера в зависимости от формата данных.
  • Авторизация и аутентификация: В зависимости от требований безопасности, вы можете использовать Стратегию для выбора разных методов аутентификации, таких как логин-пароль, OAuth, JWT.
  • Генерация отчетов: Если у вас есть разные форматы отчетов (PDF, Excel, HTML), вы можете использовать Стратегию для генерации отчетов в выбранном формате.

Реализация на JavaScript

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

// Создаем интерфейс (абстрактный класс) для стратегии
class PaymentStrategy {
  pay(amount) {
    throw new Error("Метод pay должен быть переопределен в подклассах.");
  }
}

// Создаем конкретные стратегии для оплаты
class CreditCardPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Оплачено ${amount} с помощью кредитной карты.`);
  }
}

class PayPalPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Оплачено ${amount} через PayPal.`);
  }
}

class BankTransferPayment extends PaymentStrategy {
  pay(amount) {
    console.log(`Оплачено ${amount} банковским переводом.`);
  }
}

// Создаем контекст, который будет использовать выбранную стратегию
class ShoppingCart {
  constructor(paymentStrategy) {
    this.paymentStrategy = paymentStrategy;
    this.items = [];
  }

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

  checkout() {
    const totalAmount = this.calculateTotal();
    this.paymentStrategy.pay(totalAmount);
  }

  calculateTotal() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }
}

// Используем стратегии для оплаты
const cart = new ShoppingCart(new CreditCardPayment());
cart.addItem({ name: "Product 1", price: 50 });
cart.addItem({ name: "Product 2", price: 30 });

cart.checkout(); // Выведет "Оплачено 80 с помощью кредитной карты."

MVC (Model-View-Controller)

Принцип работы

Паттерн MVC (Model-View-Controller) разделяет приложение на три основных компонента

  • Модель (Model): Этот компонент представляет данные и бизнес-логику приложения. Модель отвечает за хранение, обработку и обновление данных. Она не зависит от представления и контроллера.
  • Представление (View): Представление отображает данные из модели пользователю. Это компонент, который отвечает за отображение информации и интерфейс взаимодействия с пользователем. Представление не содержит бизнес-логику и не взаимодействует напрямую с моделью.
  • Контроллер (Controller): Контроллер обрабатывает пользовательский ввод и управляет моделью и представлением. Он принимает запросы от пользователя через представление, взаимодействует с моделью для получения или обновления данных и обновляет представление с учетом изменений в модели.

Пример из жизни

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

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

  • Веб-приложения: MVC широко используется в веб-разработке для разделения логики бэкенда (модель), отображения веб-страниц (представление) и обработки HTTP-запросов (контроллер).
  • Десктопные приложения: MVC может быть использован в десктопных приложениях для создания графического пользовательского интерфейса, где модель представляет данные, представление отображает их и контроллер обрабатывает действия пользователя.
  • Мобильные приложения: Аналогично веб- и десктоп-приложениям, MVC может быть применен в мобильных приложениях для обеспечения разделения между бизнес-логикой, пользовательским интерфейсом и обработкой событий.

Реализация на JavaScript

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

// Модель (Model)
class TaskModel {
  constructor() {
    this.tasks = []; // Хранение задач в массиве
  }

  // Метод для добавления задачи в модель
  addTask(task) {
    this.tasks.push(task);
  }

  // Метод для удаления задачи из модели
  removeTask(task) {
    const index = this.tasks.indexOf(task);
    if (index !== -1) {
      this.tasks.splice(index, 1);
    }
  }
}

// Представление (View)
class TaskView {
  constructor() {
    this.taskList = document.getElementById("task-list"); // Получение элемента списка задач из DOM
  }

  // Метод для отображения задач на странице
  render(tasks) {
    this.taskList.innerHTML = ""; // Очистка списка задач
    tasks.forEach((task) => {
      const listItem = document.createElement("li"); // Создание элемента списка
      listItem.textContent = task; // Установка текста задачи
      this.taskList.appendChild(listItem); // Добавление элемента в список
    });
  }
}

// Контроллер (Controller)
class TaskController {
  constructor(model, view) {
    this.model = model; // Инициализация модели
    this.view = view;   // Инициализация представления
  }

  // Метод для добавления задачи
  addTask(task) {
    this.model.addTask(task); // Добавление задачи в модель
    this.view.render(this.model.tasks); // Обновление представления
  }

  // Метод для удаления задачи
  removeTask(task) {
    this.model.removeTask(task); // Удаление задачи из модели
    this.view.render(this.model.tasks); // Обновление представления
  }
}

// Создание экземпляров модели, представления и контроллера
const model = new TaskModel();
const view = new TaskView();
const controller = new TaskController(model, view);

// Добавление задач и удаление одной из них
controller.addTask("Задача 1");
controller.addTask("Задача 2");
controller.removeTask("Задача 1");