CodeGym /Các khóa học /JAVA 25 SELF /Mẫu thiết kế Observer (người quan sát)

Mẫu thiết kế Observer (người quan sát)

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Làm quen với mẫu thiết kế “Observer”

Mẫu “Observer” (người quan sát) là một trong những mẫu thiết kế nổi tiếng và nền tảng nhất. Nó mô tả tình huống khi một đối tượng (được quan sát, hay subject) thông báo về các thay đổi của mình cho các đối tượng khác (observer) đã đăng ký theo dõi các thay đổi đó.

Nói đơn giản: chúng ta có một kênh Telegram (đối tượng được quan sát) và có “người theo dõi” (observer). Mỗi lần có bài đăng mới, kênh sẽ thông báo cho tất cả người theo dõi, và họ sẽ quyết định làm gì — đọc, bỏ qua hoặc hủy theo dõi.

Trong lập trình, mẫu này cho phép tự động thông báo cho các đối tượng quan tâm về các sự kiện hoặc thay đổi trạng thái mà không liên kết trực tiếp chúng với nhau. Điều này quan trọng để xây dựng các hệ thống linh hoạt, có khả năng mở rộng và dễ bảo trì.

Mẫu “Observer” xuất hiện ở đâu?

  • Trong giao diện đồ họa (Swing, AWT, JavaFX) — các listener sự kiện.
  • Trong các thư viện reactive (RxJava, Project Reactor).
  • Trong business logic: phản ứng khi trạng thái của mô hình thay đổi.
  • Trong engine trò chơi (sự kiện va chạm, thắng, thua, v.v.).
  • Bất cứ nơi nào cần tách “điều gì đã xảy ra” khỏi “xử lý thế nào”.

Mối liên hệ giữa mẫu và sự kiện/listener trong Java

Thực tế, toàn bộ mô hình sự kiện của Java được xây dựng dựa trên “Observer”. Khi bạn viết button.addActionListener(listener);, bạn đang áp dụng mẫu này:

  • Đối tượng được quan sát — nút (hoặc thành phần khác).
  • Observer — listener của bạn, hiện thực phương thức actionPerformed().
  • Sự kiện — người dùng nhấp, rê chuột, v.v.
  • Thông báo — thành phần gọi actionPerformed().

Tất cả điều đó là hiện thực Observer cổ điển!

2. Hiện thực mẫu “Observer” theo cách cổ điển

Hãy xem cách hiện thực mẫu này trên các lớp của chính bạn — không dùng Swing hay AWT — để thấy rằng không có “ma thuật” nào.

Các thành phần chính của mẫu

  • Observable (Subject) — đối tượng được quan sát. Lưu danh sách observer và thông báo cho họ khi có thay đổi.
  • Observer — giao diện observer, thường có phương thức update().

Ví dụ: Nhiệt kế và điều hòa

Giao diện observer

public interface TemperatureObserver {
    void temperatureChanged(int newTemperature);
}

Lớp “Thermometer” (được quan sát)

import java.util.*;

public class Thermometer {
    private int temperature;
    private final List<TemperatureObserver> observers = new ArrayList<>();

    public void addObserver(TemperatureObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(TemperatureObserver observer) {
        observers.remove(observer);
    }

    public void setTemperature(int newTemperature) {
        if (this.temperature != newTemperature) {
            this.temperature = newTemperature;
            notifyObservers();
        }
    }

    private void notifyObservers() {
        for (TemperatureObserver observer : observers) {
            observer.temperatureChanged(temperature);
        }
    }
}

Ví dụ observer — “AirConditioner”

public class AirConditioner implements TemperatureObserver {
    @Override
    public void temperatureChanged(int newTemperature) {
        if (newTemperature > 25) {
            System.out.println("Điều hòa đã bật! Nóng quá: " + newTemperature + "°C");
        } else {
            System.out.println("Điều hòa tắt. Nhiệt độ: " + newTemperature + "°C");
        }
    }
}

Sử dụng

public class Main {
    public static void main(String[] args) {
        Thermometer thermometer = new Thermometer();
        AirConditioner conditioner = new AirConditioner();

        thermometer.addObserver(conditioner);

        thermometer.setTemperature(22); // Điều hòa tắt. Nhiệt độ: 22°C
        thermometer.setTemperature(28); // Điều hòa đã bật! Nóng quá: 28°C
    }
}

Đó là tất cả “ma thuật”! Bạn có thể thêm cả trăm observer — tất cả sẽ nhận được thông báo khi nhiệt độ thay đổi.

Sơ đồ minh họa của mẫu

flowchart LR
    T["Nhiệt kế (Observable)"] -- thông báo --> AC["Điều hòa (Observer)"]
    T -- thông báo --> L["Logger (Observer)"]
    T -- thông báo --> Alarm["Báo động (Observer)"]

Chi tiết hiện đại: Observable đã lỗi thời và các cách tiếp cận mới

Trong thư viện chuẩn Java từng có java.util.Observablejava.util.Observer, nhưng từ Java 9 chúng được đánh dấu là lỗi thời (deprecated). Lý do — thiếu linh hoạt (ví dụ, Observable là một lớp chứ không phải interface, khiến việc kế thừa từ lớp khác trở nên khó khăn).

Cách tiếp cận hiện đại — thiết kế các interface listener riêng và logic đăng ký/hủy đăng ký (như trong ví dụ trên). Cách này linh hoạt hơn, an toàn hơn và phù hợp với nhu cầu thực tế hơn.

3. Ví dụ: Ứng dụng mini với người đăng ký

Tạo một “bộ đếm lần nhấn” với khả năng đăng ký nhận thay đổi giá trị.

Giao diện listener

public interface CounterListener {
    void counterChanged(int newValue);
}

Lớp bộ đếm

import java.util.*;

public class Counter {
    private int value = 0;
    private final List<CounterListener> listeners = new ArrayList<>();

    public void addCounterListener(CounterListener l) {
        listeners.add(l);
    }

    public void removeCounterListener(CounterListener l) {
        listeners.remove(l);
    }

    public void increment() {
        value++;
        notifyListeners();
    }

    private void notifyListeners() {
        for (CounterListener l : listeners) {
            l.counterChanged(value);
        }
    }

    public int getValue() {
        return value;
    }
}

Listener: in thông báo

public class ConsoleCounterListener implements CounterListener {
    @Override
    public void counterChanged(int newValue) {
        System.out.println("Bộ đếm đã thay đổi: " + newValue);
    }
}

Sử dụng

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.addCounterListener(new ConsoleCounterListener());

        counter.increment(); // Bộ đếm đã thay đổi: 1
        counter.increment(); // Bộ đếm đã thay đổi: 2
    }
}

4. Những điểm hữu ích

Các lựa chọn thay thế và mở rộng hiện đại

Trong các dự án thực tế, người ta thường dùng lớp ẩn danh hoặc biểu thức lambda để đăng ký: counter.addCounterListener(newValue -> System.out.println("Giá trị mới: " + newValue));

(Để làm được như vậy, interface phải là functional — có duy nhất một phương thức trừu tượng.)

Các thư viện reactive (RxJava, Project Reactor) cũng rất phổ biến, nơi “Observer” được hiện thực kèm hỗ trợ luồng sự kiện, lọc, bất đồng bộ, v.v. Để hiểu bản chất, chỉ cần sơ đồ cổ điển đã phân tích ở trên là đủ.

Ứng dụng của mẫu “Observer” trong thực tế

  • Mô hình dữ liệu. Thay đổi mô hình (danh sách nhiệm vụ, sản phẩm, người dùng) sẽ thông báo cho các view để cập nhật.
  • Ghi log. Logger đăng ký phản ứng với sự kiện trên toàn hệ thống.
  • Thông báo. Khi trạng thái thay đổi — gửi email, push notification, tin nhắn Telegram.
  • Trò chơi. Thay đổi máu, xuất hiện kẻ địch, hoàn thành màn.
  • Đa luồng. Một luồng phát sự kiện, các luồng khác phản ứng.

5. Lỗi thường gặp khi hiện thực mẫu “Observer”

Lỗi số 1: Quên xóa listener. Nếu listener không còn cần thiết nhưng không bị loại bỏ, nó vẫn tiếp tục nhận thông báo. Trong các ứng dụng sống lâu, điều này có thể dẫn đến rò rỉ bộ nhớ.

Lỗi số 2: Thao tác dài hoặc chặn trong handler. Nếu handler thực hiện công việc nặng (I/O, DB), ứng dụng có thể “đơ”, đặc biệt nếu thông báo xuất phát từ luồng UI. Hãy chuyển các tác vụ nặng sang các luồng nền.

Lỗi số 3: Exception trong listener. Exception ở một listener có thể làm gián đoạn việc gửi cho những listener còn lại. Hãy bao các lời gọi listener trong try-catch và ghi log lỗi.

Lỗi số 4: Đăng ký cùng một listener nhiều lần. Nếu một listener được thêm nhiều lần, nó sẽ nhận sự kiện trùng lặp số lần tương ứng. Hãy theo dõi việc đăng ký và áp dụng bảo vệ chống thêm trùng.

Lỗi số 5: Liên kết chặt giữa đối tượng được quan sát và observer. Nếu đối tượng được quan sát biết các hiện thực cụ thể của observer, tính liên kết lỏng bị phá vỡ. Chỉ sử dụng các interface (ví dụ, TemperatureObserver, CounterListener).

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION