面向对象编程是对现实世界的抽象,是通过代码构件一个虚拟世界。在虚拟世界中有人、有物,也有很多关系到“人”和“物”之间的事情和业务。

以下我通过一个现实世界中的简单案例,来介绍面向对象编程的一种设计模式——观察者模式,也经常被称为“发布-订阅模式”。

现实中的事情经常具有复杂性和相关性,即一件事情发生过后往往引发其他相关联的事情。以网购下单为例:我们在网上商城点击按钮支付订单之后,商城系统的后端会同步通知仓库管理员打包物品发货、记录买家用户的消费日志、给买家用户发送短信提醒等一系列操作。这个看似简单的功能,可以拆分成为4个不同的模块:

1.订单模块,用于在支付后更改订单的支付状态

2.仓管模块,用于通知仓库管理员打包物品发货

3.日志模块,用于记录买家用户的消费日志

4.短信模块,用于给买家用户发送动账消费情况

先不采用观察者模式,代码如下:

【例一】

public void Pay(Order order)
{
    // TODO: 省略支付的具体业务代码

    // 1. 订单模块,更改订单的支付状态
    order.IsPaid = true;        // 修改订单为:已支付
    OrderService.Save(order);   // 保存订单信息到数据库

    // 2. 仓管模块,仓管系统内通知仓库管理员发货
    StockSystemApi.MessageService.Add($"收到新订单[{order.Number}],请留意查看订单详情进行发货操作");

    // 3. 日志模块,日志记录订单支付状态变更
    LogService.Add(order.Buyer.Id, $"订单[{order.Number}]已完成支付,消费总额:[{order.TotalAmout}]");

    // 4. 短信模块,短信通知买家动账消费情况
    SmsService.Send(order.Buyer.PhoneNumber, $"亲,您的订单[{order.Number}]已完成支付,本单消费金额:[{order.TotalAmout}],感谢亲的光顾~");
}

以上代码在功能实现上是没有任何问题的,并且实现了4个不同模块的封装。但这段代码的弊端就在于:整个业务被封装在同一个函数体内(Pay函数),子业务之间的耦合性太高,若业务需要拆分变动,改动的代价比较大。所以针对这种耦合性很高,需要解耦的业务,可以采用观察者模式(发布-订阅模式)。


使用观察者模式(发布-订阅模式)如下:

【例二】

一、先在业务对象中抽象出两个接口:

1.被观察者(发布者)

// 被观察者(发布者)接口
public interface ISubject
{
    // 添加订阅的观察者
    public void AddObserver(IObserver observer);
    // 移除订阅的观察者
    public void RemoveObserver(IObserver observer);
    // 通知已订阅的观察者
    public void NotifyObserver(Order order);
}

2.观察者(订阅者)

// 观察者(订阅者)接口
public interface IObserver
{
    void PayOrder(Order order);
}


二、在订单的业务类中实现ISubject接口,使订单业务类成为一个被观察者(发布者),并在订单业务类中建立一个观察者(订阅者)清单,用于在订单支付完成之后逐个通知

// 订单服务器类,实现“发布者接口”的各个函数
public class OrderService : ISubject
{
    // 观察者(订阅者)清单
    private List<IObserver> Observers = new List<IObserver>();

    public void AddObserver(IObserver observer)
    {
        Observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        Observers.Remove(observer);
    }

    // 通知已订阅的观察者
    public void NotifyObserver(Order order)
    {
        foreach(var observer in Observers)
        {
            observer.PayOrder(order);
        }
    }

    public void Pay(Order order)
    {
        order.IsPaid = true;         // 修改订单为:已支付
        OrderService.Save(order);    // 保存订单信息到数据库

        NotifyObserver(order);
    }
}


三、按业务模块建立:仓管观察、日志观察、短信观察3个观察者类,并在这些观察者类中实现IObserver接口

// 仓管系统的通知信息类,实现“订阅者接口”
public class StockMessageObserver : IObserver
{
    public void PayOrder(Order order)
    {
        // 仓管系统内通知仓库管理员发货
        StockSystemApi.MessageService.Add($"收到新订单[{order.Number}],请留意查看订单详情进行发货操作");
    }
}

// 日志类,实现“订阅者接口”
public class LogObserver : IObserver
{
    public void PayOrder(Order order)
    {
        // 站内信件记录订单支付状态变更
        LogService.Add(order.Buyer.Id, $"订单[{order.Number}]已完成支付,消费总额:[{order.TotalAmout}]");
    }
}

// 短信通知类,实现“订阅者接口”
public class SmsObserver : IObserver
{
    public void PayOrder(Order order)
    {
        // 短信通知买家动账消费情况
        SmsService.Send(order.Buyer.PhoneNumber, $"亲,您的订单[{order.Number}]已完成支付,本单消费金额:[{order.TotalAmout}],感谢亲的光顾~");
    }
}


四、在调用Pay函数之前,往订单业务类中添加观察者(订阅者),实现在订单支付完成之后,由被观察者(OrderService)遍历其内部的Observers,逐个给观察者发布通知,引发观察者执行对应的行动。

// 发布者(被观察者)实例
Observer.OrderService orderService = new Observer.OrderService();
// 为 发布者 添加 订阅者(仓管系统)
orderService.AddObserver(new Observer.StockMessageObserver());
// 为 发布者 添加 订阅者(日志)
orderService.AddObserver(new Observer.LogObserver());
// 为 发布者 添加 订阅者(短信通知)
orderService.AddObserver(new Observer.SmsObserver());

orderService.Pay(order);


如上示例,观察者最大的优势在于解耦各模块独立封装,需要时按需执行,如【例二】中的第四步,如果某虚拟订单不需要通知仓管发货,则不添加仓管观察者即可;或某订单全额免单不需要短信通知用户动账消费情况,则不添加短信观察者即可。

其使用场景多用于一个对象的状态更新时,同步更新其他对象,如前文举例中所示:订单完成支付后,其支付状态变更了,需要同步通知仓管、记录日志、通知买家。

附件为Demo(运行环境:.net 5)ObserverPatternDemo.zip 


文末思考:

面向对象编程中的观察者模式(发布-订阅模式),其运行逻辑有些类似Javascript函数式编程中的函数回调:在函数体执行完成之后执行指定的其他函数。在面向对象语言中的“事件”和“委托”中也有这种执行逻辑。那么观察者模式和事件+委托的区别是什么?在这两者之间应该如何选择?