CodeGym /Các khóa học /C# SELF /Mẫu "Observer" ( Observer

Mẫu "Observer" ( Observer)

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

1. Giới thiệu

Hãy tưởng tượng bạn có một object thực hiện một số hành động — ví dụ một nút bấm hoặc Worker của chúng ta. Đồng thời có nhiều object khác cần phản ứng khi những hành động đó xảy ra. Nếu trong lớp Worker cố định liệt kê tất cả "listener" có thể có, thì việc duy trì mã sẽ biến thành ác mộng: bất kỳ thay đổi nào trong danh sách subscribers sẽ đòi hỏi sửa mã bên trong chính Worker.

Điều này vi phạm nguyên tắc Open/Closed (OCP) và được coi là một thực hành kiến trúc tồi.

Mẫu Observer: ý tưởng chung

Mẫu "Observer" (Observer) giải quyết vấn đề này. Nó cho phép object publisher thông báo cho bất kỳ số lượng listeners quan tâm về các thay đổi, mà không cần biết ai là những listeners đó và họ làm gì. Publisher chỉ "gọi" thông báo, còn ai muốn phản ứng thì phản ứng.

Tương tự: đăng ký nhận bản tin. Ban biên tập hoặc kênh (publisher) gửi mail mới, và tất cả subscribers (observers) nhận được. Ban biên tập không biết có bao nhiêu người đó, và thực ra họ cũng không cần biết.

Thú vị: "Observer" phổ biến đến mức chính thức là một trong "bộ tứ" GoF (Gang of Four).

Observer trong C#: hiện thực qua events và delegates

Trong C# mẫu "Observer" được hiện thực "mặc định" thông qua cơ chế events và delegates. Sự kiện — là "điểm mở rộng" mà các handlers có thể subscribe vào. Thay vì tự quản lý danh sách subscribers, mọi thứ được ngôn ngữ hỗ trợ bằng events. Dưới đây xem implementation "thủ công" rồi mới đến implementation qua events.

2. Triển khai cổ điển của Observer nếu không có events

Xem cách trông như thế nào nếu ngôn ngữ không có events:


// Interface của observer
public interface IObserver
{
    void Update(string message);
}

// Publisher
public class Worker
{
    private List<IObserver> observers = new List<IObserver>();

    public void Subscribe(IObserver observer)
    {
        observers.Add(observer);
    }

    public void Unsubscribe(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void DoWork()
    {
        Console.WriteLine("Worker đang làm việc...");
        NotifyObservers("Công việc hoàn tất!");
    }

    private void NotifyObservers(string message)
    {
        foreach (var observer in observers)
        {
            observer.Update(message);
        }
    }
}

// Concrete observer
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"
WorkListener nhận được thông báo: {message}");
    }
}

Khởi tạo:


var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();

Ghi chú: Ở đây danh sách subscribers (List<IObserver> observers) được quản lý thủ công, và việc subscribe/unsubscribe là các phương thức rõ ràng Subscribe/Unsubscribe.

3. Events và delegates — hiện thực "cao cấp" của Observer

Ta có thể làm giống vậy đơn giản và tinh tế hơn bằng events. Đây chính là Observer theo phong cách C#:


public class Worker
{
    public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;

    public void DoWork()
    {
        Console.WriteLine("Worker đang làm việc...");
        OnWorkCompleted("Công việc hoàn tất!");
    }

    protected virtual void OnWorkCompleted(string message)
    {
        WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
    }
}

public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; set; }
}

public class WorkListener
{
    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"WorkListener nhận được thông báo: {e.Message}");
    }
}

// Đăng ký:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();

Ưu điểm của cách này:

  • Không cần quản lý danh sách subscribers thủ công.
  • Tận dụng được mọi khả năng của events: nhiều đăng ký, unsubscribe, lambda.
  • An toàn: chỉ publisher mới có thể gọi event.
  • Coupling thấp: publisher không biết gì về listeners.

4. Observer áp dụng vào ứng dụng của chúng ta

Hãy tích hợp mẫu Observer vào ứng dụng console. Giả sử Worker có nhiều handlers khác nhau phản ứng khi công việc hoàn tất: có người ghi ra console, có người đếm số công việc đã làm, có người gửi email "Sếp! Mọi thứ đã xong!".

Mở rộng code với ví dụ


// Listener thứ hai kiểu counter
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"Công việc đã được ghi nhận. Tổng: {Count} đã hoàn thành.");
    }
}

// Tạo các đối tượng
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// Đăng ký cả hai
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// Mô phỏng nhiều công việc
worker.DoWork();
worker.DoWork();
// Output:
// Worker đang làm việc...
// WorkListener nhận được thông báo: Công việc hoàn tất!
// Công việc đã được ghi nhận. Tổng: 1 đã hoàn thành.
// Worker đang làm việc...
// WorkListener nhận được thông báo: Công việc hoàn tất!
// Công việc đã được ghi nhận. Tổng: 2 đã hoàn thành.

Như vậy, bạn thêm "observers" khi cần mà không thay bất kỳ dòng nào trong mã của Worker. Lớp Worker giữ nguyên, còn hành vi hệ thống được mở rộng thông qua subscribers.

5. Những điểm cần lưu ý hữu ích

Ví dụ thực tế: Observer trong interfaces và GUI

Mẫu Observer là nền tảng của tất cả GUI frameworks. Trong Windows Forms hoặc WPF, nhấn nút kích hoạt event Click. Bạn viết handlers (observers) phản ứng với event đó — và lớp Button hay thư viện .NET không cần biết gì về subscribers của bạn.


// Trong WPF hoặc WinForms (khoảng)
myButton.Click += (s, e) => MessageBox.Show("Người dùng đã nhấn nút!");

Observer trong dự án thực tế

  • Giao diện người dùng (phản ứng với click, thay đổi, timers, v.v.).
  • Hệ thống thông báo và event.
  • Plugins cho hệ thống mở rộng (core phát event, extensions subscribe).
  • Hệ phân tán và game engines (các chuỗi phản ứng lỏng lẻo).

Nói ngắn gọn, nếu bạn cần một hệ thống có thể mở rộng, nơi các phần có thể phản ứng với thay đổi của phần khác — Observer must have!

7. Đặc điểm, lỗi điển hình và cách tránh

Khó khăn tiềm ẩn khi dùng Observer

Rò rỉ bộ nhớ. Nếu một subscriber đăng ký vào event nhưng không unsubscribe (đặc biệt với các object sống lâu), garbage collector sẽ không thu hồi được object đó vì publisher vẫn giữ tham chiếu tới nó qua delegate của event. Điều này nghiêm trọng nếu subscriber không còn cần nữa nhưng publisher vẫn sống.

Đăng ký nhiều lần. Nếu cùng một handler được đăng ký hai lần, nó sẽ được gọi hai lần — dẫn tới hành động bị trùng và hiệu ứng bất ngờ.

Exceptions trong handlers. Nếu một trong các handlers ném exception, việc gọi các subscribers tiếp theo có thể bị dừng. Hãy thiết kế handlers chịu lỗi, và khi cần, gọi thủ công trong try-catch để các subscriber khác vẫn được chạy ngay cả khi một cái bị lỗi.

Sơ đồ rò rỉ thường gặp

flowchart LR
    Publisher["Nhà xuất bản
(Worker)"] -- sự kiện --> ObserverA["Người nghe A (Còn sống!)"] Publisher -- sự kiện --> ObserverB["Người nghe B (Rò rỉ: quên hủy đăng ký)"]
2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Sử dụng sự kiện để hiện thực Observer
Sử dụng sự kiện để hiện thực Observer
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION