CodeGym /课程 /C# SELF /模式 "观察者" ( Observer)...

模式 "观察者" ( Observer)

C# SELF
第 53 级 , 课程 3
可用

1. 介绍

想象下,你有个对象在做某些事 —— 比如一个按钮,或者我们的 Worker。同时有很多其他对象需要对这些动作作出反应。如果你在 Worker 里把所有可能的“监听者”硬编码进去,维护这种代码就会变成噩梦:订阅者名单的任何变化都会要求你改动 Worker 本身。

这会违反开闭原则(OCP),在架构上是个糟糕的做法。

Observer 模式:总体思路

"观察者"(Observer)模式解决了这个问题。它允许发布者对象通知任意数量的感兴趣的监听对象关于发生的变化,而无需知道这些监听者是谁、在做什么。发布者只是“发个信号”,想响应的人按自己的方式响应。

类比:订阅新闻推送。编辑部或频道(发布者)发一封新邮件,所有订阅者(观察者)都会收到。编辑部不需要也不知道这些人是谁。

有趣的事实:“观察者”非常常见,以至于它是“GoF”四人帮设计模式之一。

Observer 在 C#:用事件和委托实现

在 C# 里,观察者模式通过事件和委托被“开箱即用”地实现。事件就是一个扩展点,允许不同的处理器订阅。相比手动维护订阅者列表,语言层面的事件机制会替你处理这些事。下面先看「手动」实现,然后看基于事件的实现。

2. 经典的 Observer 实现(不使用事件)

假设语言里没有事件,代码可能长这样:


// 观察者接口
public interface IObserver
{
    void Update(string message);
}

// 发布者
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 正在工作...");
        NotifyObservers("工作完成了!");
    }

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

// 具体观察者
public class WorkListener : IObserver
{
    public void Update(string message)
    {
        Console.WriteLine($"
WorkListener 收到消息: {message}");
    }
}

初始化:


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

提示:这里订阅者列表(List<IObserver> observers)是手动维护的,订阅/退订是显式的方法 Subscribe/Unsubscribe

3. 事件和委托 —— C# 风格的 Observer(更高级)

我们可以用事件把同样的事做得更简洁、更优雅。这就是 C# 里的 Observer:


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

    public void DoWork()
    {
        Console.WriteLine("Worker 正在工作...");
        OnWorkCompleted("工作完成了!");
    }

    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 收到消息: {e.Message}");
    }
}

// 订阅:
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();

这种方式的优点:

  • 不用手动维护订阅者列表。
  • 事件提供了所有功能:多重订阅、退订、lambda。
  • 安全性有保障:只有发布者可以触发事件。
  • 低耦合:发布者不知道监听者的存在。

4. 观察者如何融入我们的应用

Observer 模式整合到控制台应用里。让 Worker 有任意数量的处理器,它们对完成事件有不同反应:有人写控制台日志,有人统计已完成的工作数,有人发条消息 "老板!都做完了!"

用例子扩展代码


// 第二个监听器:计数器
public class WorkCounter
{
    public int Count { get; private set; }

    public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
    {
        Count++;
        Console.WriteLine($"工作已计入。总计: {Count} 已完成。");
    }
}

// 创建对象
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();

// 两个订阅
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;

// 模拟多次工作
worker.DoWork();
worker.DoWork();
// Output:
// Worker 正在工作...
// WorkListener 收到消息: 工作完成了!
// 工作已计入。总计: 1 已完成。
// Worker 正在工作...
// WorkListener 收到消息: 工作完成了!
// 工作已计入。总计: 2 已完成。

这样你可以按需添加“观察者”,而不改动 Worker 的任何一行。Worker 保持不变,系统的行为通过订阅者来扩展。

5. 有用的注意事项

真实例子:在接口和 GUI 中的 Observer

Observer 模式是所有 GUI 框架的基础。在 Windows Forms 或 WPF 中,按钮点击会触发 Click 事件。你写处理器(观察者)来响应这个事件 —— 你的 Button 类或 .NET 库根本不需要知道你的订阅者是谁。


// 在 WPF 或 WinForms 中(大体)
myButton.Click += (s, e) => MessageBox.Show("用户点击了按钮!");

Observer 在真实项目里的用途

  • 用户界面(响应点击、变化、定时器等)。
  • 通知和事件系统。
  • 可扩展系统的插件(核心生成事件,扩展订阅)。
  • 分布式系统和游戏引擎(松耦合的反应链)。

总之,如果你需要做一个可扩展的系统,让一个部分对另一个部分的变化做出反应 —— Observer must have!

7. 特点、常见错误及其预防

使用 Observer 时的潜在问题

内存泄漏。如果订阅者订阅了事件但没有退订(尤其是订阅者寿命短而发布者寿命长时),垃圾回收器无法回收该对象,因为发布者通过事件的 delegate 还持有对它的引用。这会很严重,尤其是在订阅者已经不需要但发布者仍然活着的情况下。

重复订阅。如果同一个处理器被订阅了两次,它会被调用两次 —— 会导致动作重复和意外副作用。

处理器内的异常。如果某个处理器抛出异常,后面的订阅者可能会被阻断。考虑让处理器更健壮,必要时在触发时用 try-catch 包裹每个调用,以便即使一个失败,其他的也能执行。

常见的泄漏示意图

flowchart LR
    Publisher["发布者
(Worker)"] -- 事件 --> ObserverA["监听者 A (活着!)"] Publisher -- 事件 --> ObserverB["监听者 B (泄漏:忘记退订)"]
2
任务
C# SELF, 第 53 级, 课程 3
已锁定
使用事件实现观察者模式
使用事件实现观察者模式
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION