zoukankan      html  css  js  c++  java
  • 重温Observer模式热水器·改

    引言

    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没有任何联系,只是起了这样一个名字。

    2、我们需要依次修改 IObserver接口,Screen类的Update()方法,SubjectBase类,以及Heater类,让他们可以接收这个EventArgs参数。出于示范的目的,后面的例子我都将不再使用警报器Alarm类,它的存在仅仅是为了说明多个Observer可以注册一个Subject,上面我们已经示范过了,所以现在我们把它删掉。

    我们先来看下IObserver接口:

    public interface IObserver {
        // 推模式的实现方式,接收一个BoiledEventArgs
        void Update(BoiledEventArgs e);
    }

    接口变了,显示器(Screen)的实现也需要修改:

    public class Screen : IObserver {
        private bool isDisplayedType = false;      // 标记变量,标示是否已经打印过
        public void Update(BoiledEventArgs e) {

           // 打印产地和型号,只打印一次
           if (!isDisplayedType) {
               Console.WriteLine("{0} - {1}: ", e.Area, e.Type);
               Console.WriteLine();
               isDisplayedType = true;
           }

           if (e.Temperature < 100) {  
               Console.WriteLine(
                  String.Format("Alarm".PadRight(7) + ":水快烧开了,当前温度:{0}。", e.Temperature));
           } else {
               Console.WriteLine(
                  String.Format("Alarm".PadRight(7) + ":水已经烧开了!!"));                
           }
        }
    }

    现在可以看到,在Update()方法中,通过传递进来的BoiledEventArgs参数,我们可以获得事件进展(温度),以及事件触发者的信息(产地和型号)了。

    接下来我们看这个 BoiledEventArgs是如何传递给 Update()方法的,我们看下SubjectBase基类 和 热水器Heater需要做怎样的修改:

    public abstract class SubjectBase : IObservable {
        // 其余略...

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

    public class Heater : SubjectBase {
        // 其余略 ...

        // 供子类覆盖,以便子类拒绝被通知,或者添加额外行为
        protected virtual void OnBoiled(BoiledEventArgs e) {
           base.Notify(e);          // 调用基类方法,通知Observer
        }

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

    我们看到,在事件发生时(水温>97度),我们根据事件进展状况和热水器的属性创建了BoiledEventArgs类型的实例,并且传递给了OnBoiled()方法,进而调用了基类的方法,传递了该实例。

    我们再次对程序进行一下测试:

    class Program {
        static void Main(string[] args) {
           Heater heater = new Heater();
           Screen screen = new Screen();

           heater.Register(screen);     // 注册显示器
           heater.BoilWater();             // 烧水
        }
    }

    输出为:
    China Xi'an - RealFire 001:

    Alarm  :水快烧开了,当前温度:98。
    Alarm  :水快烧开了,当前温度:99。
    Alarm  :水已经烧开了!!

    Observer 中的拉模式

    继续进行之前,我们在ObserverPattern解决方案下,再创建一个新的Console项目,命名为ConsoleApp3,然后把ConsoleApp2 项目下的文件拷贝过来,把启动项目设置为ConsoleApp3。

    拉模式的意思就是说,Subject(热水器)在事件发生时(水温超过97度),并非将自身状态封装成对象通过Notify()方法,进而再通过Observer的引用,调用Update()方法传递给Observer(显示器),而是直接将自身的引用(以基类或者Object的形式)传递过去。Observer在Update()方法中,对传递进来的引用进行一个向下转换(Downcast),转换成具体的Subject类(比如热水器),然后通过这个引用调用Subject实体类(热水器)的公共属性获取状态信息(从中把有用数据拉出来 :-)。

    我们需要再次对IObserver接口的Update()方法修改,相应的修改还要修改SubjectBase基类、Heater类 以及 IObserver接口的实现--显示器类(Screen)。

    public interface IObserver {
        // 拉模式的Update()方法定义
        void Update(IObservable sender);
    }

    注意这里接收一个IObservable类型作为Update()方法的参数,而IObservable接口本身只包含Regesiter()和Unregister()两个方法,所以在IObserver的实现中,这里要进行向下转换,转换为响应的实体类对象,才能获得对象的属性。这里也可以接受一个Object类型参数。

    我们现在看这个接口的实现,显示器类(Screen):

    public class Screen : IObserver {
        private bool isDisplayedType = false;

        public void Update(IObservable obj) {

           // 这里存在一个向下转换(由继承体系中高级别的类向低级别的类转换)。
           Heater heater = (Heater)obj;

           // 打印产地和型号,只打印一次
           if (!isDisplayedType) {
               Console.WriteLine("{0} - {1}: ", heater.Area, heater.Type);
               Console.WriteLine();
               isDisplayedType = true;            
           }

           if (heater.Temperature < 100) {     // 通过热水器引用heater获取温度
               Console.WriteLine(
                  String.Format("Alarm".PadRight(7) + ":水快烧开了,当前温度:{0}。", heater.Temperature));
           } else {
               Console.WriteLine(
                  String.Format("Alarm".PadRight(7) + ":水已经烧开了!!"));                
           }
        }
    }

    接下来我们再看下 SubjectBase基类,以及热水器Heater的修改:

    public class SubjectBase
        // 其余略...

        // 接受一个 IObservable 类型
        protected virtual void Notify(IObservable obj) {      // 通知所有注册了的Observer
           foreach (IObserver observer in container) {
               observer.Update(obj);        // 调用Observer的Update()方法
           }
        }
    }

    public class Heater : SubjectBase {
        // 其余略...

        // 新添属性 Temperature
        public int Temperature { get { return temperature; } }

        // 供子类覆盖,以便子类拒绝被通知,或者添加额外行为
        protected virtual void OnBoiled() {
           base.Notify(this);           // <-- 将本身传递过去
        }

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

    注意,Heater类以前不提供对temperature字段的访问,而为了能在Observer(显示器)的Update()方法中的通过引用访问到temperature,我们需要为Heater类再添加一个 Temperature属性:

    public int Temperature { get { return temperature; } }

    而在调用Notify()方法时,我们通过this关键字将对热水器Heater本身的引用传递了进去:

    base.Notify(this);           // <-- 将本身传递过去

    我们再来做个测试:

    class Program {
        static void Main(string[] args) {
           Heater heater = new Heater();
           Screen screen = new Screen();

           heater.Register(screen);     // 注册显示器
           heater.BoilWater();             // 烧水
        }
    }

    输出为:
    China Xi'an - RealFire 001:

    Alarm  :水快烧开了,当前温度:98。
    Alarm  :水快烧开了,当前温度:99。
    Alarm  :水已经烧开了!!

    可以看到和前面完全一样的输出。

    推模式和拉模式 的区别

    那么大家一定想问,使用推模式和拉模式,有什么区别呢?

    • 推模式的好处是 按需供给,想要提供给 Observer端什么数据,就将这些数据封装成对象,传递给Observer,缺点是需要创建自定义的EventArgs对象。
    • 拉模式的好处 则是不需要另外定义对象,直接将自身的引用传递进去就可以了。但是缺点是我们可能会需要暴露我们不想暴露的内部成员,比如本例中的temperature。我们期望将它作为类的内部数据,仅提供给显示器。但是使用拉模式,你只得为它再提供一个公共的Temperature访问器,这样在程序的其他的地方也可以访问到了,比如说在Program里。除此以外,我们不期望Screen可以进行烧水BoilWater()这一动作,但是由于它获得了Heater的引用,而BoilWater()方法又是Public公共的,所以在Update()方法中也具备了对热水器操作的能力,比如调用 BoilWater() 方法。

    .Net 中没有内置的IObserver和IObservable接口,因为在.Net中,可以通过委托和事件来完成,但是一样面临选择推模式还是拉模式的问题,何时使用哪种策略完全依赖于设计者,你也可以将两种方式都实现了,比如,将IObserver接口定义成这样:

    // 类似微软的实现:两个都用 ...
    void Update(Object sender, BoiledEventArgs e);

    注意,这里我用得是BoiledEventArgs作为Update()的参数,这里显然不够合适,如果期望这个接口可以为各种Observer服务,而不仅限于烧水这一事件,那么最好定义一个基类 EventArgs,然后对于各种不同的事件,定义不同的EventArgs类,再让它们去继承EventArgs。如此,可以得到下面的接口定义:

    void Update(Object sender, EventArgs e);

    呵呵,看到这里君应该都明白了吧,微软对这个方法原型定义了一个委托,叫做EventHandler:

    public delegate void EventHandler(object sender, EventArgs e);

    再谈下去又绕到委托和事件了,我们回到主题,将本文的内容做个总结吧。

    总结

    本文我再次使用热水器的例子实现了Observer设计模式,但这一次我没有使用委托和事件,而是通过经典的GOF方式。我同时还讨论了实现Observer模式时Subject向Observer提供数据值可以采用的两种方式--推模式和拉模式。最后,我们对这两种模式进行了一个简单的比较,并简要介绍了.Net Framework中采用的方式。

    感谢阅读,希望这篇文章能给你带来帮助!

  • 相关阅读:
    Swift入门篇-Hello World
    Swift入门篇-swift简介
    Minecraft 插件 world edit 的cs 命令
    搭建本地MAVEN NEXUS 服务
    MC java 远程调试 plugin 开发
    企业内部从零开始安装docker hadoop 提纲
    javascript 命令方式 测试例子
    ca des key crt scr
    JSF 抽象和实现例子 (函数和属性)
    form 上传 html 代码
  • 原文地址:https://www.cnblogs.com/bober/p/2108070.html
Copyright © 2011-2022 走看看