Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Патерн декоратор». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Патерн декоратор (Decorator)** обгортає об'єкт, щоб додати нову поведінку під час виконання, не змінюючи оригінальний клас. ```typescript class MilkDecorator implements Coffee { constructor(private coffee: Coffee) {} cost() { return this.coffee.cost() + 2; } // додає до оригіналу description() { return this.coffee.description() + ", Milk"; } } const latte = new MilkDecorator(new SimpleCoffee()); // cost: 7 ``` **Ключове:** декоратор реалізує той самий інтерфейс, що й обгорнутий об'єкт, тому викликаючий код не помічає різниці.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Патерн декоратор (Decorator)** обгортає об'єкт, щоб додати нову поведінку під час виконання програми, не змінюючи оригінальний об'єкт і не створюючи підкласів. ## Теорія ### Коротко - Аналогія: піца з топінгами. Кожен топінг обгортає попередній шар; піца залишається тією ж. Топінги можна комбінувати в будь-якому порядку. - Головна різниця: наслідування додає поведінку до класу на етапі компіляції; декоратор додає поведінку до конкретного екземпляра під час виконання. - Правило вибору: використовуй декоратор, коли потрібно динамічно змішувати функції, не можна змінювати оригінальний клас, або хочеш уникнути вибуху підкласів. ### Швидкий приклад ```typescript // Оригінальний об'єкт - не змінюється class Coffee { cost() { return 5; } description() { return "Coffee"; } } // Декоратор обгортає і розширює class MilkDecorator { constructor(private coffee: Coffee) {} cost() { return this.coffee.cost() + 2; } description() { return this.coffee.description() + ", Milk"; } } const coffee = new Coffee(); const withMilk = new MilkDecorator(coffee); console.log(withMilk.cost()); // 7 console.log(withMilk.description()); // "Coffee, Milk" ``` `MilkDecorator` зберігає посилання на оригінальний `Coffee`, викликає його методи і додає свою логіку поверх. Оригінал не зачеплений. ### Головна різниця з наслідуванням Наслідування створює фіксовану ієрархію на етапі компіляції. `CoffeeWithMilk extends Coffee` додає поведінку молока до кожного екземпляра цього підкласу назавжди. Декоратор обгортає конкретні екземпляри під час виконання. Можна застосувати `MilkDecorator` до одного об'єкта кави і `SugarDecorator` до іншого, не створюючи нових класів. Це також вирішує проблему вибуху підкласів. Без декораторів потрібні окремі класи `CoffeeWithMilkAndSugar`, `CoffeeWithMilkAndCaramel`, `CoffeeWithSugarAndCaramel`. З трьома класами-декораторами можна покрити будь-яку комбінацію. ### Коли застосовувати - **Сторонній код:** не можеш змінити оригінальний клас - обгорни його. - **Динамічні комбінації:** `MilkDecorator(SugarDecorator(coffee))` або просто `SugarDecorator(coffee)` без зміни класів. - **Наскрізні задачі:** додай логування, кешування або валідацію, не торкаючись основної логіки. - **Фічер-флаги:** обгорни об'єкт умовно залежно від конфігурації або середовища. ### Як це працює всередині Декоратор зберігає посилання на обгорнутий об'єкт і реалізує той самий інтерфейс. Коли метод викликається на декораторі, він викликає метод обгорнутого об'єкта, додає власну логіку до або після і повертає результат. Так утворюється ланцюг. Кожен шар перехоплює виклики і передає їх далі. В JavaScript і TypeScript це відбувається через об'єктну композицію під час виконання. Жодних змін в ієрархії класів. Просто передаєш об'єкти всередину інших об'єктів. ### Типові помилки **1. Не реалізований повний інтерфейс** Декоратор повинен мати всі методи обгорнутого об'єкта. Пропустиш один - і код впаде в рантаймі. ```typescript // Неправильно - відсутні методи зламають виклики class LoggingDecorator { constructor(private obj: UserRepository) {} save() { console.log("saving"); return this.obj.save(); } // Відсутні: delete(), update(), find() } // Правильно - повний інтерфейс збережено class LoggingDecorator implements UserRepository { constructor(private obj: UserRepository) {} save() { console.log("saving"); return this.obj.save(); } delete() { console.log("deleting"); return this.obj.delete(); } update() { console.log("updating"); return this.obj.update(); } find() { console.log("finding"); return this.obj.find(); } } ``` **2. Мутація обгорнутого об'єкта** Додавати властивості напряму до `this.obj` не можна. Оригінал забруднюється і відкотити декорацію вже не вийде. ```typescript // Неправильно - забруднює оригінал class CacheDecorator { constructor(private obj: DataService) { (this.obj as any).cache = new Map(); // так не роби } } // Правильно - декоратор зберігає власний стан class CacheDecorator implements DataService { private cache = new Map(); constructor(private obj: DataService) {} getData(key: string) { if (this.cache.has(key)) return this.cache.get(key); const data = this.obj.getData(key); this.cache.set(key, data); return data; } } ``` **3. Порядок декораторів ігнорується** Порядок важливий. Якщо `UppercaseDecorator` обгортає `MetadataDecorator`, то `toUpperCase()` отримає об'єкт замість рядка і впаде. В Express `withCache(withAuth(handler))` означає, що перевірка авторизації відбувається всередині перевірки кешу. Переверни порядок - і запити без авторизації потраплять у кеш. ```typescript // Працює: спочатку uppercase, потім metadata обгортає результат const v1 = new MetadataDecorator(new UppercaseDecorator(processor)); // { value: "HELLO", timestamp: 1713110126000 } // Падає: UppercaseDecorator отримує об'єкт замість рядка const v2 = new UppercaseDecorator(new MetadataDecorator(processor)); // TypeError: result.toUpperCase is not a function ``` **4. Декоратор замість наслідування** Якщо поведінка потрібна всім екземплярам класу, наслідування простіше. Декоратори - для опціональної поведінки конкретних екземплярів. ```typescript // Правильно: всі собаки гавкають - використовуй наслідування class Animal { move() {} } class Dog extends Animal { bark() {} } // Правильно: тільки деякі собаки навчені - використовуй декоратор const trainedDog = new TrainedDecorator(new Dog()); ``` **5. Глибокі ланцюги на часто викликаних шляхах** Кожен шар декоратора додає виклик функції. П'ять декораторів - нормально. П'ятдесят на кожному запиті - варто профілювати. Не вважай декорацію безкоштовною операцією. ### Де зустрічається в реальних проектах - **React:** Higher-order components (HOC) - `withRouter`, `connect` з Redux обгортають компоненти, щоб додати props. - **Express/Node.js:** Middleware. `withAuth(withLogging(handler))` - це ланцюг декораторів на обробники запитів. - **Python:** Синтаксис `@decorator`. `@lru_cache`, `@property`, `@staticmethod` - все це декоратори. - **Java Streams:** `stream().filter().map().collect()` - ланцюг декораторів на колекціях. - **TypeScript/JavaScript:** Функціональна композиція через Lodash `_.compose()` або Ramda. Особисто мені патерн став зрозумілий після роботи з Express. Кожен `app.use()` - це декоратор, тільки замаскований під middleware. ### Питання на співбесіді **Q:** Як декоратор відрізняється від патерну Strategy? **A:** Strategy замінює алгоритм всередині об'єкта (один алгоритм за раз). Декоратор додає поведінку поверх того, що об'єкт вже робить. Strategy змінює *що* робить об'єкт; декоратор додає *додаткову* логіку навколо цього. **Q:** Що відбувається, коли застосовуєш кілька декораторів до одного об'єкта? **A:** Кожен декоратор обгортає попередній, утворюючи ланцюг. Виклики проходять через кожен шар у зворотному порядку (останній застосований виконується першим). Саме тому порядок важливий, особливо коли декоратори мають побічні ефекти. **Q:** Чи можна використовувати декоратори зі збереженням стану? **A:** Так. Декоратор обгортає об'єкт, а не його стан. Зміни стану обгорнутого об'єкта видно декоратору. Власний стан декоратора (наприклад, кеш) зберігається окремо. **Q:** Як зберегти типобезпеку в TypeScript при ланцюжку декораторів? **A:** Оголоси інтерфейс і реалізуй його в кожному декораторі. Або використовуй дженерики: `class Logger<T extends UserRepository>` - TypeScript знатиме, які методи доступні. Без цього після першого обгортання втрачається автодоповнення. **Q:** (Senior) Як зробити декоратор, який підтримує і синхронні, і асинхронні методи? **A:** Перевір, чи є повернене значення `Promise`. Якщо так - перехоплюй через `.then()`. Якщо ні - модифікуй синхронно. ```typescript class TransformDecorator implements DataProcessor { constructor(private obj: DataProcessor) {} process(...args: any[]) { const result = this.obj.process(...args); if (result instanceof Promise) { return result.then(value => this.transform(value)); } return this.transform(result); } private transform(value: string) { return value.toUpperCase(); } } ``` ## Приклади ### Базовий: кава з добавками Класичний приклад. Кожен декоратор реалізує той самий інтерфейс, тому їх можна комбінувати в будь-якому порядку. ```typescript interface Coffee { cost(): number; description(): string; } class SimpleCoffee implements Coffee { cost() { return 5; } description() { return "Coffee"; } } class MilkDecorator implements Coffee { constructor(private coffee: Coffee) {} cost() { return this.coffee.cost() + 2; } description() { return this.coffee.description() + ", Milk"; } } class SugarDecorator implements Coffee { constructor(private coffee: Coffee) {} cost() { return this.coffee.cost() + 1; } description() { return this.coffee.description() + ", Sugar"; } } const plain = new SimpleCoffee(); const latte = new MilkDecorator(plain); const sweetLatte = new SugarDecorator(latte); console.log(sweetLatte.cost()); // 8 console.log(sweetLatte.description()); // "Coffee, Milk, Sugar" ``` Три класи покривають будь-яку комбінацію. Жодного підкласу `CoffeeWithMilkAndSugar` не потрібно. ### Середній рівень: Express middleware як декоратори Express middleware - це реальний ланцюг декораторів. Кожна функція обгортає наступний обробник і вирішує, чи передавати виклик далі. ```typescript type Handler = (req: Request, res: Response) => void; function getUser(req: Request, res: Response) { const user = db.findUser(req.params.id); res.json(user); } function withAuth(handler: Handler): Handler { return (req, res) => { if (!req.headers.authorization) { return res.status(401).json({ error: "Unauthorized" }); } return handler(req, res); }; } function withLogging(handler: Handler): Handler { return (req, res) => { console.log(`${req.method} ${req.path}`); return handler(req, res); }; } // Останній застосований виконується першим const secureHandler = withLogging(withAuth(getUser)); app.get("/users/:id", secureHandler); // Потік запиту: логування -> перевірка авторизації -> оригінальний обробник ``` `withLogging` обгортає `withAuth`, який обгортає `getUser`. Кожен шар відповідає за одну задачу і не торкається інших. ### Просунутий рівень: декоратор з підтримкою async Декоратор, який однаково працює і з синхронними, і з асинхронними методами. ```typescript interface DataProcessor { process(data: string): string | Promise<string>; } class RawProcessor implements DataProcessor { process(data: string) { return data; } } class UppercaseDecorator implements DataProcessor { constructor(private processor: DataProcessor) {} process(data: string) { const result = this.processor.process(data); // Обробляємо обидва варіанти прозоро if (result instanceof Promise) { return result.then(value => value.toUpperCase()); } return result.toUpperCase(); } } // Працює з синхронним процесором const syncDecorated = new UppercaseDecorator(new RawProcessor()); console.log(syncDecorated.process("hello")); // "HELLO" // Працює і з асинхронним class AsyncRawProcessor implements DataProcessor { process(data: string) { return Promise.resolve(data); } } const asyncDecorated = new UppercaseDecorator(new AsyncRawProcessor()); asyncDecorated.process("hello").then(console.log); // "HELLO" ``` Декоратор не знає і не дбає, чи синхронний чи асинхронний внутрішній процесор. Він перевіряє тип результату в рантаймі і обробляє обидва випадки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.