Паттерны программирования — это особые способы или решения, которые программисты используют для решения обычных задач при написании программ. Эти способы помогают сделать код более понятным, удобным для работы и повторного использования. Паттерны — это своего рода шаблоны, которые предоставляют соглашения о том, как лучше всего структурировать и организовать код для достижения нужных целей.
В этой статье мы проведем обзор основных паттернов программирования для новичков.
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) — это порождающий паттерн проектирования, который предоставляет интерфейс для создания объектов в суперклассе, но позволяет подклассам выбирать класс создаваемого объекта. Таким образом, он делегирует ответственность за создание конкретных объектов подклассам, что способствует более гибкой и расширяемой архитектуре программы.
Принцип работы Фабричного метода
- Есть абстрактный суперкласс (или интерфейс), который объявляет метод (фабричный метод), отвечающий за создание объекта.
- Подклассы реализуют этот фабричный метод, возвращая конкретные экземпляры объектов, соответствующие их логике или требованиям.
- Клиентский код работает с абстрактным суперклассом и вызывает фабричный метод для создания объектов, не заботясь о конкретных классах.
Пример из жизни
Представьте, что вы создаете фабрику по производству мебели. У вас есть абстрактный суперкласс «Мебель» с фабричным методом «создать_мебель». Затем у вас есть подклассы, такие как «Стол», «Стул», «Шкаф», каждый из которых реализует фабричный метод и создает соответствующий объект (например, стол, стул или шкаф).
Использование на практике
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");