返回首页
当前位置: 主页 > 网络编程 > .Net实例教程 >

重温Observer模式--热水器

时间:2017-02-11 21:34来源:知行网www.zhixing123.cn 编辑:麦田守望者

在 C#中的委托和事件 一文的后半部分,我向大家讲述了Observer(观察者)模式,并使用委托和事件实现了这个模式。实际上,不使用委托和事件,一样可以实现Observer模式。在本文中,我将使用GOF的经典方式,再次实现一遍Observer模式,同时将讲述在 C#中的委托和事件 一文中没有提及的推模式(Push)和拉模式(Pull)。

设计思想概述

在 C#中的委托和事件 一文后半部分中我已经较详细的讲述了Observer设计模式的思想,所以这里仅简单的提及一下。Observer设计模式中实际上只包含了两类对象,一个是Subject(主题),一个是Observer(观察者)。它们之间的角色是:

  • Subject:主题(被监视对象),它往往包含着Observer所感兴趣的内容。
  • Observer:观察者,它观察Subject。当Subject中的某件事发生的时候(通常是它所感兴趣的内容改变的时候),会被自动告知,而Observer则会采取相应的行动(通常为更新自身状态或者显示输出)。

它们之间交互的核心工作流程就是:

  1. Subject 提供方法,比如Register()和UnRegister(),用于Observer进行注册(表示它对Suject感兴趣)和取消注册(不再对它感兴趣)。
    • Register()方法实现为:它接收一个Observer的引用作为参数,并保存此引用。
    • 保存的方式通常为在 Subject内声明一个集合类,比如:List<Observer>。
    • 一个Subject可以供多个Observer注册。
  2. 调用Subject实例的Register()方法,并将一个Observer的引用传递进去。
  3. Observer 包含一个Update()方法,此方法供 Subject(通过保存的Observer的引用)以后调用。
  4. Subject 包含一个Notify()方法,当某件事发生时,调用Notify(),通知Subject。
    • Notify的方法实现为:遍历保存Observer引用的集合类,然后在Observer的引用上调用Update方法,更新Observer。
    • 某件事是一个不确定的事,对于热水器来说,这个事就是“温度达到一定高度”。它对外界暴露的方法,应该是“烧水” -- BoilWater(),而不是Notify(),所以Notify通常实现为私有方法。

Observer 向 Subject 注册的序列图表示如下:

Subject事件触发时,通知Observer调用Update()方法的序列图如下:

模式的接口定义

按照面向对象设计的原则:面向接口编程,而非面向实现编程。那么现在应该首先定义Subject和Observer的接口,我们可能很自然地会想到将这两个接口分别命名为 ISubjcet 和 IObserver。而实际上,据我查阅的一些资料,这里约定俗成的命名为:IObservable 和 IObserver,其中由 Subject 实现 IObservable。

NOTE:可能很多人和我当初一样困惑,命名为ISubject不是很好么,为什么叫 IObservable?我参考了一些资料,大概的解释是这样的:接口定义的是一个行为,表示的是一种能力,所以对于接口的命名最好用动词的形容词或者名词变体。这里,Observe是一个动词,意为观察,Observer是动词的名词变体,意为观察者;Observable是动词的形容词变体,表示为可观察的。类似的例子有很多,比如IComparable 和 IComparer 接口、IEnumerable 和 IEnumerator 接口等。

现在我们先来看Subject需要实现的接口IObservable。

IObservable接口

首先创建解决方案ObserverPattern,并在其下添加控制台项目ConsoleApp,然后假如IObservable.cs文件,来完成这个接口。如同我们上面分析的,Suject将实现这个接口,它只用定义两个方法 Register()和Unregister:

public interface IObservable {
    void Register(IObserver obj);       // 注册IObserver
    void Unregister(IObserver obj);     // 取消IObserver的注册
}

注意它的两个方法接收 IObserver类型的对象,分别用于注册和取消注册。

IObserver 接口

现在我们再来完成IObserver接口,所有的Observer都需要实现这个接口,以便在事件发生时能够被 自动告知(自动调用其Update()方法,改变自身状态),它仅包含一个Update()方法:

public interface IObserver {
    void Update();      // 事件触发时由Subject调用,更新自身状态
}

再强调一遍,这里的关键就是Update()方法不是由Observer本身调用,而是由Subject在某事发生时调用。

抽象基类 SubjectBase

注意到上面序列图中的Container(容器),它用于保存IObserver引用的方式,对于很多IObservable的实现来说可能都是一样的,比如说都用List<IObserver>或者是Hashtable等。所以我们最好再定义一个抽象类,让它实现 IObservable 接口,并使用List<IObserver>作为容器的一个默认实现,以后我们再创建实现IObservalbe的类(Subject),只需要继承这个基类就可以了,这样可以更好地代码重用:

public abstract class SubjectBase : IObservable {
    // 使用一个 List<T> 作为 IObserver 引用的容器
    private List<IObserver> container = new List<IObserver>();
   
    public void Register(IObserver obj) {
       container.Add(obj);
    }

    public void Unregister(IObserver obj) {
       container.Remove(obj);
    }

    protected virtual void Notify() {       // 通知所有注册了的Observer
       foreach (IObserver observer in container) {
           observer.Update();           // 调用Observer的Update()方法
       }
    }
}

有了这样两个接口,一个抽象类我们的UML类图便可以画出来了:

注意这里也可以不使用IObservable接口,直接定义一个抽象类,定义IObservable接口能进一步的抽象,更灵活一些,可以基于这个接口定义出不同的抽象类来(主要区别为Container的实现不同,可以用其他的集合类)。

Observer模式的实现

现在我们来实现Observer模式,我们先创建我们的实体类(Concrete Class):热水器(Heater),报警器(Alarm),显示器(Screen)。其中,热水器是Subject,报警器和显示器是Observer。报警器和显示器关心的东西是热水器的水温,当热水器的水温大于97度时,显示器需要显示“水快烧开了”,报警器发出声音,也提示“嘟嘟嘟,水快烧开了”。

下面的代码非常的简单明了,也添加了注释,我就不做说明了:

热水器(Subject)的实现

热水器继承自SujectBase基类,并添加了BoilWater()方法。

public class Heater : SubjectBase {
    private string type;              // 添加型号作为演示
    private string area;              // 添加产地作为演示
    private int temprature;         // 水温

    public Heater(string type, string area) {
       this.type = type;
       this.area = area;
       temprature = 0;
    }

    public string Type { get { return type; } }
    public string Area { get { return Area; } }

    public Heater() : this("RealFire 001", "China Xi'an") { }

    // 供子类覆盖,以便子类拒绝被通知,或添加额外行为
    protected virtual void OnBoiled() {
       base.Notify(); // 调用父类Notify()方法,进而调用所有注册了的Observer的Update()方法
    }

    public void BoilWater() {       // 烧水
       for (int i = 0; i <= 99; i++) {
           temprature = i+1;
           if (temprature > 97) {       // 当水快烧开时(温度>97度),通知Observer
              OnBoiled();
           }
       }
    }
}

报警器 和 显示器 (Observer)的实现

报警器(Alarm)和显示器(Screen)的实现是类似的,仅仅为了说明多个Observer可以注册同一个Subject。

// 显示器
public class Screen : IObserver {

    // Subject在事件发生时调用,通知Observer更新状态(通过Notify()方法)
    public void Update() {
       Console.WriteLine("Screen".PadRight(7) + ": 水快烧开了。");
    }
}
// 报警器
public class Alarm : IObserver {
    public void Update() {
       Console.WriteLine("Alarm".PadRight(7) + ":嘟嘟嘟,水温快烧开了。");
    }
}

运行程序

接下来,我们运行一下程序:

class Program {
    static void Main(string[] args) {

       Heater heater = new Heater();
       Screen screen = new Screen();
       Alarm alarm = new Alarm();

       heater.Register(screen);     // 注册显示器
       heater.Register(alarm);         // 注册热水器

       heater.BoilWater();             // 烧水
       heater.Unregister(alarm);    // 取消报警器的注册

       Console.WriteLine();
       heater.BoilWater();             // 再次烧水
    }
}

输出为:

Screen : 水快烧开了。
Alarm  :嘟嘟嘟,水快烧开了。
Screen : 水快烧开了。
Alarm  :嘟嘟嘟,水快烧开了。
Screen : 水快烧开了。
Alarm  :嘟嘟嘟,水快烧开了。

Screen : 水快烧开了。
Screen : 水快烧开了。
Screen : 水快烧开了。

推模式 和 拉模式

像上面这种实现方式,基本上是没有太大意义的。比如说,我们通常会希望在Screen上能够即时地显示水的温度,而且当水在100度的时候显示“水已经烧开了”,而非“水快烧开了”。我们还可能希望显示热水器的型号和产地。所以我们需要 在Observer的Update()方法中能够获得 Subject中所发生的事件的进展状况 或者事件触发者Suject的状态和属性。在本例中事件的进展状况,就是水的温度;事件触发者(Suject)的状态和属性,则为 热水器的型号和产地。此时,我们有两种策略,一种是 推模式,一种是拉模式,我们先看看推模式。

Observer中的推模式

顾名思义,推模式就是Subject在事件发生后,调用Notify时,将事件的状况(水温),以及自身的属性(状态)封装成一个对象,推给Observer。而如何推呢?当然是通过Notify()方法,让Notify()方法接收这个对象,在Notify()方法内部,再次将对象传递给Update()方法了。那么现在要做两件事:1、创建新类型,这个类型封装了我们想要推给Observer(显示器)的事件进展状况(水温),以及事件触发者Subject(热水器)的属性(或者叫状态)。

我们在ObserverPattern解决方案下重新建一个控制台项目,起名为ConsoleApp2,并设置为启动项目。将上一项目ConsoleApp中的文件复制进来,然后我们创建一个新类型BoiledEventArgs,用它来封装我们推给Observer的数据。

public class BoiledEventArgs {
    private int temperature;     // 温度
    private string type;         // 类型
    private string area;         // 产地

    public BoiledEventArgs(int temperature, string type, string area) {
       this.temperature = temperature;
       this.type = type;
       this.area = area;
    }

    public int Temperature { get { return temperature; } }
    public string Type { get { return type; } }
    public string Area { get { return area; } }
}

注意这个类型的命名虽然为BoiledEventArgs,但是和.Net中的内置类型EventArgs没有任何联系,只是起了这样一个名字。

顶一下
(1)
100%
踩一下
(0)
0%
标签(Tag):Observer模式
------分隔线----------------------------
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
验证码:点击我更换图片
猜你感兴趣