Паттерны проектирования в JavaScript: продвинутый уровень с примерами на Node.js
Паттерны проектирования — это проверенные архитектурные решения для типовых задач. Если базовые паттерны (Одиночка, Фабрика, Наблюдатель) уже знакомы, то настало время углубиться в более сложные конструкции. В этой статье мы разберём четыре продвинутых паттерна GoF на примерах JavaScript (Node.js): Компоновщик, Цепочка обязанностей, Состояние и Хранитель. Эти паттерны помогут вам писать гибкий, масштабируемый и поддерживаемый код.
1. Компоновщик (Composite)
Назначение: Позволяет сгруппировать объекты в древовидную структуру и работать с каждым объектом или группой единообразно.
Пример: Система файлов, где папка может содержать файлы и другие папки. Мы хотим подсчитать общий размер.
// Компонент
class FileSystemComponent {
getSize() {
throw new Error('Метод getSize() должен быть реализован');
}
}
// Лист
class File extends FileSystemComponent {
constructor(name, size) {
super();
this.name = name;
this.size = size;
}
getSize() {
return this.size;
}
}
// Контейнер (узел)
class Folder extends FileSystemComponent {
constructor(name) {
super();
this.name = name;
this.children = [];
}
add(component) {
this.children.push(component);
}
getSize() {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
}
// Использование
const root = new Folder('root');
const src = new Folder('src');
const index = new File('index.js', 500);
const utils = new File('utils.js', 300);
src.add(index);
src.add(utils);
root.add(src);
root.add(new File('package.json', 200));
console.log(`Общий размер: ${root.getSize()} байт`); // 1000 байт
Плюсы: Упрощает работу с деревьями, скрывает сложность иерархии от клиента.
2. Цепочка обязанностей (Chain of Responsibility)
Назначение: Позволяет передавать запрос по цепочке обработчиков, пока один из них не обработает запрос.
Пример: Обработка HTTP-запросов в Express.js (middleware).
class Handler {
setNext(handler) {
this.next = handler;
return handler;
}
handle(request) {
if (this.next) {
return this.next.handle(request);
}
return null;
}
}
class AuthHandler extends Handler {
handle(request) {
if (!request.token) {
return 'Ошибка: неавторизован';
}
console.log('AuthHandler: проверка пройдена');
return super.handle(request);
}
}
class RoleHandler extends Handler {
handle(request) {
if (request.role !== 'admin') {
return 'Ошибка: недостаточно прав';
}
console.log('RoleHandler: роль подтверждена');
return super.handle(request);
}
}
class LoggingHandler extends Handler {
handle(request) {
console.log(`LoggingHandler: запрос от ${request.user}`);
return super.handle(request);
}
}
// Сборка цепочки
const auth = new AuthHandler();
const role = new RoleHandler();
const log = new LoggingHandler();
auth.setNext(role).setNext(log);
const request = { token: 'abc123', role: 'admin', user: 'Ivan' };
const result = auth.handle(request);
console.log(result); // null (все ок)
const badRequest = { token: 'abc123', role: 'user', user: 'Petr' };
console.log(auth.handle(badRequest)); // 'Ошибка: недостаточно прав'
Плюсы: Снижает связанность, легко добавлять/удалять обработчики.
3. Состояние (State)
Назначение: Позволяет объекту менять поведение в зависимости от своего внутреннего состояния.
Пример: Документ с состояниями: Черновик, На модерации, Опубликован.
class Document {
constructor() {
this.state = new DraftState(this);
}
setState(state) {
this.state = state;
}
publish() {
this.state.publish();
}
delete() {
this.state.delete();
}
}
// Абстрактное состояние
class State {
constructor(document) {
this.document = document;
}
publish() {
throw new Error('Метод publish() не реализован');
}
delete() {
throw new Error('Метод delete() не реализован');
}
}
class DraftState extends State {
publish() {
console.log('Черновик отправлен на модерацию');
this.document.setState(new ModerationState(this.document));
}
delete() {
console.log('Черновик удалён');
}
}
class ModerationState extends State {
publish() {
console.log('Документ опубликован');
this.document.setState(new PublishedState(this.document));
}
delete() {
console.log('Документ отклонён модератором');
this.document.setState(new DraftState(this.document));
}
}
class PublishedState extends State {
publish() {
console.log('Документ уже опубликован');
}
delete() {
console.log('Публикация отменена');
this.document.setState(new DraftState(this.document));
}
}
// Использование
const doc = new Document();
doc.publish(); // Черновик отправлен на модерацию
doc.publish(); // Документ опубликован
doc.publish(); // Документ уже опубликован
doc.delete(); // Публикация отменена
Плюсы: Избавляет от больших условных операторов, упрощает добавление новых состояний.
4. Хранитель (Memento)
Назначение: Позволяет сохранять и восстанавливать предыдущее состояние объекта без раскрытия его внутренней структуры.
Пример: Редактор текста с функцией отмены (undo).
// Хранитель (Memento)
class TextMemento {
constructor(content) {
this.content = content;
}
getContent() {
return this.content;
}
}
// Создатель (Originator)
class TextEditor {
constructor() {
this.content = '';
}
type(text) {
this.content += text;
}
save() {
return new TextMemento(this.content);
}
restore(memento) {
this.content = memento.getContent();
}
show() {
console.log(`Текущий текст: "${this.content}"`);
}
}
// Опекун (Caretaker)
class History {
constructor() {
this.mementos = [];
}
push(memento) {
this.mementos.push(memento);
}
pop() {
return this.mementos.pop();
}
}
// Использование
const editor = new TextEditor();
const history = new History();
editor.type('Привет, ');