重温设计模式之观察者

之前看过一些设计模式的书和文章,像head first,大话设计模式,不过一直都是囫囵吞枣,一知半解,

时间一长,大多忘了个十之八九,和没看过竟然相差无几,这实在是件很让人伤感的事。

这情形就好像你看过一部很经典的电影,有一天和女神聊天,女神说:我看过一部很经典的电影………,

然后神情的忘着你,想和你交流下那部电影的观后感,而此时,你的心里确是一片空白,你甚至会狠咬自己一口看能否从

疼痛中挤出点印象来,因为你知道,和女神聊电影的机会可是不多的,

可是很可惜,你还是错过了,于是你镇痛过后,决定再次重温,将其记录下来!

闲话太多了,下面切入正题,此处涉及如下几个问题:

1 观察者模式的定义?

2 它主要应用在何处?

3 示例?

4 剩下的问题?

1 观察者模式的定义:

此模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。[GOF 《设计模式》]

说白点意思就是:假如有对象a,b,c,当a发生变化时,需要对象b,c得到通知,并做相应的变化,那么此情形就是一个观察者模式。

2 应用在何处?

举几个例子:

(1)以去银行柜台取钱为例,步骤应该是你先去拿号然后等着显示屏上显示你拿的号码了,就过去柜台取钱,这是一个很经典的观察者模式。

柜台办理完业务后通知显示屏,然后显示屏更新信息,这里类图关系如下:

柜台类a,显示屏b,c,d等,类a变化时通知b,c,d,然后b,c,d等收到变更通知后更新信息

(2)以天气预报为例,气象台取得最新天气信息后,通知所有的电视台播报员,然后播报员预播最新的天气信息,

类图关系如:气象台a,电视台b,c,d等,气象台a观察到天气信息有变化,通知b,c,d天气有变,然后b,c,d播报最新天气信息

3 此时似乎还是一知半解,来看示例吧!

以银行柜台显示屏为例来看,我们先从最简单的实现代码开始,逐渐修改成观察者模式实现方式:

(1)先从实现如上需求最简单的代码开始,如下:

 /// <summary>
    /// 柜台
    /// </summary>
    public class Counter
    {
        private List<Screen> lstScreen = new List<Screen>();//显示屏列表
        private string counterName;//柜台名称
        private string bussinessNo;//当前业务号

     public Counter(string name,string no)
     {
         this.counterName = name;
         this.bussinessNo = no;
     }

        //增加需要通知的屏幕
      public void AddScreen(Screen Screen)
      {
            lstScreen.Add(Screen);
      }

      //删除需要通知的屏幕
      public void RevScreen(Screen Screen)
      {
         lstScreen.Remove(Screen);
      }

        //柜台业务发生变化,通知显示屏更新信息
        public void NotifyChange()
        {
            foreach(Screen Screen in lstScreen )
            {
                Screen.Display();
            }
        }

        //通知显示屏要更新的信息
        public string GetInformation()
        {
            return "请"+bussinessNo+"号到"+ counterName+"号柜台办理业务!";
        }


    }

    /// <summary>
    /// 显示屏
    /// </summary>
    public class Screen
    {
        private string name;
        private Counter counter;

        public Screen(string name,Counter counter)
        {
            this.name = name;
            this.counter = counter;
        }

        public void Display()
        {
            Console.WriteLine(this.name+":"+counter.GetInformation());
        }
    }

上面有2个类,柜台类和显示屏类,业务类调用代码如下:

  static void Main(string[] args)
        {
            Counter counter = new Counter("1号柜台","业务号9");
            Screen scr1 = new Screen("1号显示屏", counter);
            Screen scr2 = new Screen("2号显示屏", counter);
            counter.AddScreen(scr1);
            counter.AddScreen(scr2);

            counter.NotifyChange();
        }

 执行过程:先实例化柜台类,然后将要通知显示的显示屏加到通知列表,然后调用通知的方法,

显示屏更新的过程在同志的 方法里实现。上述过程有个显而易见的问题就是,双向耦合调用。

 
 public void NotifyChange()
        {
            foreach(Screen Screen in lstScreen )
            {
                Screen.Display();
            }
        }


  public void Display()
        {
            Console.WriteLine(this.name+":"+counter.GetInformation());
        }

柜台类的通知方法

 NotifyChange()

里调用了显示屏更新信息的方法,而显示屏更新信息

Display()

的方法又调用了柜台类获取通知信息的方法。

 (2)若此时需求出现变更,除了显示屏,还需要用广播通知,即柜台办理完业务后,需要用广播更新信息,

业务你想到了,再新建一个广播类(和显示屏类类似),然后在柜台类最如下修改

  //柜台业务发生变化,通知显示屏更新信息
        public void NotifyChange()
        {
            foreach(Screen Screen in lstScreen )
            {
                Screen.Display();
            }
遍历广播类,通知其更新信息         }

我们增加了一个广播类Speaker,还修改了类Counter,,当前提出的需求算是解决了,可是代码的耦合度却加大了,已经有3个类纠缠在一起了,那么以后再追加接收信息的终端怎么办?每一次追加观察者,都不得不改一遍类Counter的代码,如果继续做下去,出错的几率将不断加大,可维护性不断降低,这段代码明显违背开闭原则,所以,我们需要重新审视之前的设计了。

此时也许你想到了抽象出一个类作为显示屏,和广播类的基类。面向对象的设计中最重要的思维就是抽象。显然,无论是小屏、大屏还是音箱,它们只是外观和更新信息的手段有所不同,更新信息的功能却是一致的,所以,我们完全可以把这些观察者抽象出来一个基类Observer,更新信息的手段由各个观察者自己负责实现。

  abstract public class Observer
    {
        protected String name;
        protected Counter counter;
        //构造函数
        public Observer(String name, Counter counter)
        {
            this.name = name;
            this.counter = counter;
        }
        //更新信息
        public abstract void update();
    }

 然后柜台类作如下修改:

 private List<Observer> lstScreen = new List<Observer>();

经过我们这么一番改造,应对观察者的加入是绝对没有问题了,再也不用去修改柜台类Counter了,把柜台类对具体终端的依赖关系给去除了。不管是小屏还是音箱,柜台类一视同仁,进行同样的处理,它根本不需要知道需要通知的对象是什么。比如:银行又提出来加入大屏的显示。这样的需求,对于我们来说已经是很easy的事情了,可以从Observer类再派成出来一个大屏类LargeScreen。有人问:“大屏和小屏还不一样,为什么不用一个类来表示呢”。其实,做过排队项目的人可能很清楚,大屏幕的显示驱动和显示方式可能与小屏完全不同,甚至于供货厂商都不一样,在这个案例中,我们只是剥离出来它的其中一项显示功能而已。又比如:银行为了提升服务质量,准备加入短信提醒用户的功能。现在,我们是不是很容易对付了?

(3)银行又提出了新的需求:“除了柜台上可以发布呼号信息以外,银行内部的管理部门在某些情况下,也能利用大小屏发布一些紧急的信息”。 我们按照处理观察者方法,可以把这些通知者抽象出来,形成一个基类Subject,银行柜台和管理部门作为两个通知者,都可以从基类Subject派生出来,以后再增加通知者,就能够以此类推。我们按照这个思路形成最终一个版本:

抽象出柜台类的基类

public abstract class Subject {
    //观察者列表
     private List<Screen> lstScreen = new List<Screen>();      //增加需要通知的屏幕
      public void AddScreen(Screen Screen)
      {
            lstScreen.Add(Screen);
      }

      //删除需要通知的屏幕
      public void RevScreen(Screen Screen)
      {
         lstScreen.Remove(Screen);
      }

        //柜台业务发生变化,通知显示屏更新信息
        public void NotifyChange()
        {
            foreach(Screen Screen in lstScreen )
            {
                Screen.Display();
            }
        }    
 //通知显示屏要更新的信息
        public string GetInformation()
        {
            return ""+bussinessNo+"号到"+ counterName+"号柜台办理业务!";
        }

}

柜台类等通知类继承此基类,其它地方无需更改,调用如下:

          Counter counter = new Counter("1号柜台","业务号9");
            Screen scr1 = new Screen("1号显示屏", counter);
            Screen scr2 = new Screen("2号显示屏", counter);
            counter.AddScreen(scr1);
            counter.AddScreen(scr2);

            counter.NotifyChange();

            Speaker speaker = new Speaker("1号广播", "业务号9");
            Screen scr1 = new Screen("1号显示屏", speaker);
            Screen scr2 = new Screen("2号显示屏", speaker);
            speaker.AddScreen(scr1);
            speaker.AddScreen(scr2);

            speaker.NotifyChange();

又经过我们的一番改造,应对主题和观察者的需求变化都没有什么问题了,以后针对类似这样的需求,我们已经非常有经验了,完全可以把这段代码稍加修改套用一下就可以了。换句话说,我们把观察者模式的实现方式做成了一个很小的框架。

参考博客:http://blog.csdn.net/wanghao72214/article/details/4017507

http://kb.cnblogs.com/page/49989/

原文地址:https://www.cnblogs.com/jangwewe/p/3009355.html