Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Decorator Pattern! ๐ In this guide, weโll explore how to dynamically extend object functionality without modifying the original code.
Youโll discover how the Decorator Pattern can transform your TypeScript development experience. Whether youโre building web applications ๐, game engines ๐ฎ, or e-commerce platforms ๐, understanding this pattern is essential for writing flexible, maintainable code.
By the end of this tutorial, youโll feel confident using decorators to add superpowers to your objects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Decorator Pattern
๐ค What is the Decorator Pattern?
The Decorator Pattern is like adding toppings to a pizza ๐. Think of it as wrapping gifts ๐ - each wrapper adds a new layer of functionality while preserving whatโs inside.
In TypeScript terms, decorators allow you to add new behaviors to objects dynamically without altering their structure. This means you can:
- โจ Add features on-the-fly
- ๐ Combine multiple behaviors
- ๐ก๏ธ Keep original code untouched
๐ก Why Use Decorator Pattern?
Hereโs why developers love decorators:
- Open/Closed Principle ๐: Classes open for extension, closed for modification
- Flexible Composition ๐ป: Mix and match behaviors like LEGO blocks
- Runtime Configuration ๐: Add features based on user preferences
- Single Responsibility ๐ง: Each decorator does one thing well
Real-world example: Imagine building a coffee shop app โ. With decorators, you can add milk, sugar, whipped cream, or caramel to any coffee without changing the base coffee class!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Decorator Pattern!
interface Coffee {
cost(): number;
description(): string;
}
// ๐จ Base coffee implementation
class SimpleCoffee implements Coffee {
cost(): number {
return 2.00; // โ Basic coffee price
}
description(): string {
return "Simple coffee";
}
}
// ๐ฏ Abstract decorator
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
abstract cost(): number;
abstract description(): string;
}
๐ก Explanation: We define a base interface and a simple implementation. The abstract decorator will be our foundation for adding features!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Milk decorator
class MilkDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.50; // ๐ฅ Add milk cost
}
description(): string {
return `${this.coffee.description()} + Milk`;
}
}
// ๐จ Pattern 2: Sugar decorator
class SugarDecorator extends CoffeeDecorator {
cost(): number {
return this.coffee.cost() + 0.25; // ๐ฌ Sweet addition
}
description(): string {
return `${this.coffee.description()} + Sugar`;
}
}
// ๐ Pattern 3: Using decorators
const myCoffee = new SimpleCoffee();
const coffeeWithMilk = new MilkDecorator(myCoffee);
const sweetCoffee = new SugarDecorator(coffeeWithMilk);
console.log(`${sweetCoffee.description()} costs $${sweetCoffee.cost()}`);
// Output: Simple coffee + Milk + Sugar costs $2.75 โโจ
๐ก Practical Examples
๐ Example 1: E-commerce Product Features
Letโs build something real:
// ๐๏ธ Define our product interface
interface Product {
getPrice(): number;
getDescription(): string;
getFeatures(): string[];
}
// ๐ฆ Base product class
class BasicProduct implements Product {
constructor(
private name: string,
private basePrice: number,
private emoji: string // Every product needs an emoji!
) {}
getPrice(): number {
return this.basePrice;
}
getDescription(): string {
return `${this.emoji} ${this.name}`;
}
getFeatures(): string[] {
return ["๐ฆ Standard packaging"];
}
}
// ๐ Gift wrap decorator
class GiftWrapDecorator implements Product {
constructor(private product: Product) {}
getPrice(): number {
return this.product.getPrice() + 5.99; // ๐ Gift wrap fee
}
getDescription(): string {
return `${this.product.getDescription()} (Gift Wrapped)`;
}
getFeatures(): string[] {
return [...this.product.getFeatures(), "๐ Beautiful gift wrapping"];
}
}
// โก Express shipping decorator
class ExpressShippingDecorator implements Product {
constructor(private product: Product) {}
getPrice(): number {
return this.product.getPrice() + 15.00; // ๐ Express fee
}
getDescription(): string {
return `${this.product.getDescription()} with Express Shipping`;
}
getFeatures(): string[] {
return [...this.product.getFeatures(), "โก 24-hour delivery"];
}
}
// ๐ฎ Let's use it!
const laptop = new BasicProduct("Gaming Laptop", 999.99, "๐ป");
const giftLaptop = new GiftWrapDecorator(laptop);
const rushOrder = new ExpressShippingDecorator(giftLaptop);
console.log(rushOrder.getDescription());
console.log(`Total: $${rushOrder.getPrice()}`);
console.log("Features:", rushOrder.getFeatures().join(", "));
๐ฏ Try it yourself: Add a warranty decorator and insurance decorator!
๐ฎ Example 2: Game Character Abilities
Letโs make it fun:
// ๐ Character interface
interface GameCharacter {
attack(): number;
defend(): number;
getAbilities(): string[];
getStats(): string;
}
// ๐ฆธ Base character
class BaseCharacter implements GameCharacter {
constructor(
private name: string,
private baseAttack: number,
private baseDefense: number
) {}
attack(): number {
return this.baseAttack;
}
defend(): number {
return this.baseDefense;
}
getAbilities(): string[] {
return ["โ๏ธ Basic Attack"];
}
getStats(): string {
return `${this.name} - ATK: ${this.attack()} | DEF: ${this.defend()}`;
}
}
// ๐ฅ Fire enchantment decorator
class FireEnchantment implements GameCharacter {
constructor(private character: GameCharacter) {}
attack(): number {
return this.character.attack() + 15; // ๐ฅ Fire damage bonus
}
defend(): number {
return this.character.defend();
}
getAbilities(): string[] {
return [...this.character.getAbilities(), "๐ฅ Fire Strike"];
}
getStats(): string {
return `${this.character.getStats()} ๐ฅ`;
}
}
// ๐ก๏ธ Shield decorator
class ShieldDecorator implements GameCharacter {
constructor(private character: GameCharacter) {}
attack(): number {
return this.character.attack();
}
defend(): number {
return this.character.defend() + 20; // ๐ก๏ธ Shield bonus
}
getAbilities(): string[] {
return [...this.character.getAbilities(), "๐ก๏ธ Shield Block"];
}
getStats(): string {
return `${this.character.getStats()} ๐ก๏ธ`;
}
}
// ๐ฎ Create a powerful character!
const hero = new BaseCharacter("Knight", 50, 30);
const fireKnight = new FireEnchantment(hero);
const shieldedFireKnight = new ShieldDecorator(fireKnight);
console.log(shieldedFireKnight.getStats());
console.log("Abilities:", shieldedFireKnight.getAbilities().join(", "));
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: TypeScript Decorators
When youโre ready to level up, try TypeScriptโs built-in decorators:
// ๐ฏ Method decorator
const LogExecution = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`โก Executing ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`โจ Result:`, result);
return result;
};
return descriptor;
};
// ๐ช Using the decorator
class MagicCalculator {
@LogExecution
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new MagicCalculator();
calc.multiply(5, 3); // โก Logs execution automatically!
๐๏ธ Advanced Topic 2: Decorator Factory
For the brave developers:
// ๐ Decorator factory with configuration
const PowerUp = (powerLevel: number, emoji: string) => {
return (constructor: Function) => {
constructor.prototype.powerLevel = powerLevel;
constructor.prototype.powerEmoji = emoji;
constructor.prototype.showPower = function() {
return `${this.powerEmoji} Power Level: ${this.powerLevel}`;
};
};
};
// ๐ช Apply decorators with different configs
@PowerUp(9000, "๐ฅ")
class SuperSaiyan {
name = "Goku";
}
@PowerUp(100, "โก")
class Pikachu {
name = "Pikachu";
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Decorator Order Matters
// โ Wrong way - order confusion!
const coffee1 = new MilkDecorator(new SugarDecorator(new SimpleCoffee()));
const coffee2 = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
// ๐ฅ Different results if decorators have side effects!
// โ
Correct way - be intentional about order!
class OrderedDecorator {
private decorators: CoffeeDecorator[] = [];
add(decorator: typeof CoffeeDecorator, coffee: Coffee): Coffee {
// ๐ฏ Clear, trackable decoration order
return new decorator(coffee);
}
}
๐คฏ Pitfall 2: Infinite Decoration
// โ Dangerous - might create infinite loop!
const addAllTheThings = (product: Product): Product => {
return new GiftWrapDecorator(addAllTheThings(product));
// ๐ฅ Stack overflow!
}
// โ
Safe - add validation!
class SafeDecorator {
private static maxDecorations = 5;
private decorationCount = 0;
decorate(product: Product): Product {
if (this.decorationCount >= SafeDecorator.maxDecorations) {
console.log("โ ๏ธ Maximum decorations reached!");
return product;
}
this.decorationCount++;
return new GiftWrapDecorator(product); // โ
Safe now!
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Decorators Focused: Each decorator should add one specific feature
- ๐ Document Decoration Order: Make it clear which order decorators should be applied
- ๐ก๏ธ Validate Inputs: Check that the wrapped object is valid
- ๐จ Use Meaningful Names:
PremiumShippingDecorator
notDecorator1
- โจ Consider Performance: Too many decorators can impact performance
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Pizza Order System
Create a type-safe pizza ordering system with decorators:
๐ Requirements:
- โ Base pizza with size options (small, medium, large)
- ๐ท๏ธ Topping decorators (cheese, pepperoni, mushrooms, etc.)
- ๐ค Special request decorator for custom instructions
- ๐ Delivery time decorator
- ๐จ Each topping needs an emoji!
๐ Bonus Points:
- Add pricing based on size
- Implement a maximum toppings limit
- Create a receipt generator
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe pizza system!
interface Pizza {
getCost(): number;
getDescription(): string;
getToppings(): string[];
getSize(): "small" | "medium" | "large";
}
// ๐ Base pizza
class BasePizza implements Pizza {
constructor(private size: "small" | "medium" | "large") {}
getCost(): number {
const prices = { small: 8, medium: 12, large: 16 };
return prices[this.size];
}
getDescription(): string {
return `${this.size} pizza`;
}
getToppings(): string[] {
return ["๐
Tomato sauce", "๐ง Mozzarella"];
}
getSize(): "small" | "medium" | "large" {
return this.size;
}
}
// ๐ Abstract topping decorator
abstract class ToppingDecorator implements Pizza {
constructor(protected pizza: Pizza) {}
abstract getCost(): number;
abstract getDescription(): string;
abstract getToppings(): string[];
getSize(): "small" | "medium" | "large" {
return this.pizza.getSize();
}
}
// ๐ฅ Pepperoni decorator
class PepperoniDecorator extends ToppingDecorator {
getCost(): number {
return this.pizza.getCost() + 2.50;
}
getDescription(): string {
return `${this.pizza.getDescription()} + Pepperoni`;
}
getToppings(): string[] {
return [...this.pizza.getToppings(), "๐ฅ Pepperoni"];
}
}
// ๐ Mushroom decorator
class MushroomDecorator extends ToppingDecorator {
getCost(): number {
return this.pizza.getCost() + 1.50;
}
getDescription(): string {
return `${this.pizza.getDescription()} + Mushrooms`;
}
getToppings(): string[] {
return [...this.pizza.getToppings(), "๐ Mushrooms"];
}
}
// ๐ Special request decorator
class SpecialRequestDecorator extends ToppingDecorator {
constructor(pizza: Pizza, private request: string) {
super(pizza);
}
getCost(): number {
return this.pizza.getCost(); // ๐ No extra charge!
}
getDescription(): string {
return `${this.pizza.getDescription()} (${this.request})`;
}
getToppings(): string[] {
return [...this.pizza.getToppings(), `๐ Special: ${this.request}`];
}
}
// ๐ Delivery decorator
class DeliveryDecorator implements Pizza {
constructor(
private pizza: Pizza,
private deliveryTime: number // Minutes
) {}
getCost(): number {
const deliveryFee = this.deliveryTime <= 30 ? 5 : 3;
return this.pizza.getCost() + deliveryFee;
}
getDescription(): string {
return `${this.pizza.getDescription()} ๐ (${this.deliveryTime}min delivery)`;
}
getToppings(): string[] {
return this.pizza.getToppings();
}
getSize(): "small" | "medium" | "large" {
return this.pizza.getSize();
}
}
// ๐ฎ Test it out!
const myPizza = new BasePizza("large");
const pepperoniPizza = new PepperoniDecorator(myPizza);
const mushroomPepperoniPizza = new MushroomDecorator(pepperoniPizza);
const specialPizza = new SpecialRequestDecorator(
mushroomPepperoniPizza,
"Extra crispy crust"
);
const deliveredPizza = new DeliveryDecorator(specialPizza, 25);
console.log("๐ Order Summary:");
console.log(`Description: ${deliveredPizza.getDescription()}`);
console.log(`Toppings: ${deliveredPizza.getToppings().join(", ")}`);
console.log(`Total Cost: $${deliveredPizza.getCost().toFixed(2)}`);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create decorators with confidence ๐ช
- โ Avoid common mistakes like infinite loops and order issues ๐ก๏ธ
- โ Apply decorators in real projects ๐ฏ
- โ Debug decorator chains like a pro ๐
- โ Build flexible systems with TypeScript! ๐
Remember: The Decorator Pattern is your friend for adding features without changing existing code! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Decorator Pattern!
Hereโs what to do next:
- ๐ป Practice with the pizza ordering exercise above
- ๐๏ธ Build a notification system with email, SMS, and push decorators
- ๐ Move on to our next tutorial: Abstract Factory Pattern
- ๐ Share your decorated creations with others!
Remember: Every design pattern expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ