Lập trình · Mẫu thiết kế
Mẫu Observer (Design Pattern) giải thích kèm ví dụ
Trả lời nhanh
Observer là một mẫu thiết kế hành vi (behavioral) trong đó một đối tượng, gọi là subject, giữ danh sách các đối tượng phụ thuộc, gọi là observer, và tự động thông báo cho chúng khi trạng thái của nó thay đổi. Observer đăng ký và hủy đăng ký lúc chạy, và subject không cần biết lớp cụ thể của chúng. Trình lắng nghe sự kiện trong trình duyệt, EventEmitter của Node.js và các thư viện reactive như RxJS đều xây dựng trên ý tưởng này.
Mục đích và vấn đề mà mẫu này giải quyết
Mục đích: định nghĩa phụ thuộc một-nhiều giữa các đối tượng sao cho khi một đối tượng đổi trạng thái, mọi đối tượng phụ thuộc vào nó được thông báo và cập nhật tự động. Observer là một trong các mẫu hành vi trong sách Gang of Four (GoF), và còn được gọi là publish-subscribe hoặc mẫu listener.
Hãy tưởng tượng giỏ hàng của một cửa hàng web. Khi thêm một mặt hàng, biểu tượng giỏ ở phần đầu trang phải hiển thị số lượng mới, thanh bên phải hiển thị tổng mới, và một module phân tích phải ghi lại sự kiện. Nếu giỏ hàng gọi trực tiếp từng thứ này, module giỏ hàng phụ thuộc vào phần đầu trang, thanh bên và phân tích. Thêm phản ứng thứ tư nghĩa là lại phải sửa giỏ hàng.
Với Observer, giỏ hàng chỉ giữ một danh sách subscriber và gọi từng cái khi nó thay đổi. Phần đầu trang, thanh bên và phân tích tự đăng ký. Giỏ hàng không import chúng, và các tính năng mới có thể phản ứng với thay đổi của giỏ mà không chạm vào mã giỏ hàng.
Cấu trúc và các thành phần
| Vai trò | Trách nhiệm |
|---|---|
| Subject (publisher) | Giữ danh sách observer và cung cấp phương thức để đăng ký và hủy đăng ký; thông báo cho chúng khi có thay đổi |
| Observer (subscriber) | Interface có phương thức update, thường chỉ là một hàm callback |
| Concrete subject | Giữ trạng thái đáng quan tâm và gọi notify khi nó thay đổi |
| Concrete observer | Phản ứng với thông báo, ví dụ vẽ lại một phần giao diện |
Có hai kiểu thông báo. Ở mô hình push, subject gửi dữ liệu đã thay đổi cùng với thông báo. Ở mô hình pull, nó chỉ nói “có gì đó đã thay đổi” và observer tự hỏi subject dữ liệu họ cần.
Cài đặt bằng TypeScript
Một lớp Subject generic nhỏ xử lý việc đăng ký. Observer là các hàm thông thường, dạng phổ biến nhất trong TypeScript. subscribe trả về một hàm để hủy đăng ký, nên nơi gọi không thể quên listener nào mình đã đăng ký.
type Listener<T> = (value: T) => void;
class Subject<T> {
private listeners = new Set<Listener<T>>();
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
protected notify(value: T) {
for (const listener of [...this.listeners]) listener(value);
}
}
type CartEvent = { count: number; total: number };
class Cart extends Subject<CartEvent> {
private items: { name: string; price: number }[] = [];
add(name: string, price: number) {
this.items.push({ name, price });
const total = this.items.reduce((sum, item) => sum + item.price, 0);
this.notify({ count: this.items.length, total });
}
}
const cart = new Cart();
cart.subscribe(({ count }) => console.log("header: " + count + " items"));
const stopAnalytics = cart.subscribe(({ total }) => console.log("analytics: total " + total));
cart.add("Book", 12);
stopAnalytics(); // unsubscribe
cart.add("Pen", 3);
// header: 1 items
// analytics: total 12
// header: 2 itemsnotify lặp trên một bản sao của tập hợp ([...this.listeners]). Nhờ vậy một listener tự hủy đăng ký trong lúc thông báo sẽ không làm rối vòng lặp.
Dùng EventTarget có sẵn
Trình duyệt, Node.js và Deno đều cung cấp EventTarget, nên bạn thường không cần lớp subject riêng:
const thermostat = new EventTarget();
thermostat.addEventListener("change", (e) => {
console.log("new temperature:", (e as CustomEvent<number>).detail);
});
thermostat.dispatchEvent(new CustomEvent("change", { detail: 21.5 }));
// new temperature: 21.5Node.js cũng có lớp EventEmitter trong module node:events, với các phương thức on, off và emit.
Khi nào nên dùng và khi nào không
- Dùng khi thay đổi ở một đối tượng phải cập nhật các đối tượng khác, và bạn không biết trước có bao nhiêu hay cái nào.
- Dùng để giữ một module lõi độc lập với mã giao diện, ghi log hoặc phân tích phản ứng với nó.
- Dùng cho các sự kiện xảy ra theo thời gian, như đầu vào người dùng, thông điệp hoặc số đọc cảm biến.
- Đừng dùng khi chỉ có đúng một bên nhận cố định; gọi phương thức trực tiếp hoặc tham số callback dễ theo dõi hơn.
- Cẩn thận với chuỗi observer kích hoạt các thông báo khác. Chúng có thể gây vòng lặp cập nhật và khiến khó thấy vì sao một thứ thay đổi.
- Đừng dựa vào thứ tự các observer được gọi, trừ khi subject ghi rõ điều đó.
Ứng dụng thực tế
- DOM:
element.addEventListener("click", handler)đăng ký một observer vào một phần tử. - Node.js:
EventEmitterlà nền tảng của stream, máy chủ HTTP và nhiều đối tượng lõi khác. - Các API observer của trình duyệt như
MutationObserver,IntersectionObservervàResizeObserverthông báo cho callback về các thay đổi của trang. - RxJS mô hình hóa luồng giá trị thành các observable mà observer đăng ký theo dõi.
- Java:
java.util.Observableđã bị đánh dấu deprecated từ Java 9; thay vào đó dùng các interfacejava.util.concurrent.Flow(publisher và subscriber) và các interface event listener.
Các mẫu liên quan
| Mẫu | Khác với Observer ở điểm nào |
|---|---|
| Mediator | Mediator biết các đối tượng tham gia và điều phối chúng; subject chỉ phát và không biết observer làm gì. |
| Publish-subscribe với broker | Publisher và subscriber hoàn toàn không biết nhau và giao tiếp qua message broker hoặc event bus, thường giữa các tiến trình. |
| Command | Một thông báo có thể mang theo một đối tượng command; Command nói về một yêu cầu, Observer nói về thông báo một-nhiều. |
| Iterator | Iterator dựa trên pull (bên tiêu thụ hỏi giá trị kế tiếp); Observer dựa trên push (bên sản xuất gửi giá trị khi chúng xảy ra). |
Câu hỏi thường gặp
Observer khác publish-subscribe ở điểm nào?
Trong mẫu Observer cổ điển, observer đăng ký trực tiếp với subject. Trong publish-subscribe, một broker hoặc kênh sự kiện nằm giữa chúng, nên publisher và subscriber không biết nhau. Nhiều người dùng hai tên này khá lẫn lộn cho cùng một ý tưởng.
Làm sao tránh rò rỉ bộ nhớ với mẫu Observer?
Hủy đăng ký observer khi không còn cần. Trả về một hàm hủy đăng ký từ subscribe, hoặc dùng AbortSignal với addEventListener, giúp dọn dẹp đơn giản.
Observer có được gọi đồng bộ không?
Trong ví dụ trên và trong EventTarget và EventEmitter, các listener chạy đồng bộ theo thứ tự được thêm. Các thư viện khác có thể gửi thông báo bất đồng bộ, nên hãy xem tài liệu của chúng.
Observer có giống lập trình reactive không?
Lập trình reactive xây dựng trên Observer. Các thư viện như RxJS thêm các toán tử để lọc, kết hợp và biến đổi luồng thông báo, cùng quy tắc cho lỗi và hoàn tất.