+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 299 of 354

๐Ÿ“˜ Mediator Pattern: Object Communication

Master mediator pattern: object communication in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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 Mediator Pattern! ๐ŸŽ‰ In this guide, weโ€™ll explore how to manage complex object communication in TypeScript using this powerful design pattern.

Have you ever tried to coordinate a group chat where everyone talks to everyone? It gets messy fast! ๐Ÿ˜… The Mediator Pattern is like having a group admin who manages all the messages - making communication organized and efficient.

By the end of this tutorial, youโ€™ll feel confident implementing the Mediator Pattern in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Mediator Pattern

๐Ÿค” What is the Mediator Pattern?

The Mediator Pattern is like an air traffic controller at an airport ๐Ÿ›ซ. Instead of planes talking directly to each other (chaos!), they all communicate through the control tower. This central mediator coordinates all interactions, keeping things safe and organized.

In TypeScript terms, the Mediator Pattern defines how a set of objects interact by encapsulating their communication in a mediator object. This means you can:

  • โœจ Reduce dependencies between communicating objects
  • ๐Ÿš€ Change interaction logic in one place
  • ๐Ÿ›ก๏ธ Keep objects loosely coupled

๐Ÿ’ก Why Use the Mediator Pattern?

Hereโ€™s why developers love the Mediator Pattern:

  1. Decoupling ๐Ÿ”—: Objects donโ€™t need to know about each other
  2. Reusability โ™ป๏ธ: Components can be reused in different contexts
  3. Maintainability ๐Ÿ”ง: Changes to communication logic are centralized
  4. Testability ๐Ÿงช: Easier to test individual components

Real-world example: Imagine building a smart home system ๐Ÿ . With the Mediator Pattern, your lights, thermostat, and security system donโ€™t talk directly - they communicate through a central hub!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly chat room example:

// ๐Ÿ‘‹ Hello, Mediator Pattern!
interface Mediator {
  sendMessage(message: string, sender: User): void;
  addUser(user: User): void;
}

// ๐ŸŽจ Creating a user class
class User {
  constructor(
    public name: string,
    private mediator: Mediator
  ) {}
  
  // ๐Ÿ’ฌ Send a message
  send(message: string): void {
    console.log(`${this.name}: ${message}`);
    this.mediator.sendMessage(message, this);
  }
  
  // ๐Ÿ“จ Receive a message
  receive(message: string, sender: string): void {
    console.log(`  ${this.name} received from ${sender}: ${message}`);
  }
}

// ๐ŸŽฏ The chat room mediator
class ChatRoom implements Mediator {
  private users: User[] = [];
  
  addUser(user: User): void {
    this.users.push(user);
    console.log(`โœจ ${user.name} joined the chat!`);
  }
  
  sendMessage(message: string, sender: User): void {
    this.users.forEach(user => {
      if (user !== sender) {
        user.receive(message, sender.name);
      }
    });
  }
}

๐Ÿ’ก Explanation: Notice how users donโ€™t know about each other - they only know about the mediator! The ChatRoom handles all the message routing.

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

// ๐Ÿ—๏ธ Pattern 1: Abstract mediator
abstract class BaseMediator {
  protected components: Component[] = [];
  
  abstract notify(sender: Component, event: string): void;
  
  register(component: Component): void {
    this.components.push(component);
    component.setMediator(this);
  }
}

// ๐ŸŽจ Pattern 2: Component interface
interface Component {
  setMediator(mediator: BaseMediator): void;
  operation(): void;
}

// ๐Ÿ”„ Pattern 3: Concrete implementation
class ConcreteComponent implements Component {
  private mediator!: BaseMediator;
  
  setMediator(mediator: BaseMediator): void {
    this.mediator = mediator;
  }
  
  operation(): void {
    this.mediator.notify(this, "operation");
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Smart Shopping System

Letโ€™s build a shopping system where inventory, payment, and shipping communicate:

// ๐Ÿ›๏ธ Define our shopping mediator
interface ShoppingMediator {
  processOrder(orderId: string): void;
  notifyComponent(sender: string, event: string, data?: any): void;
}

// ๐Ÿ“ฆ Inventory system
class InventorySystem {
  private mediator: ShoppingMediator;
  private stock: Map<string, number> = new Map([
    ["laptop", 5],
    ["phone", 10],
    ["tablet", 3]
  ]);
  
  constructor(mediator: ShoppingMediator) {
    this.mediator = mediator;
  }
  
  // ๐Ÿ” Check if item is available
  checkStock(item: string, quantity: number): boolean {
    const available = this.stock.get(item) || 0;
    const hasStock = available >= quantity;
    
    console.log(`๐Ÿ“ฆ Inventory: ${item} - ${hasStock ? "โœ… In stock" : "โŒ Out of stock"}`);
    this.mediator.notifyComponent("inventory", "stock-checked", { item, hasStock });
    
    return hasStock;
  }
  
  // ๐Ÿ“‰ Reserve items
  reserveItems(item: string, quantity: number): void {
    const current = this.stock.get(item) || 0;
    this.stock.set(item, current - quantity);
    console.log(`๐Ÿ“ฆ Reserved ${quantity} ${item}(s)`);
  }
}

// ๐Ÿ’ณ Payment system
class PaymentSystem {
  private mediator: ShoppingMediator;
  
  constructor(mediator: ShoppingMediator) {
    this.mediator = mediator;
  }
  
  // ๐Ÿ’ฐ Process payment
  processPayment(amount: number): boolean {
    const success = Math.random() > 0.1; // 90% success rate
    
    if (success) {
      console.log(`๐Ÿ’ณ Payment: $${amount} processed successfully! โœ…`);
      this.mediator.notifyComponent("payment", "payment-success", { amount });
    } else {
      console.log(`๐Ÿ’ณ Payment: Failed to process $${amount} โŒ`);
      this.mediator.notifyComponent("payment", "payment-failed");
    }
    
    return success;
  }
}

// ๐Ÿšš Shipping system
class ShippingSystem {
  private mediator: ShoppingMediator;
  
  constructor(mediator: ShoppingMediator) {
    this.mediator = mediator;
  }
  
  // ๐Ÿ“ฎ Schedule delivery
  scheduleDelivery(orderId: string): void {
    const deliveryDate = new Date();
    deliveryDate.setDate(deliveryDate.getDate() + 3);
    
    console.log(`๐Ÿšš Shipping: Order ${orderId} scheduled for ${deliveryDate.toDateString()}`);
    this.mediator.notifyComponent("shipping", "delivery-scheduled", { orderId, date: deliveryDate });
  }
}

// ๐ŸŽฎ The shopping mediator
class ShoppingSystemMediator implements ShoppingMediator {
  private inventory: InventorySystem;
  private payment: PaymentSystem;
  private shipping: ShippingSystem;
  
  constructor() {
    this.inventory = new InventorySystem(this);
    this.payment = new PaymentSystem(this);
    this.shipping = new ShippingSystem(this);
  }
  
  // ๐Ÿ›’ Process complete order
  processOrder(orderId: string): void {
    console.log(`\n๐Ÿ›’ Processing order ${orderId}...`);
    
    // Check inventory
    if (this.inventory.checkStock("laptop", 1)) {
      // Process payment
      if (this.payment.processPayment(999.99)) {
        // Reserve items and schedule shipping
        this.inventory.reserveItems("laptop", 1);
        this.shipping.scheduleDelivery(orderId);
        console.log(`\n๐ŸŽ‰ Order ${orderId} completed successfully!`);
      } else {
        console.log(`\n๐Ÿ˜” Order ${orderId} failed - payment issue`);
      }
    } else {
      console.log(`\n๐Ÿ˜” Order ${orderId} failed - out of stock`);
    }
  }
  
  // ๐Ÿ“ข Handle notifications between components
  notifyComponent(sender: string, event: string, data?: any): void {
    // Central logging or additional coordination logic
    console.log(`๐Ÿ“ข Event from ${sender}: ${event}`);
  }
}

// ๐ŸŽฎ Let's use it!
const shop = new ShoppingSystemMediator();
shop.processOrder("ORD-12345");

๐ŸŽฏ Try it yourself: Add a notification system that emails customers about order status!

๐ŸŽฎ Example 2: Game Event System

Letโ€™s make a game where players, enemies, and the UI communicate:

// ๐Ÿ† Game mediator for coordinating events
interface GameMediator {
  notify(sender: GameObject, event: GameEvent): void;
  registerObject(obj: GameObject): void;
}

// ๐ŸŽฏ Game events
type GameEvent = 
  | { type: "damage"; amount: number; target: string }
  | { type: "heal"; amount: number }
  | { type: "levelUp" }
  | { type: "enemyDefeated"; exp: number };

// ๐ŸŽฎ Base game object
abstract class GameObject {
  constructor(
    protected name: string,
    protected mediator: GameMediator
  ) {
    mediator.registerObject(this);
  }
  
  abstract handleEvent(event: GameEvent): void;
}

// ๐Ÿฆธ Player class
class Player extends GameObject {
  private health = 100;
  private level = 1;
  private exp = 0;
  
  constructor(name: string, mediator: GameMediator) {
    super(name, mediator);
    console.log(`๐Ÿฆธ ${name} enters the game!`);
  }
  
  // โš”๏ธ Attack an enemy
  attack(targetName: string): void {
    const damage = 10 + this.level * 5;
    console.log(`โš”๏ธ ${this.name} attacks for ${damage} damage!`);
    this.mediator.notify(this, { type: "damage", amount: damage, target: targetName });
  }
  
  // ๐Ÿ“จ Handle game events
  handleEvent(event: GameEvent): void {
    switch (event.type) {
      case "damage":
        if (event.target === this.name) {
          this.health -= event.amount;
          console.log(`๐Ÿ’” ${this.name} takes ${event.amount} damage! Health: ${this.health}`);
        }
        break;
      case "enemyDefeated":
        this.exp += event.exp;
        console.log(`โœจ ${this.name} gains ${event.exp} exp!`);
        if (this.exp >= this.level * 100) {
          this.levelUp();
        }
        break;
    }
  }
  
  // ๐Ÿ“ˆ Level up!
  private levelUp(): void {
    this.level++;
    this.health = 100 + this.level * 20;
    console.log(`๐ŸŽ‰ ${this.name} reached level ${this.level}!`);
    this.mediator.notify(this, { type: "levelUp" });
  }
}

// ๐Ÿ‘พ Enemy class
class Enemy extends GameObject {
  private health: number;
  private expReward: number;
  
  constructor(name: string, health: number, expReward: number, mediator: GameMediator) {
    super(name, mediator);
    this.health = health;
    this.expReward = expReward;
    console.log(`๐Ÿ‘พ Wild ${name} appears!`);
  }
  
  handleEvent(event: GameEvent): void {
    if (event.type === "damage" && event.target === this.name) {
      this.health -= event.amount;
      console.log(`๐Ÿ’ฅ ${this.name} takes ${event.amount} damage! Health: ${this.health}`);
      
      if (this.health <= 0) {
        console.log(`โ˜ ๏ธ ${this.name} is defeated!`);
        this.mediator.notify(this, { type: "enemyDefeated", exp: this.expReward });
      }
    }
  }
}

// ๐Ÿ“Š UI System
class GameUI extends GameObject {
  constructor(mediator: GameMediator) {
    super("UI", mediator);
  }
  
  handleEvent(event: GameEvent): void {
    switch (event.type) {
      case "levelUp":
        this.showNotification("๐ŸŒŸ LEVEL UP! ๐ŸŒŸ");
        break;
      case "enemyDefeated":
        this.showNotification("๐Ÿ’€ Enemy Defeated! ๐Ÿ’€");
        break;
    }
  }
  
  private showNotification(message: string): void {
    console.log(`\n๐Ÿ–ผ๏ธ UI Notification: ${message}\n`);
  }
}

// ๐ŸŽฏ The game mediator
class BattleMediator implements GameMediator {
  private gameObjects: GameObject[] = [];
  
  registerObject(obj: GameObject): void {
    this.gameObjects.push(obj);
  }
  
  notify(sender: GameObject, event: GameEvent): void {
    // Broadcast event to all game objects
    this.gameObjects.forEach(obj => {
      if (obj !== sender) {
        obj.handleEvent(event);
      }
    });
  }
}

// ๐ŸŽฎ Let's play!
const game = new BattleMediator();
const ui = new GameUI(game);
const hero = new Player("TypeScript Hero", game);
const goblin = new Enemy("Goblin", 30, 50, game);

hero.attack("Goblin");
hero.attack("Goblin");
hero.attack("Goblin");

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Type-Safe Event System

When youโ€™re ready to level up, try this advanced pattern:

// ๐ŸŽฏ Advanced type-safe mediator
type EventMap = {
  userJoined: { userId: string; timestamp: Date };
  messagePosted: { userId: string; message: string };
  userLeft: { userId: string };
};

type EventType = keyof EventMap;

class TypeSafeMediator {
  private handlers = new Map<EventType, Set<(data: any) => void>>();
  
  // โœจ Type-safe event subscription
  on<T extends EventType>(
    event: T,
    handler: (data: EventMap[T]) => void
  ): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
  }
  
  // ๐Ÿš€ Type-safe event emission
  emit<T extends EventType>(event: T, data: EventMap[T]): void {
    const eventHandlers = this.handlers.get(event);
    if (eventHandlers) {
      eventHandlers.forEach(handler => handler(data));
    }
  }
}

// ๐Ÿช„ Using the type-safe mediator
const mediator = new TypeSafeMediator();

mediator.on("userJoined", (data) => {
  // TypeScript knows data has userId and timestamp! โœจ
  console.log(`๐Ÿ‘‹ ${data.userId} joined at ${data.timestamp}`);
});

mediator.emit("userJoined", { 
  userId: "hero123", 
  timestamp: new Date() 
});

๐Ÿ—๏ธ Advanced Topic 2: Async Mediator Pattern

For the brave developers handling async operations:

// ๐Ÿš€ Async mediator for complex operations
class AsyncMediator {
  private asyncHandlers = new Map<string, (data: any) => Promise<any>>();
  
  // ๐ŸŽฏ Register async handler
  registerHandler(event: string, handler: (data: any) => Promise<any>): void {
    this.asyncHandlers.set(event, handler);
  }
  
  // โšก Process async request
  async request<T>(event: string, data: any): Promise<T> {
    const handler = this.asyncHandlers.get(event);
    if (!handler) {
      throw new Error(`No handler for event: ${event}`);
    }
    
    console.log(`โณ Processing ${event}...`);
    const result = await handler(data);
    console.log(`โœ… ${event} completed!`);
    
    return result;
  }
}

// ๐ŸŽฎ Example usage
const asyncMediator = new AsyncMediator();

// Register handlers
asyncMediator.registerHandler("fetchUser", async (userId: string) => {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  return { id: userId, name: "TypeScript Master", level: 42 };
});

// Use it!
async function demo() {
  const user = await asyncMediator.request("fetchUser", "user123");
  console.log(`๐ŸŽ‰ Fetched user: ${user.name}`);
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: The God Mediator

// โŒ Wrong way - mediator knows too much!
class GodMediator {
  private userAge: number = 0;
  private userEmail: string = "";
  private productPrice: number = 0;
  // ... hundreds of properties ๐Ÿ˜ฐ
  
  handleEverything(): void {
    // Complex business logic everywhere! ๐Ÿ’ฅ
  }
}

// โœ… Correct way - keep mediator focused!
class FocusedMediator {
  notify(sender: Component, event: string): void {
    // Just coordinate, don't store business data! ๐Ÿ›ก๏ธ
    this.components.forEach(c => {
      if (c !== sender) {
        c.handleEvent(event);
      }
    });
  }
}

๐Ÿคฏ Pitfall 2: Direct Component References

// โŒ Dangerous - components know each other!
class BadComponent {
  constructor(private otherComponent: BadComponent) {} // ๐Ÿ’ฅ Coupling!
  
  doSomething(): void {
    this.otherComponent.react(); // Direct call!
  }
}

// โœ… Safe - components only know mediator!
class GoodComponent {
  constructor(private mediator: Mediator) {} // โœ… Decoupled!
  
  doSomething(): void {
    this.mediator.notify(this, "something-happened");
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Keep Mediator Simple: Donโ€™t put business logic in the mediator
  2. ๐Ÿ“ Clear Event Names: Use descriptive event names like โ€œorder-completedโ€
  3. ๐Ÿ›ก๏ธ Type Safety: Use TypeScriptโ€™s type system for event data
  4. ๐ŸŽจ Single Responsibility: Each component should have one job
  5. โœจ Loose Coupling: Components shouldnโ€™t know about each other

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Smart Home System

Create a type-safe smart home system with mediator pattern:

๐Ÿ“‹ Requirements:

  • โœ… Devices: Lights, Thermostat, Security System, Smart Speaker
  • ๐Ÿท๏ธ Events: motion detected, temperature changed, voice command
  • ๐Ÿ‘ค Scenarios: Away mode, Sleep mode, Party mode
  • ๐Ÿ“… Time-based automation
  • ๐ŸŽจ Each device needs an emoji!

๐Ÿš€ Bonus Points:

  • Add energy saving features
  • Implement scene presets
  • Create a mobile app controller component

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our smart home mediator system!
interface SmartHomeMediator {
  notify(sender: SmartDevice, event: HomeEvent): void;
  registerDevice(device: SmartDevice): void;
  activateScene(scene: "away" | "sleep" | "party"): void;
}

type HomeEvent = 
  | { type: "motion"; location: string }
  | { type: "temperature"; value: number }
  | { type: "voice"; command: string }
  | { type: "lightChange"; brightness: number }
  | { type: "securityAlert"; severity: "low" | "high" };

abstract class SmartDevice {
  constructor(
    protected name: string,
    protected emoji: string,
    protected mediator: SmartHomeMediator
  ) {
    mediator.registerDevice(this);
    console.log(`${emoji} ${name} connected to smart home!`);
  }
  
  abstract handleEvent(event: HomeEvent): void;
}

// ๐Ÿ’ก Smart lights
class SmartLights extends SmartDevice {
  private brightness = 50;
  
  constructor(location: string, mediator: SmartHomeMediator) {
    super(`${location} Lights`, "๐Ÿ’ก", mediator);
  }
  
  setBrightness(level: number): void {
    this.brightness = level;
    console.log(`${this.emoji} ${this.name}: Brightness set to ${level}%`);
    this.mediator.notify(this, { type: "lightChange", brightness: level });
  }
  
  handleEvent(event: HomeEvent): void {
    switch (event.type) {
      case "motion":
        console.log(`${this.emoji} Motion detected - turning on lights!`);
        this.setBrightness(100);
        break;
      case "voice":
        if (event.command.includes("lights off")) {
          this.setBrightness(0);
        }
        break;
    }
  }
}

// ๐ŸŒก๏ธ Smart thermostat
class SmartThermostat extends SmartDevice {
  private temperature = 22;
  
  constructor(mediator: SmartHomeMediator) {
    super("Thermostat", "๐ŸŒก๏ธ", mediator);
  }
  
  setTemperature(temp: number): void {
    this.temperature = temp;
    console.log(`${this.emoji} Temperature set to ${temp}ยฐC`);
    this.mediator.notify(this, { type: "temperature", value: temp });
  }
  
  handleEvent(event: HomeEvent): void {
    if (event.type === "lightChange" && event.brightness === 0) {
      console.log(`${this.emoji} Lights off - entering sleep mode`);
      this.setTemperature(18);
    }
  }
}

// ๐Ÿ”’ Security system
class SecuritySystem extends SmartDevice {
  private armed = false;
  
  constructor(mediator: SmartHomeMediator) {
    super("Security System", "๐Ÿ”’", mediator);
  }
  
  arm(): void {
    this.armed = true;
    console.log(`${this.emoji} Security system ARMED ๐Ÿšจ`);
  }
  
  disarm(): void {
    this.armed = false;
    console.log(`${this.emoji} Security system disarmed โœ…`);
  }
  
  handleEvent(event: HomeEvent): void {
    if (event.type === "motion" && this.armed) {
      console.log(`${this.emoji} ALERT! Motion detected while armed! ๐Ÿšจ`);
      this.mediator.notify(this, { type: "securityAlert", severity: "high" });
    }
  }
}

// ๐Ÿ  The home automation hub
class SmartHomeHub implements SmartHomeMediator {
  private devices: SmartDevice[] = [];
  
  registerDevice(device: SmartDevice): void {
    this.devices.push(device);
  }
  
  notify(sender: SmartDevice, event: HomeEvent): void {
    this.devices.forEach(device => {
      if (device !== sender) {
        device.handleEvent(event);
      }
    });
  }
  
  activateScene(scene: "away" | "sleep" | "party"): void {
    console.log(`\n๐ŸŽฌ Activating ${scene} mode...`);
    
    switch (scene) {
      case "away":
        this.notify(this as any, { type: "voice", command: "lights off" });
        // In real implementation, would arm security
        break;
      case "sleep":
        this.notify(this as any, { type: "lightChange", brightness: 10 });
        break;
      case "party":
        this.notify(this as any, { type: "lightChange", brightness: 100 });
        console.log("๐ŸŽ‰ Party mode activated!");
        break;
    }
  }
}

// ๐ŸŽฎ Test it out!
const home = new SmartHomeHub();
const livingRoomLights = new SmartLights("Living Room", home);
const thermostat = new SmartThermostat(home);
const security = new SecuritySystem(home);

// Simulate events
console.log("\n๐Ÿƒ Someone enters the room...");
livingRoomLights.handleEvent({ type: "motion", location: "Living Room" });

console.log("\n๐Ÿ˜ด Time for bed...");
home.activateScene("sleep");

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create mediator patterns with confidence ๐Ÿ’ช
  • โœ… Avoid tight coupling between objects ๐Ÿ›ก๏ธ
  • โœ… Apply the pattern in real projects ๐ŸŽฏ
  • โœ… Debug communication issues like a pro ๐Ÿ›
  • โœ… Build scalable systems with TypeScript! ๐Ÿš€

Remember: The Mediator Pattern is your friend when objects need to talk but shouldnโ€™t know each otherโ€™s phone numbers! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the Mediator Pattern!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the smart home exercise above
  2. ๐Ÿ—๏ธ Refactor existing code to use mediator pattern
  3. ๐Ÿ“š Move on to our next tutorial: Observer Pattern
  4. ๐ŸŒŸ Share your mediator implementations with others!

Remember: Every TypeScript expert was once a beginner. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ