行为型模式——观察者(Observer)
问题背景
当需要用一种通知机制代替轮询时,考虑使用观察者。在一些游戏中,策划出于某种目的会把玩家的属性划分成一级属性(力量、智力、体质等)和二级属性(生命、魔法、攻击力、防御力等)。在其他因素(装备、修炼、BUFF等)不变的条件下,二级属性是一级属性的函数。因此,在一级属性发生变化时,需要立刻更新二级属性的值。要实现这一功能,最粗暴的做法就是轮询,每帧重新计算一次二级属性。但这显然是不可能的,游戏的一帧只有10毫秒左右,可没时间给你算这玩意。
解决方案
为了避免轮询产生大量不必要的计算,我们采用另一种策略:Don’t call me, I’ll call you。也就是说,不要让二级属性一直去访问一级属性,而是让一级属性发生变化时通知二级属性。这样,就能大幅降低系统的开销。
提取两个接口:IObserver和IObservable,分别表示观察者和被观察者。IObserver有一个Update方法,表示在被通知时执行的操作。IObservable有两个方法:Subscribe、Unsubscribe,分别表示订阅和注销。书中的被观察者接口还有一个Notify方法,表示通知订阅者发生了变化。把这个方法放到接口中就意味着外界也可以触发通知操作,说实话我并不赞同这种设计,C#也是(参见C#的事件机制)。
观察者如果想接收被观察者的通知,就调用它的Subscribe方法进行注册。这样,每当被观察者发生变化时,观察者就会被通知,并有机会执行一些操作。当观察者不想再订阅被观察者时,就调用它的Unsubscribe方法进行注销。被观察者必须保证一点:发生变化时一定会通知观察者。
使用观察者后的程序结构如下:
效果
- 降低了观察者和被观察者的耦合度。
- 支持广播通信。
- 与轮询相比降低了系统开销。
缺陷
观察者模式维护难度大,观察者的详情无法在编译期确定,每个观察者都不了解其他观察者的情况,它的一次看似无害的更新可能导致其他观察者出错,这种错误通常很难定位。
相关模式
- 中介:可以用观察者实现中介。
- 单例:可以用单例实现一个全局的事件泵。
实现
using System;
using System.Collections.Generic;
namespace Observer
{
class Client
{
public interface IObserver
{
void Update();
}
public interface IObservable
{
void Subscribe(IObserver observer);
void Unsubscribe(IObserver observer);
}
public class Primary : IObservable
{
private List<IObserver> observers = new List<IObserver>();
private int str;
private int @int;
private int con;
public int STR
{
get => str;
set { str = value; Notify(); }
}
public int INT
{
get => @int;
set { @int = value; Notify(); }
}
public int CON
{
get => con;
set { con = value; Notify(); }
}
private void Notify()
{
Console.WriteLine($"一级属性发生变化: [STR: {str}, INT: {@int}, CON: {con}]");
observers.ForEach((item) => item.Update());
}
public void Subscribe(IObserver observer)
{
observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
observers.Remove(observer);
}
}
public class Secondary : IObserver
{
private Primary primary;
public int HP { get; set; }
public int MP { get; set; }
public int ATK { get; set; }
public int DEF { get; set; }
public Secondary(Primary primary)
{
this.primary = primary;
}
public void Update()
{
HP = 5 * primary.STR + 10 * primary.CON;
MP = 15 * primary.INT;
ATK = 5 * primary.STR;
DEF = 5 * primary.CON;
Console.WriteLine($"二级属性: [HP: {HP}, MP: {MP}, ATK: {ATK}, DEF: {DEF}]");
}
}
public class Character
{
public Primary Primary { get; }
public Secondary Secondary { get; }
public Character()
{
Primary = new Primary();
Secondary = new Secondary(Primary);
}
}
static void Main(string[] args)
{
var character = new Character();
character.Primary.Subscribe(character.Secondary);
character.Primary.STR = 10;
character.Primary.INT = 10;
character.Primary.CON = 10;
character.Primary.STR = 20;
character.Primary.CON = 45;
}
}
}