1.1 观察者模式定义
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。观察者模式是满足这一要求的各种设计方案中最重要的一种。
Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式, 其是一种松耦合的设计模式。
1.2 观察者模式的结构
观察者模式的类图如下:
可以看出,在这个观察者模式的实现里有下面这些角色:
抽象主题(Subject)角色:主题角色把所有对观察考对象的引用保存在一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,主题角色又叫做抽象被观察者(Observable)角色,一般用一个抽象类或者一个接口实现。
抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。这个接口叫做更新接口。抽象观察者角色一般用一个抽象类或者一个接口实现。在这个示意性的实现中,更新接口只包含一个方法(即Update()方法),这个方法叫做更新方法。
具体主题(ConcreteSubject)角色:将有关状态存入具体现察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者角色(Concrete Observable)。具体主题角色通常用一个具体子类实现。
具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体现察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体现察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体子类实现。
从具体主题角色指向抽象观察者角色的合成关系,代表具体主题对象可以有任意多个对抽象观察者对象的引用。之所以使用抽象观察者而不是具体观察者,意味着主题对象不需要知道引用了哪些ConcreteObserver类型,而只知道抽象Observer类型。这就使得具体主题对象可以动态地维护一系列的对观察者对象的引用,并在需要的时候调用每一个观察者共有的Update()方法。这种做法叫做"针对抽象编程"。
1.3 观察者代码
下面通过一个例子来说明Observer模式。监控某一个公司000002.SZ(深万科A)的股票价格变化,当价格发生变化的时候,通知的对象:投资者A,投资者B。炒股用户A与炒股用户B将000002.SZ放入自己的自选股中【订阅股票信息】,当交易所将股票的信息发送到终端的时候,炒股用户A和B能够实时观察到自己股票池中的股票000002.SZ的交易信息,并进行相应的操作。如果用户A此时觉得000002.SZ没有上涨的空间将其从自选股中剔除【取消订阅】,那么用户A此时便不再看到A的交易信息了。我们知道观察者模式定义了对象之间的一对多依赖,让多个观察者同时监听某一个主题对象,当这个主题对象发生改变时,便会通知所有的观察者更新。就拿上面的例子来说的话,股票【主题(Subject)】是依赖于用户群【观察者(Observer)】的,也就是在对象股票和用户之间是一对多的关系,同时,当对象股票交易价格变化的时候,便会通知其用户群接收行情信息。
代码:
public abstract class Subject { //用来存放所有的观察者对象 private IList<Observer> observers = new List<Observer>(); //注册一个观察者 public void RegisterObserver(Observer observer) { observers.Add(observer); } //移除一个观察者 public void RemoveObserver(Observer observer) { observers.Remove(observer); } //通知所有的观察者 public void NotifyObserver() { foreach (Observer observer in observers) { //调用观察者的 DisplayMessage 来让他们自动显示交易信息 observer.DisplayMessage(); } } } public class StockInfo:Subject { public string StockCode { get; set; } public double Close { get; set; } public StockInfo(string code, double close) { this.StockCode = code; this.Close = close; } } public interface Observer { /// <summary> /// 在接口中定义了当观察者得到主题给的通知后,自动更新自己的方法 /// </summary> void DisplayMessage(); } public class ConcreteObserver:Observer { //保存一个具体的主题对象,来获取主题提供的信息 private StockInfo subject; private string userName; public ConcreteObserver(string userName,StockInfo subject) { this.userName = userName; this.subject = subject; } // 告知用户XX股票的信息 public void DisplayMessage() { Console.WriteLine("UserName:{0},StockCode:{1},Close:{2}", userName,subject.StockCode,subject.Close); } } class Program { static void Main(string[] args) { //实例化一个主题对象 StockInfo stockInfo = new StockInfo("000002.SZ", 12.16); //实例化观察者对象 ConcreteObserver observerA = new ConcreteObserver("userA", stockInfo); ConcreteObserver observerB = new ConcreteObserver("userB", stockInfo); ConcreteObserver observerC = new ConcreteObserver("userC", stockInfo); //注册观察者 stockInfo.RegisterObserver(observerA); stockInfo.RegisterObserver(observerB); //取消订阅 stockInfo.RemoveObserver(observerC); //通知观察者最新收盘价 stockInfo.NotifyObserver(); Console.Read(); } }
1.4 推拉模式
观察者模式分为推模式和拉模式:
何为推模式呢:推模式就是当有新的消息时,把消息以参数的形式传递给每个观察者。
而拉模式呢:是当有新消息时,并不把消息的信息以参数的形式传递给每个观察者,而只是仅仅通知观察者有消息来到,而至于要不要提取出消息,那是观察者自己的事情了。也就是说,消息的提取必须由观察者自行完成,而不是由主题对象统一广播给所有的观察者。
其实上面的例子中使用的就是拉模式,您可以看到在 Subject 这个抽象类中的 NotifyObserver 方法中,
//通知所有的观察者
public void NotifyObserver()
{
foreach (Observer observer in observers)
{
//调用观察者的 DisplayMessage 来让他们自动显示交易信息
observer.DisplayMessage();
}
}
在上面的 DisplayMessage()方法中并没有传递进去参数,而是采用在 ConcreteObserver 类中保存了一个 ConcreteSubject 对象的引用,
public class ConcreteObserver:Observer
{
//保存一个具体的主题对象,来获取主题提供的信息
private StockInfo subject;
private string userName;
public ConcreteObserver(string userName,StockInfo subject)
{
this.userName = userName;
this.subject = subject;
}
通过这个引用来获取具体的消息。
// 告知用户XX股票的信息
public void DisplayMessage()
{
Console.WriteLine("UserName:{0},StockCode:{1},Close:{2}", userName,subject.StockCode,subject.Close);
}
}
从上面的做法,不难看出拉模式的一些优点和缺点:
优点在于拉模式可以按需取得消息数据,因为您可以在 DisplayMessage()按需要来决定是否要得到(在这里就是显示)数据。
当然,缺点就是在 ConcreteObserver 中保存了 ConcreteSubject 对象的引用,这样使得主题对象和观察者之间的耦合加强了。
我们再来看看观察者模式的推模式实现方式,具体见如下代码:
public abstract class Subject { //用来存放所有的观察者对象 public List<Observer> observers = new List<Observer>(); //注册一个观察者 public void RegisterObserver(Observer observer) { observers.Add(observer); } //移除一个观察者 public void RemoveObserver(Observer observer) { observers.Remove(observer); } public abstract void NotifyObserver(); } public class StockInfo : Subject { public string StockCode { get; set; } public double Close { get; set; } public StockInfo(string code, double close) { this.StockCode = code; this.Close = close; } //通知所有的观察者 public override void NotifyObserver() { foreach (Observer observer in observers) { //调用观察者的 Update 来让他们自动更新 observer.DisplayMessage(this); } } } public interface Observer { /// <summary> /// 在接口中定义了当观察者得到主题给的通知后,自动更新自己的方法 /// </summary> void DisplayMessage(StockInfo subject); } public class ConcreteObserver:Observer { //保存一个具体的主题对象,来获取主题提供的信息 private string userName; public ConcreteObserver(string userName) { this.userName = userName; } // 告知用户XX股票的信息 public void DisplayMessage(StockInfo subject) { Console.WriteLine("UserName:{0},StockCode:{1},Close:{2}", userName,subject.StockCode,subject.Close); } } class Program { static void Main(string[] args) { //实例化一个主题对象 StockInfo stockInfo = new StockInfo("000002.SZ", 12.16); //实例化观察者对象 ConcreteObserver observerA = new ConcreteObserver("userA"); ConcreteObserver observerB = new ConcreteObserver("userB"); ConcreteObserver observerC = new ConcreteObserver("userC"); //注册观察者 stockInfo.RegisterObserver(observerA); stockInfo.RegisterObserver(observerB); //取消订阅 stockInfo.RemoveObserver(observerC); //通知观察者最新收盘价 stockInfo.NotifyObserver(); Console.Read(); } }
从上面的代码中不难看出推模式的优缺点:
优点:所有的观察者直接得到消息,因为在 DisplayMessage()方法中直接将整个信息作为了参数传递。同时,您不必再在 ConcreteObserver 中保存和维护一个 ConcreteSubject 对象了,这样就减少了两者之间的耦合,当然,其缺点也是很明显的,就是观察者不能按需所取,即每次消息都是广播通知,每个观察者都将得到所有的信息。
从拉模式和推模式来看的话,两者是互补的,拉模式解决了推模式带来的缺点,而推模式又解决了拉模式带来的缺点,那么有没有一种办法可以同时继承拉模式和推模式的优点呢?
答案是. Net 里头的更好的解决办法---委托和事件
1.5 事件实现观察者模式
结合上篇文章,我们改动部分代码即可实现上面的例子:
public abstract class Subject { //通知所有的观察者 public abstract void NotifyObserver(); } //步骤1:定义一个委托类型,其有两个参数,其中参数 StockInfoEventArgs 是一种自定义的事件参数类型 public delegate void StockPriceChangedEventHanler(object sender, StockInfoEventArgs e); public class StockInfo : Subject { // 步骤4:定义一个事件,且事件的委托类型为 StockPriceChangedEventHanler public event StockPriceChangedEventHanler closeEvent; // 步骤6,以调用delegate的方式写事件触发函数 public override void NotifyObserver() { if (closeEvent != null) { StockInfoEventArgs e= new StockInfoEventArgs("000002.SZ",12.16); //步骤7:触发事件 closeEvent(this, e); } } } //步骤2:定义事件参数类,此类应当从System.EventArgs类派生。如果事件不带参数,这一步可以省略。 public class StockInfoEventArgs : EventArgs { public string StockCode { get; set; } public double Close { get; set; } public StockInfoEventArgs(string code, double close) { this.StockCode = code; this.Close = close; } } public interface Observer { /// <summary> /// 在接口中定义了当观察者得到主题给的通知后 /// 自动更新自己的方法 /// </summary> void DisplayMessage(object sender, StockInfoEventArgs e); } public class ConcreteObserver : Observer { private string userName; public ConcreteObserver(string userName) { this.userName = userName; } // 步骤3,定义事件处理方法,它与delegate对象具有相同的参数和返回值类型 public void DisplayMessage(object sender, StockInfoEventArgs e) { Console.WriteLine("UserName:{0},StockCode:{1},Close:{2}", userName, e.StockCode, e.Close); } } class Program { static void Main(string[] args) { //实例化一个主题对象 StockInfo stockInfo = new StockInfo(); //实例化观察者对象 ConcreteObserver observerA = new ConcreteObserver("userA"); ConcreteObserver observerB = new ConcreteObserver("userB"); ConcreteObserver observerC = new ConcreteObserver("userC"); // 步骤5,事件的挂钩用+=操作符将事件添加到队列中 stockInfo.closeEvent += observerA.DisplayMessage; stockInfo.closeEvent += observerB.DisplayMessage; stockInfo.closeEvent += observerC.DisplayMessage; //通知观察者最新收盘价 stockInfo.NotifyObserver(); Console.Read(); } }