Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке патерн Command?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн Command** перетворює дію на об'єкт з методами `execute()` та `undo()`. ```javascript const cmd = new LightOnCommand(light); cmd.execute(); // Світло увімкнено ``` Відправник викликає `execute()`, не знаючи нічого про отримувача. **Ключове:** обгортає запит, щоб його можна було поставити в чергу, відкласти або скасувати.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн Command** перетворює дію на об'єкт, який можна зберегти, передати, поставити в чергу або скасувати. ## Теорія ### TL;DR - Аналогія: офіціант передає кухні (receiver) slip із замовленням (command object). Кухня виконує, не знаючи хто клієнт. - Головна ідея: обгорнути запит в об'єкт, щоб відправник і виконавець нічого не знали одне про одного. - `execute()` та `undo()` живуть на об'єкті-команді, що робить будь-яку дію зворотною. - Використовуй коли потрібне undo/redo, черги задач або складені макрокоманди. - Для простих одноразових викликів без черги і undo, просто виклич метод напряму. ### Короткий приклад ```javascript class Light { on() { console.log('Світло увімкнено'); } off() { console.log('Світло вимкнено'); } } class LightOnCommand { constructor(light) { this.light = light; } execute() { this.light.on(); } // Світло увімкнено } class RemoteControl { setCommand(cmd) { this.slot = cmd; } pressButton() { this.slot.execute(); } } const remote = new RemoteControl(); remote.setCommand(new LightOnCommand(new Light())); remote.pressButton(); // Світло увімкнено ``` `RemoteControl` нічого не знає про `Light`. Він лише викликає `execute()`. Замінюєш `LightOnCommand` на `DimmerOnCommand` і код пульта залишається без змін. ### Головна відмінність від прямого виклику Коли викликаєш `light.on()` прямо з пульта, пульт і лампочка пов'язані. Додаєш димер і переписуєш пульт. З Command пульт тримає об'єкт-команду і викликає `execute()`. Відправник не змінюється, коли змінюється отримувач. ### Коли використовувати - Потрібне undo/redo: команда зберігає в конструкторі достатньо стану для скасування. - Черги задач: дії стають об'єктами, які можна серіалізувати і відкласти. - Макрокоманди: кілька команд об'єднуються в одну складену дію. - Логування і транзакції: обгортаєш execute хуками до і після. - Простий одноразовий виклик без черги і undo: не потрібен Command, накладні витрати на об'єкти без жодного результату. ### Як це працює в JavaScript Команди - це звичайні об'єкти в V8. Виклик `execute()` диспетчеризується через прототипне наслідування, без спеціальної магії рушія. Стан зберігається через closure і посилання, передані в конструктор. Node.js event loop природно ставить команди в чергу через `setImmediate`; браузерні UI-команди часто використовують `requestAnimationFrame`. ### Типові помилки **1. Забув зберегти стан у конструкторі** ```javascript // Неправильно: this.text = undefined в undo class AddTextCommand { execute() { editor.add('hello'); } undo() { editor.remove(this.text); } // TypeError: undefined } // Правильно class AddTextCommand { constructor(editor, text) { this.editor = editor; this.text = text; // зберігаємо для undo } execute() { this.editor.add(this.text); } undo() { this.editor.remove(this.text); } } ``` **2. Мутація спільного стану без знімку** ```javascript // Неправильно: undo видаляє елемент, але re-execute знову росте масив constructor(editor) { this.editor = editor; } // немає backup // Правильно: знімок у конструкторі constructor(editor, text) { this.backup = [...editor.text]; // зберегли до execute } undo() { this.editor.text = this.backup; } // повне відновлення ``` Це найчастіший баг у черзі undo, який я бачив у продакшені. Масив мутує між re-execute і логіка скасування тихо збивається. **3. Немає null-перевірки на history.pop()** ```javascript // Падає на порожній черзі undo() { const cmd = history.pop(); cmd.undo(); } // Безпечно undo() { const cmd = history.pop(); cmd?.undo(); } ``` **4. Зловживання Command для простих дій** Одна функція без черги і undo? Просто виклич її. Кожен об'єкт-команда коштує алокацію і цикл GC. У щільних циклах це відчутно. ### Де зустрічається в реальних проектах - Redux: кожен dispatched action - об'єкт-команда, reducer - receiver. - BullMQ (Node.js): задачі в черзі реалізовані як команди з вбудованою логікою retry. - Figma і Photoshop: вся історія undo - це стек команд. - Бібліотека use-undo (React): обгортає зміни стану як зворотні команди. - jQuery UI: використовує команди всередині для undo у drag-and-drop. ### Follow-up питання **Q:** Як реалізувати undo для canvas? **A:** Зберігай знімок стану в конструкторі команди. В `undo()` відновлюй зі знімку, а не намагайся вгадати зворотну операцію. **Q:** Як Command дозволяє макрокоманди? **A:** `MacroCommand` тримає масив команд. `execute()` проходить вперед, `undo()` у зворотному порядку. Якщо одна команда падає, всі вже виконані відкочуються назад перед повторним кидком помилки. **Q:** Яка різниця між Command і Strategy? **A:** Strategy підміняє алгоритм під час виконання, але виконує його одразу. Command обгортає запит для відкладення, черги або скасування. Strategy про *як* зробити, Command про *коли* і *чи взагалі*. **Q:** Як обробляти async-команди в Node.js? **A:** Повертай Promise з `execute()` і чекай його в invoker. Сам патерн не змінюється, але треба обробляти часткове відкочування якщо команда в макроланцюзі відхиляє Promise. **Q:** Спроектуй кошик з undo і знижками. **A:** Кожна дія (додати товар, застосувати знижку) стає командою. Знімаєш знімок суми до execute. Rollback відновлює попередній знімок. Для паралельних оновлень додаєш лічильник версій і відкидаєш застарілі команди. ## Приклади ### Базовий: пульт дистанційного керування ```javascript class Light { on() { console.log('Світло увімкнено'); } off() { console.log('Світло вимкнено'); } } class LightOnCommand { constructor(light) { this.light = light; } execute() { this.light.on(); } undo() { this.light.off(); } } class LightOffCommand { constructor(light) { this.light = light; } execute() { this.light.off(); } undo() { this.light.on(); } } class RemoteControl { constructor() { this.history = []; } press(cmd) { cmd.execute(); this.history.push(cmd); } undo() { this.history.pop()?.undo(); } } const remote = new RemoteControl(); const light = new Light(); remote.press(new LightOnCommand(light)); // Світло увімкнено remote.press(new LightOffCommand(light)); // Світло вимкнено remote.undo(); // Світло увімкнено (скасовано) ``` Пульт тримає об'єкти-команди, не пряме посилання на лампочку. Код пульта не змінюється при будь-якій заміні команди. ### Середній рівень: canvas з історією змін ```javascript class Canvas { constructor() { this.elements = []; } add(shape) { this.elements.push(shape); console.log(`Додано ${shape}`); } remove(shape) { this.elements = this.elements.filter(s => s !== shape); console.log(`Видалено ${shape}`); } } class AddShapeCommand { constructor(canvas, shape) { this.canvas = canvas; this.shape = shape; } execute() { this.canvas.add(this.shape); } undo() { this.canvas.remove(this.shape); } } class History { constructor() { this.stack = []; } execute(cmd) { cmd.execute(); this.stack.push(cmd); } undo() { this.stack.pop()?.undo(); } } const canvas = new Canvas(); const history = new History(); history.execute(new AddShapeCommand(canvas, 'коло')); // Додано коло history.execute(new AddShapeCommand(canvas, 'квадрат')); // Додано квадрат history.undo(); // Видалено квадрат console.log(canvas.elements); // ['коло'] ``` Саме так Figma і Photoshop зберігають історію малювання. Кожна дія користувача - одна команда в стеку. ### Просунутий рівень: транзакційна макрокоманда ```javascript class MacroCommand { constructor(commands) { this.commands = commands; } execute() { const done = []; for (const cmd of this.commands) { try { cmd.execute(); done.push(cmd); } catch (e) { done.slice().reverse().forEach(c => c.undo()); // відкочування throw e; } } } undo() { this.commands.slice().reverse().forEach(cmd => cmd.undo()); } } class ApiCallCommand { constructor(id) { this.id = id; } execute() { if (Math.random() < 0.3) throw new Error('Таймаут мережі'); console.log(`Оброблено ${this.id}`); } undo() { console.log(`Відкочено ${this.id}`); } } const macro = new MacroCommand([ new ApiCallCommand(1), new ApiCallCommand(2), new ApiCallCommand(3), ]); try { macro.execute(); } catch (e) { console.log('Макрос упав, всі зміни відкочено'); } ``` Зворотний порядок undo тут принциповий. Якщо крок 3 падає після успішних кроків 1 і 2, відкочуємо спочатку 2, потім 1. Саме так транзакції в базах даних обробляють часткову відмову.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.