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
- Publisher: Đối tượng tạo ra thông báo và gửi đến kênh
- Subscriber: Đối tượng đăng ký với kênh để nhận thông báo
- Message Broker/Event Bus: Trung gian quản lý việc đăng ký và phân phối thông báo
- 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:
- Loose coupling: Publishers và subscribers không biết về sự tồn tại của nhau
- 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
- 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
- Phân tán: Có thể triển khai trên nhiều hệ thống phân tán
Nhược điểm:
- Khó debug: Khó theo dõi luồng dữ liệu khi hệ thống phức tạp
- Overhead: Có thể tạo ra overhead khi có nhiều subscribers và publishers
- Khó đảm bảo thứ tự: Không đảm bảo thứ tự xử lý các thông báo
- 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 Pattern | Pub-Sub Pattern |
---|---|---|
Coupling | Tight coupling (Subject biết về Observers) | Loose coupling (Publishers và Subscribers không biết về nhau) |
Trung gian | Không có trung gian | Có trung gian (Message Broker/Event Bus) |
Phạm vi | Thường trong một ứng dụng | Có thể mở rộng qua nhiều ứng dụng/hệ thống |
Triển khai | Đơn giản hơn | Phức tạp hơn do có trung gian |
Use cases | UI events, Object state changes | Distributed 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ể.