设计模式的征途—15.观察者(Observer)模式
在日常生活中,交通信号灯指挥者日益拥挤的城市交通。红灯亮,汽车停止;绿灯亮,汽车继续前行;在这个过程中,交通信号灯是汽车的观察目标,而汽车则是观察者。随着交通信号灯的变化,汽车的行为也会随之变化,一盏交通信号灯可以指挥多辆汽车。在软件系统中,有些对象之间也存在类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将会导致其他对象的状态或者行为也发生改变,它们之间将产生联动,正所谓牵一发而动全身。为了更好地描述对象之间存在的这种一对多的联动,观察者模式应运而生。
观察者模式(Chain of Responsibility) | 学习难度:★★★☆☆ | 使用频率:★★★★★ |
一、多人联机对战游戏的设计
需求背景:M公司欲开发一款多人联机对战游戏,在游戏中,多个游戏玩家可以加入同一战队组成联盟,当战队中某一成员收到敌人攻击时将给所有其他盟友发送通知,盟友收到通知后将作出响应。M公司开发人员需要提供一个设计方案来实现战队成员之间的联动。
M公司开发人员通过分析,发现在该系统中战队成员之间的联动过程可以简单描述如下:
联盟成员收到攻击 => 发送通知给盟友 => 盟友作出响应
如果每个联盟成员都需要持有盟友的信息才能及时通知每一位盟友,因此这样系统开销较大。因此,M公司开发人员决定引入一个新角色“战队控制中心”来负责维护和管理每个战队所有成员的信息,如下图所示:
如何实现对象之间的联动?让我们来看看观察者模式吧。
二、观察者模式概述
2.1 观察者模式简介
观察者模式是一种使用频率最高的设计模式之一,用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。
观察者(Observer)模式:定义对象之间的一种一对多依赖关系,使得当每一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
2.2 观察者模式结构
观察者模式包含以下4个角色:
(1)Subject(抽象目标):又称为主题,是被观察的对象。
(2)ConcreteSubject(具体目标):抽象目标的子类,通常包含有经常发生改变的数据,当它的状态发生改变时,向其各个观察者发出通知。
(3)Observer(抽象观察者):观察者将对观察目标的改变做出反应。
(4)ConcreteObserver(具体观察者):具体观察者中维持一个指向具体目标对象的引用,它用于存储具体观察者的有关状态,这些状态需要和具体目标地状态保持一致。
三、重构多人联机对战游戏
3.1 重构后的设计结构
其中,AllyControlCenter充当抽象目标,ConcreteAllyControlCenter则充当具体目标,IObserver充当抽象观察者,Player则充当具体观察者。
3.2 具体代码实现
(1)抽象观察者:IObserver
/// <summary> /// 抽象观察类 - IObserver接口 /// </summary> public interface IObserver { string Name { get; set; } void Help(); // 声明支援盟友的方法 void BeAttacked(AllyControlCenter acc); // 声明遭受攻击的方法 }
(2)具体观察者:Player
/// <summary> /// 具体观察者类:战队成员 /// </summary> public class Player : IObserver { public string Name { get; set; } public void BeAttacked(AllyControlCenter acc) { Console.WriteLine("{0}:我正被攻击,速来援救!", this.Name); // 调用战队控制中心类的通知方法来通知盟友 acc.NotifyObserver(this.Name); } public void Help() { Console.WriteLine("{0} :坚持住,立马来救你!", this.Name); } }
(3)抽象目标:AllyControlCenter
/// <summary> /// 抽象目标类:战队控制中心 /// </summary> public abstract class AllyControlCenter { public string AllyName { get; set; } protected IList<IObserver> playerList = new List<IObserver>(); public void Join(IObserver observer) { playerList.Add(observer); Console.WriteLine("通知:{0} 加入 {1} 战队", observer.Name, this.AllyName); } public void Quit(IObserver observer) { playerList.Remove(observer); Console.WriteLine("通知:{0} 退出 {1} 战队", observer.Name, this.AllyName); } // 声明抽象通知方法 public abstract void NotifyObserver(string name); }
(4)具体目标:ConcreteAllyControlCenter
public class ConcreteAllyControlCenter : AllyControlCenter { public ConcreteAllyControlCenter(string allyName) { Console.WriteLine("系统通知:{0} 战队组建成功!", this.AllyName); Console.WriteLine("-------------------------------------------------------"); this.AllyName = allyName; } // 实现通知方法 public override void NotifyObserver(string playerName) { Console.WriteLine("通知:盟友们,{0} 正遭受敌军攻击,速去抢救!", playerName); foreach (var player in playerList) { if (!player.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)) { player.Help(); } } } }
(5)客户端测试
public class Program { public static void Main(string[] args) { // Step1.定义观察者对象 AllyControlCenter acc = new ConcreteAllyControlCenter("金庸群侠"); // Step2.定义4个观察者对象 IObserver playerA = new Player() { Name = "杨过" }; acc.Join(playerA); IObserver playerB = new Player() { Name = "令狐冲" }; acc.Join(playerB); IObserver playerC = new Player() { Name = "张无忌" }; acc.Join(playerC); IObserver playerD = new Player() { Name = "段誉" }; acc.Join(playerD); // Step3.当某盟友遭受攻击 playerA.BeAttacked(acc); Console.ReadKey(); } }
编译调试结果如下图所示:
在本实例中,实现了两次对象之间的联动:Player.BeAttacked() => AllyControlCenter.NotifyObserver() => Player.Help()
四、观察者模式与MVC
在当前流行的MVC(Model-View-Controller)结构中也应用了观察者模式,它包含了3个角色:模型、视图和控制器。其中,模型可对应观察者模式中的观察目标,而视图则对应于观察者,控制器充当二者之间的中介者。当模型层的数据发生改变时,视图将会发出数据请求以期改变其显示内容,如下图所示:
五、观察者模式总结
5.1 主要优点
(1)可以实现表示层和数据逻辑层的分离 => 各种不同的表示层可以充当具体观察者
(2)支持广播通信,观察目标会向已注册的观察者对象发送通知 => 简化一对多系统设计的难度
(3)增加新的观察者无须修改原有系统代码 => 满足开闭原则
5.2 主要缺点
(1)如果一个观察目标有很多直接和间接的观察者 => 所有观察者收到通知会花费大量时间
(2)如果观察者和观察目标之间存在循环依赖 => 可能导致系统崩溃
5.3 应用场景
(1)一个抽象模型有两个方面,其中一个方面依赖于另一个方面 => 封装起来使其独立改变和复用
(2)一个对象的改变将导致一个或多个其他对象也发生改变,但并不知道具体有多少个对象将要发生改变 => 最熟悉的陌生人
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》