面向对象编程是对现实世界的抽象,是通过代码构件一个虚拟世界。在虚拟世界中有人、有物,也有很多关系到“人”和“物”之间的事情和业务。
以下我通过一个现实世界中的简单案例,来介绍面向对象编程的一种设计模式——观察者模式,也经常被称为“发布-订阅模式”。
现实中的事情经常具有复杂性和相关性,即一件事情发生过后往往引发其他相关联的事情。以网购下单为例:我们在网上商城点击按钮支付订单之后,商城系统的后端会同步通知仓库管理员打包物品发货、记录买家用户的消费日志、给买家用户发送短信提醒等一系列操作。这个看似简单的功能,可以拆分成为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函数式编程中的函数回调:在函数体执行完成之后执行指定的其他函数。在面向对象语言中的“事件”和“委托”中也有这种执行逻辑。那么观察者模式和事件+委托的区别是什么?在这两者之间应该如何选择?