1. Pub-Sub Pattern là gì?

Pub-Sub (Publisher-Subscriber) Pattern là một mẫu thiết kế hành vi trong đó publishers (nhà phát hành) không gửi thông báo trực tiếp đến subscribers (người đăng ký) cụ thể. Thay vào đó, các thông báo được phân loại thành các kênh (channels) hoặc chủ đề (topics) mà không cần biết có những subscribers nào đang lắng nghe.

Điểm khác biệt chính giữa Pub-Sub và Observer Pattern là:

  • Trong Observer Pattern, subject (chủ thể) biết rõ về observers (người quan sát) của nó
  • Trong Pub-Sub Pattern, publishers và subscribers không biết về sự tồn tại của nhau, chúng giao tiếp thông qua một trung gian (message broker/event bus)

  graph LR
    A[Publisher] -->|Publish| B[Event Channel/Broker]
    B -->|Notify| C[Subscriber 1]
    B -->|Notify| D[Subscriber 2]
    B -->|Notify| E[Subscriber 3]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#dfd,stroke:#333,stroke-width:1px
    style D fill:#dfd,stroke:#333,stroke-width:1px
    style E fill:#dfd,stroke:#333,stroke-width:1px

2. Cấu trúc của Pub-Sub Pattern

  1. Publisher: Đối tượng tạo ra thông báo và gửi đến kênh
  2. Subscriber: Đối tượng đăng ký với kênh để nhận thông báo
  3. Message Broker/Event Bus: Trung gian quản lý việc đăng ký và phân phối thông báo
  4. Channel/Topic: Phân loại thông báo theo chủ đề

3. Triển khai Pub-Sub Pattern trong JavaScript

3.1. Triển khai cơ bản

class PubSub {
  constructor() {
    this.channels = {};
  }

  // Đăng ký subscriber với một kênh
  subscribe(channel, subscriber) {
    if (!this.channels[channel]) {
      this.channels[channel] = [];
    }

    this.channels[channel].push(subscriber);
    return () => this.unsubscribe(channel, subscriber);
  }

  // Hủy đăng ký subscriber từ một kênh
  unsubscribe(channel, subscriber) {
    if (!this.channels[channel]) return;

    this.channels[channel] = this.channels[channel].filter(
      (sub) => sub !== subscriber
    );
  }

  // Publish thông báo đến một kênh
  publish(channel, data) {
    if (!this.channels[channel]) return;

    this.channels[channel].forEach((subscriber) => {
      subscriber(data);
    });
  }
}

// Sử dụng
const pubsub = new PubSub();

// Tạo subscribers
const userSubscriber = (data) => {
  console.log(`User event received: ${JSON.stringify(data)}`);
};

const logSubscriber = (data) => {
  console.log(`Log event: ${JSON.stringify(data)}`);
};

// Đăng ký subscribers với các kênh
pubsub.subscribe("user:login", userSubscriber);
pubsub.subscribe("user:logout", userSubscriber);
pubsub.subscribe("log", logSubscriber);

// Publish thông báo
pubsub.publish("user:login", { userId: 123, username: "john_doe" });
pubsub.publish("log", { level: "info", message: "User logged in" });
pubsub.publish("user:logout", { userId: 123, username: "john_doe" });

3.2. Triển khai với TypeScript

type Subscriber<T> = (data: T) => void;
type Unsubscribe = () => void;

class PubSub<T = any> {
  private channels: Record<string, Subscriber<T>[]> = {};

  subscribe(channel: string, subscriber: Subscriber<T>): Unsubscribe {
    if (!this.channels[channel]) {
      this.channels[channel] = [];
    }

    this.channels[channel].push(subscriber);

    // Trả về hàm để hủy đăng ký
    return () => this.unsubscribe(channel, subscriber);
  }

  unsubscribe(channel: string, subscriber: Subscriber<T>): void {
    if (!this.channels[channel]) return;

    this.channels[channel] = this.channels[channel].filter(
      (sub) => sub !== subscriber
    );
  }

  publish(channel: string, data: T): void {
    if (!this.channels[channel]) return;

    this.channels[channel].forEach((subscriber) => {
      subscriber(data);
    });
  }
}

// Định nghĩa các kiểu dữ liệu cho các kênh
interface UserEvent {
  userId: number;
  username: string;
}

interface LogEvent {
  level: "info" | "warning" | "error";
  message: string;
}

// Sử dụng với kiểu dữ liệu cụ thể
const userPubSub = new PubSub<UserEvent>();
const logPubSub = new PubSub<LogEvent>();

// Đăng ký subscribers
const userLoginSubscriber = (data: UserEvent) => {
  console.log(`User logged in: ${data.username}`);
};

const logSubscriber = (data: LogEvent) => {
  console.log(`[${data.level.toUpperCase()}] ${data.message}`);
};

// Đăng ký và lưu hàm hủy đăng ký
const unsubscribeUserLogin = userPubSub.subscribe("login", userLoginSubscriber);
logPubSub.subscribe("system", logSubscriber);

// Publish thông báo
userPubSub.publish("login", { userId: 123, username: "john_doe" });
logPubSub.publish("system", { level: "info", message: "System started" });

// Hủy đăng ký
unsubscribeUserLogin();

4. Ví dụ thực tế: Hệ thống thông báo trong ứng dụng

Dưới đây là một ví dụ về hệ thống thông báo trong ứng dụng sử dụng Pub-Sub Pattern:

// Định nghĩa kiểu dữ liệu cho các thông báo
interface Notification {
  id: string;
  type: "info" | "success" | "warning" | "error";
  message: string;
  timestamp: Date;
}

// Hệ thống thông báo sử dụng Pub-Sub
class NotificationSystem {
  private pubsub = new PubSub<Notification>();

  // Đăng ký nhận thông báo
  subscribe(
    type: "info" | "success" | "warning" | "error" | "all",
    callback: (notification: Notification) => void
  ): Unsubscribe {
    return this.pubsub.subscribe(type, callback);
  }

  // Gửi thông báo
  notify(
    type: "info" | "success" | "warning" | "error",
    message: string
  ): void {
    const notification: Notification = {
      id: this.generateId(),
      type,
      message,
      timestamp: new Date(),
    };

    // Publish thông báo đến kênh cụ thể
    this.pubsub.publish(type, notification);

    // Publish thông báo đến kênh 'all'
    this.pubsub.publish("all", notification);
  }

  private generateId(): string {
    return Math.random().toString(36).substring(2, 9);
  }
}

// Sử dụng hệ thống thông báo
const notificationSystem = new NotificationSystem();

// UI Component hiển thị thông báo
class NotificationUI {
  constructor(private container: HTMLElement) {
    // Đăng ký nhận tất cả thông báo
    notificationSystem.subscribe("all", this.displayNotification.bind(this));
  }

  private displayNotification(notification: Notification): void {
    const element = document.createElement("div");
    element.className = `notification ${notification.type}`;
    element.innerHTML = `
      <strong>${notification.type.toUpperCase()}</strong>: 
      ${notification.message}
      <small>${notification.timestamp.toLocaleTimeString()}</small>
    `;

    this.container.appendChild(element);

    // Tự động xóa thông báo sau 5 giây
    setTimeout(() => {
      element.classList.add("fade-out");
      setTimeout(() => element.remove(), 500);
    }, 5000);
  }
}

// Logger ghi lại thông báo lỗi
class ErrorLogger {
  constructor() {
    // Chỉ đăng ký nhận thông báo lỗi
    notificationSystem.subscribe("error", this.logError.bind(this));
  }

  private logError(notification: Notification): void {
    console.error(
      `ERROR [${notification.timestamp.toISOString()}]: ${notification.message}`
    );
    // Trong thực tế, có thể gửi lỗi đến hệ thống giám sát
  }
}

// Khởi tạo các thành phần
const notificationUI = new NotificationUI(
  document.getElementById("notifications") as HTMLElement
);
const errorLogger = new ErrorLogger();

// Tạo thông báo
notificationSystem.notify("info", "Ứng dụng đã khởi động");
notificationSystem.notify("success", "Dữ liệu đã được lưu thành công");
notificationSystem.notify("warning", "Kết nối mạng không ổn định");
notificationSystem.notify("error", "Không thể kết nối đến máy chủ");

5. Pub-Sub trong các framework và thư viện

5.1. Pub-Sub trong Node.js với EventEmitter

Node.js có sẵn module events với class EventEmitter hoạt động tương tự như Pub-Sub:

const EventEmitter = require("events");

class OrderSystem extends EventEmitter {
  placeOrder(product, user) {
    // Xử lý đặt hàng
    console.log(`Order placed: ${product} for ${user}`);

    // Emit sự kiện với dữ liệu
    this.emit("order_placed", { product, user, date: new Date() });
  }

  shipOrder(product, user) {
    // Xử lý vận chuyển
    console.log(`Order shipped: ${product} to ${user}`);

    // Emit sự kiện với dữ liệu
    this.emit("order_shipped", { product, user, date: new Date() });
  }
}

const orderSystem = new OrderSystem();

// Đăng ký listeners (subscribers)
orderSystem.on("order_placed", (data) => {
  console.log(`Notification: New order at ${data.date}`);
  // Gửi email xác nhận đơn hàng
});

orderSystem.on("order_shipped", (data) => {
  console.log(`Notification: Order for ${data.user} has been shipped`);
  // Gửi thông báo vận chuyển
});

// Thêm nhiều listeners cho cùng một sự kiện
orderSystem.on("order_placed", (data) => {
  console.log(`Inventory update: Reduce stock for ${data.product}`);
  // Cập nhật kho hàng
});

// Sử dụng
orderSystem.placeOrder("Laptop", "[email protected]");
orderSystem.shipOrder("Laptop", "[email protected]");

5.2. Pub-Sub trong React với Context API

React Context API có thể được sử dụng để triển khai Pub-Sub Pattern:

import React, { createContext, useContext, useState, useCallback } from "react";

// Tạo context
const PubSubContext = createContext(null);

// Provider component
export const PubSubProvider = ({ children }) => {
  const [subscribers, setSubscribers] = useState({});

  const subscribe = useCallback((channel, callback) => {
    setSubscribers((prev) => {
      const channelSubscribers = prev[channel] || [];
      return {
        ...prev,
        [channel]: [...channelSubscribers, callback],
      };
    });

    // Trả về hàm hủy đăng ký
    return () => {
      setSubscribers((prev) => {
        const channelSubscribers = prev[channel] || [];
        return {
          ...prev,
          [channel]: channelSubscribers.filter((cb) => cb !== callback),
        };
      });
    };
  }, []);

  const publish = useCallback(
    (channel, data) => {
      const channelSubscribers = subscribers[channel] || [];
      channelSubscribers.forEach((callback) => callback(data));
    },
    [subscribers]
  );

  return (
    <PubSubContext.Provider value={{ subscribe, publish }}>
      {children}
    </PubSubContext.Provider>
  );
};

// Hook để sử dụng PubSub
export const usePubSub = () => {
  const context = useContext(PubSubContext);
  if (!context) {
    throw new Error("usePubSub must be used within a PubSubProvider");
  }
  return context;
};

// Sử dụng trong component
const NotificationButton = () => {
  const { publish } = usePubSub();

  const handleClick = () => {
    publish("notification", {
      type: "success",
      message: "Operation completed successfully",
    });
  };

  return <button onClick={handleClick}>Notify Success</button>;
};

const NotificationList = () => {
  const { subscribe } = usePubSub();
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    // Đăng ký nhận thông báo
    const unsubscribe = subscribe("notification", (data) => {
      setNotifications((prev) => [...prev, data]);
    });

    // Hủy đăng ký khi component unmount
    return unsubscribe;
  }, [subscribe]);

  return (
    <div className="notification-list">
      {notifications.map((notification, index) => (
        <div key={index} className={`notification ${notification.type}`}>
          {notification.message}
        </div>
      ))}
    </div>
  );
};

// App component
const App = () => {
  return (
    <PubSubProvider>
      <NotificationButton />
      <NotificationList />
    </PubSubProvider>
  );
};

6. Ưu điểm và nhược điểm

Ưu điểm:

  1. Loose coupling: Publishers và subscribers không biết về sự tồn tại của nhau
  2. Khả năng mở rộng: Dễ dàng thêm publishers và subscribers mới mà không ảnh hưởng đến hệ thống
  3. Linh hoạt: Một subscriber có thể đăng ký nhiều kênh, một publisher có thể gửi thông báo đến nhiều kênh
  4. Phân tán: Có thể triển khai trên nhiều hệ thống phân tán

Nhược điểm:

  1. Khó debug: Khó theo dõi luồng dữ liệu khi hệ thống phức tạp
  2. Overhead: Có thể tạo ra overhead khi có nhiều subscribers và publishers
  3. Khó đảm bảo thứ tự: Không đảm bảo thứ tự xử lý các thông báo
  4. Memory leaks: Nếu không hủy đăng ký đúng cách, có thể gây ra memory leaks

7. So sánh với Observer Pattern

Tiêu chíObserver PatternPub-Sub Pattern
CouplingTight coupling (Subject biết về Observers)Loose coupling (Publishers và Subscribers không biết về nhau)
Trung gianKhông có trung gianCó trung gian (Message Broker/Event Bus)
Phạm viThường trong một ứng dụngCó thể mở rộng qua nhiều ứng dụng/hệ thống
Triển khaiĐơn giản hơnPhức tạp hơn do có trung gian
Use casesUI events, Object state changesDistributed systems, Cross-component communication

8. Khi nào nên sử dụng Pub-Sub Pattern?

  • Khi cần giao tiếp giữa các thành phần không liên quan trực tiếp
  • Trong hệ thống phân tán với nhiều microservices
  • Khi cần xử lý sự kiện bất đồng bộ
  • Khi cần giảm sự phụ thuộc giữa các thành phần
  • Trong các ứng dụng real-time cần phản ứng với nhiều loại sự kiện

9. Kết luận

Pub-Sub Pattern là một mẫu thiết kế mạnh mẽ cho phép xây dựng các hệ thống có khả năng mở rộng cao với các thành phần tách biệt. Mặc dù có một số nhược điểm như khó debug và overhead, pattern này vẫn là lựa chọn tốt cho các ứng dụng hiện đại cần xử lý nhiều sự kiện và có nhiều thành phần giao tiếp với nhau.

Trong JavaScript và TypeScript, Pub-Sub Pattern có thể được triển khai dễ dàng và được hỗ trợ bởi nhiều thư viện và framework. Hiểu rõ sự khác biệt giữa Pub-Sub và Observer Pattern sẽ giúp bạn chọn đúng mẫu thiết kế cho từng trường hợp sử dụng cụ thể.