浅谈观察者模式(发布-订阅模式)的应用

浅谈观察者模式(发布-订阅模式)的应用

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

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

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

  1. 订单模块,用于在支付后更改订单的支付状态
  2. 仓管模块,用于通知仓库管理员打包物品发货
  3. 日志模块,用于记录买家用户的消费日志
  4. 短信模块,用于给买家用户发送动账消费情况

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

【例一】

csharp
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.被观察者(发布者)

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

2.观察者(订阅者)

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

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

csharp
// 订单服务器类,实现“发布者接口”的各个函数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接口

csharp
// 仓管系统的通知信息类,实现“订阅者接口”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,逐个给观察者发布通知,引发观察者执行对应的行动。

csharp
// 发布者(被观察者)实例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


文末思考:

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

← 返回 Blog