小菜对大鸟说:“今天白天真的笑死人了,我们一同事在上斑期间看股票行情,被老板当场看到,老板很生气,后果很严重呀。”
“最近股市这么火,也应该可以理解的,你们老板说不定也炒股。”
“其实最近项目计划排得紧,是比较忙的。而最近的股市又特别的火,所以很多人都在偷偷地通过网页看行情.老板时常会出门办事,于是大家就可以轻松一些,看看行情,几个人聊聊买卖股票的心得什么的,但是一不小心,老板就会回来,让老板看到工作当中做这些总是不太好,你猜他们想到怎么办?”
“只能小心点,那能怎么办?”
“我们公司前台秘书是一个小美眉,她的名字叫童子喆,因为平时同事们买个饮料或零食什么的,都拿一份孝敬于她,所以关系比较好,现在他们就请小子品帮忙,如果老板出门后回来,就一定要打个电话进来,大家也好马上各就各位.这样就不会被老板发现问题了。”
“哈,好主意,老板被人卧了底,这下你们那些人就不怕被发现了。”
“是呀,只要老板进门,子喆拨个电话给同事中的一个,所有人就都知道老板回来了。这种做法屡试不爽。”
“那怎么还会有今天被发现的事?”
“今天是这样的,老板出门后,大家开始个个都打开股票行情查看软件,然后还聚在一起讨论着‘大盘现在如何’,‘你的股票抛了没有’等事。这时老板回来后,并没有直接走进去,而是对子品交待了几句,可能是要她打印些东西,并叫她跟老板去拿材料,这样子品就根本没有任何时间去打电话了。”
“哈,这下完了。”
“是呀,老板带着子喆走进了办公室的时候,办公室一下子从热闹转向了安静,好几个同事本是聚在一起聊天的,赶快不说话了,回到自己的座位上,最可怜的是那个背对大门的同事一一魏关姹,他显然不知道老板回来了,竟然还叫了一句‘我的股票涨停了哦。’,声音很大,就当他兴奋的转过身想表达一下激动的心情时,却看到了老板愤怒的面孔和其他同事同情的眼神。”
“幸运却又倒霉的人,谁叫他没看到老板来呢。”
“但我们老板很快恢复了笑容,平静地说道:‘魏关姹,恭喜发财呀,你是不是考虑请我们大家吃饭啊。’魏关姹面红耳赤地说,‘老板,实在对不起!以后不会了。’‘以后工作时还是好好工作吧。大家都继续工作吧。’老板没再说什么,就去忙事情去了。”
“啊,就这样结束了?我还当他会拿魏关姹做典型,好好批评一顿呢。不过回过头来想想看,你们老板其实很厉害,这比直接批评来得更有效,大家都是明白人,给个面子或许都能下得了台,如果真的当面批评,或许魏关蛇就干不下去了。”
“是的,生气却不发作,很牛。”
“你说的这件事的情形,是一个典型的观察者模式,你不妨把其间发生的事写成程序看看。”
“中啊,我想想看。”
半个小时后,小菜给了大鸟程序。
//前台秘书类 public class Secretary { private List<StockObserver> observers = new ArrayList<StockObserver>(); private String action; public void attach(StockObserver observer) { observers.add(observer); } public void announce() { for (StockObserver obj : observers) { obj.update(); } } public String getAction() { return action; } public void setAction(String action) { this.action = action; } } //看股票同事类 public class StockObserver { private String name; private Secretary sub; public StockObserver(String name, Secretary sub) { this.name = name; this.sub = sub; } public void update() { System.out.println(sub.getAction() + " " + name + "关闭股票行情,继续工作!"); } } //客户端代码 public class Main { public static void main(String[] args) { // 前台小组童子喆 Secretary tongzizhe = new Secretary(); // 看股票的同事 StockObserver tongshi1 = new StockObserver("魏关姹", tongzizhe); StockObserver tongshi2 = new StockObserver("易管查", tongzizhe); //前台记下了两位同事 tongzizhe.attach(tongshi1); tongzizhe.attach(tongshi2); //发现老板回来 tongzizhe.setAction("老板回来了!"); //通知两个同事 tongzizhe.announce(); } } 运行结果: 老板回来了! 魏关姹关闭股票行情,继续工作! 老板回来了! 易管查关闭股票行情,继续工作!
“写的8错8错,把整个事情都包括了。现在有个问题,不知道你发现没有,这个‘前台’类和这个‘看股票者’类之间怎样?”
“嗯,你是不是指互相耦合?我写的时候就感觉到了,前台类要增加观察者,观察者类需要前台的状态。”
“对啊,你想想看,如果观察者当中还有人想看NBA的网上直播,你的‘前台’类代码怎么办?”
“那就得改了。”
“你都发现了这个问题,想想我们的设计原则,该怎样做?”
“我就知道,你又要提醒我了。首先开放-封闭原则,修改原有代码就说明设计不够好。其次是依赖倒转原则,我们应该让程序都依赖于抽象,而不是相互依赖。OK,我去改改,应该不难的。”
半小时后,小菜给出了第二版本代码。
//抽象的观察者 public abstract class Observer { protected String name; protected Secretary sub; public Observer(String name, Secretary sub) { this.name = name; this.sub = sub; } public abstract void update(); } //两个具体的观察者,继承抽象观察者,对于update的方法做重写操作 public class StockObserver extends Observer { public StockObserver(String name, Secretary sub) { super(name, sub); } public void update() { System.out.println(sub.getAction() + " " + name + "关闭股票行情,继续工作!"); } } public class NBAObserver extends Observer { public NBAObserver(String name, Secretary sub) { super(name, sub); } public void update() { System.out.println(sub.getAction() + " " + name + "关闭NBA直播,继续工作!"); } } //前台秘书类,把所有的与具体观察者耦合的地方都改为了抽象观察者 public class Secretary { private List<Observer> observers = new ArrayList<Observer>(); private String action; public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void announce() { for (Observer obj : observers) { obj.update(); } } public String getAction() { return action; } public void setAction(String action) { this.action = action; } }
客户端的代码同前面一样。
“小菜,你这样写只完成了一半啊。”
“为什么,我不是已经增加了一个抽象观察者了吗?”
“你小子,考虑问题为什么就不能全面点,你仔细看看,在具体观察者中,有没有与具体的类耦合的?”
“嗯?这里有什么?哦,我明白了,你的意思是‘前台秘书’是一个具体的类,也应该抽象出来。”
“对啊,你想想看,你们公司最后一次,你们的老板回来,前台来不及电话了,于是通知大家的任务变成谁来做?”
“是老板,对的,其实老板也好,前台也好,都是具体的通知者,这里观察者也不应该依赖具体的实现,而是一个抽象的通知者。”
“另外,就算是你们的前台,如果某一个同事和她有矛盾,她生气了,于是不再通知这位同事,此时,她是否应该把这个对象从她加入的观察者列表中删除?”
“这个容易,调用detach方法将其减去就可以了。”
“好的,再去写写看。”
半小时后,小菜给出了第三版本代码。
//通知者接口 public interface Subject { void attach(Observer observer); void detach(Observer observer); void announce(); String getAction(); void setAction(String action); } //具体的通知者类可能是前台,也可能是老板,它们也许有各自的一些方法,但对于通知者来说,它们是一样的,所以它们都去实现这个接口。 public class Boss implements Subject { private List<Observer> observers = new ArrayList<Observer>(); private String action; public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void announce() { for (Observer obj : observers) { obj.update(); } } public String getAction() { return action; } public void setAction(String action) { this.action = action; } } public class Secretary implements Subject { private List<Observer> observers = new ArrayList<Observer>(); private String action; public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void announce() { for (Observer obj : observers) { obj.update(); } } public String getAction() { return action; } public void setAction(String action) { this.action = action; } } //对于具体的观察者,需要更改的地方就是把与‘前台’耦合的地方都改成针对抽象的通知者 public class StockObserver extends Observer { public StockObserver(String name, Subject sub) { super(name, sub); } public void update() { System.out.println(sub.getAction() + " " + name + "关闭股票行情,继续工作!"); } } public class NBAObserver extends Observer { public NBAObserver(String name, Subject sub) { super(name, sub); } public void update() { System.out.println(sub.getAction() + " " + name + "关闭NBA直播,继续工作!"); } } //客户端代码 public class Main { public static void main(String[] args) { // 老板胡汉三 Boss huhansan = new Boss(); // 看股票的同事 StockObserver tongshi1 = new StockObserver("魏关姹", huhansan); // 看NBA的同事 NBAObserver tongshi2 = new NBAObserver("易管查", huhansan); huhansan.attach(tongshi1); huhansan.attach(tongshi2); huhansan.detach(tongshi1); // 老板回来了 huhansan.setAction("我胡汉三回来了!"); huhansan.announce(); } } 运行结果: 我胡汉三回来了! 易管查关闭NBA直播,继续工作!
“由于魏关姹没有被通知到,所以他被当场抓获,下场很惨,现在我做到两者都不耦合了。”
“写的好,把结构图画出来看看。”
“这不难。”
小菜画出了代码的结构图。
“哈,小菜非常好,你已经把观察者模式的精华都写出来了,现在我们来看看什么叫观察者模式。”
观察者模式又叫发布-订阅(Publish/Subscribe)模式。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
观察者模式(Observer)结构图
//Subject类,可翻译为主题或抽象通知者,一般用一个抽象类或一个接口实现。它把所有对观察者对象的引用保存在一个聚集里面,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以拉回和删除观察者对象。 public abstract class Subject { private List<Observer> observers = new ArrayList<Observer>(); public void attach(Observer observer) { observers.add(observer); } public void detach(Observer observer) { observers.remove(observer); } public void announce() { for (Observer obj : observers) { obj.update(); } } } //Observer类,抽象观察者,为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。抽象观察者一般用一个抽象类或者一个接口实现。更新接口通常包含一个update()方法,这个方法叫做更新方法。 public abstract class Observer { public abstract void update(); } //ConcreteSubject类,叫做具体主题或具体通知者,将有关状态存入具体观察者对象,在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色通常用一个具体子类实现。 public class ConcreteSubject extends Subject { // 具体被观察者状态 private String subjectState; public String getSubjectState() { return subjectState; } public void setSubjectState(String subjectState) { this.subjectState = subjectState; } } //ConcreteObserver类,具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。具体观察者角色可以保存一个指向具体主题对象的引用。具体观察者角色通常用一个具体子类实现。 public class ConcreteObserver extends Observer { private String name; private String observerState; private ConcreteSubject subject; public ConcreteObserver(String name, ConcreteSubject subject) { this.name = name; this.subject = subject; } public void update() { observerState = subject.getSubjectState(); System.out.println("观察者" + name + "的新状态是" + observerState); } public ConcreteSubject getSubject() { return subject; } public void setSubject(ConcreteSubject subject) { this.subject = subject; } } //客户端代码 public class Main { public static void main(String[] args) { ConcreteSubject s = new ConcreteSubject(); s.attach(new ConcreteObserver("X", s)); s.attach(new ConcreteObserver("Y", s)); s.attach(new ConcreteObserver("Z", s)); s.setSubjectState("ABC"); s.announce(); } } 结果显示: 观察者X的新状态是ABC 观察者Y的新状态是ABC 观察者Z的新状态是ABC
“那观察者模式的动机是什么呢?”
“问的好,将一个系统分割成一系列的相互协作的类有一个很不好的副作用,那就是需要维护相关对象的一致性。我们不希望为了维持一致性而使种类紧密耦合,这样会给维护、扩展和重用都带来不便。而观察者模式的关键对象是主题Subject和观察者Observer,一个Subject可以有任意数目依赖它的Observer,一旦Subject的状态发生了改变,所有的Observer都可以得到通知。Subject发出通知时并不需要知道谁是它的观察者,也就是说,具体观察者是谁,它根本不需要知道。而任何一个具体观察者不知道也不需要知道其他观察者的存在。”
“什么时候考虑使用观察者模式呢?”
“你说什么时候?”
“当一个对象的改变需要同时改变其他对象的时候。”
“补充一下,而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。还有吗?”
“我感觉当一个抽象模式有两个方面,其中一方面依赖于另一方面,这时用观察者模式可以将两者封装在独立的对象中使它们各自独立地改变和复用。”
“非常好,总的来说,观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。”
“啊,这实在是依赖倒转原则的最佳体现啊。”
“我问你,在抽象观察者时,你的代码用的是抽象类,为什么不用接口?”
“因为我觉得两个具体观察者,看股票观察者和看NBA观察者类是相似的,所以用了抽象类,这样可以共用一些代码,用接口只是方法上的实现,没什么太大的意义了。”
“那抽象观察者可不可以用接口来定义?”
“用接口?我不知道,应该没必要吧。”
“哈,那是因为你不知道观察者模式的应用都是怎样的。现实的编程中,具体的观察者完全可能是风马牛不相及的类,但它们都需要根据通知者的通知来做出update()的操作,所以让它们都实现下面这样的一个接口就可以实现这个想法了。”
public interface Observer { public abstract void update(); }
“大鸟说的真好,这时用接口比较好。等等,这里还是有问题,在.NET环境下,这些控件要么是.NET类库,要么是其他人事先写好的控件,它如何再去实现拥有update()的Observer接口呢?”
大鸟笑而不语:“举个例子让我听听。”
“这样的例子很好举啊,比如,VS2005,当你点击运行程序之前的时候,整个界面是下面这样的。”
“当运行程序以后,除了弹出一个控制台的程序窗体以外,工具栏发生了变化,工具箱不见了,‘错误列表’变成了‘自动窗口’和‘命令窗口’打开。仅仅是点击了一个‘运行’按钮,就发生了这么多的变化,而各个变化都涉及到不同的控件。”
“我觉得没办法让每个控件都去实现一个Observer接口啊,因为这些控件都早已被它们的制造商给封装了。”
“小菜聪明啊,你问到点子上去了,尽管当点击‘运行’按钮时,确实是在通知相关的控件产生变化,但它们是不可能用接口的方式来实现观察者模式的。”
“那怎么办?”
“我了个去~凉拌呗。”
“快说。”
“还是回到刚才那个‘老板’、前台与同事的例子,你看看它还有什么不足之处?”
“和刚才说的问题一样,尽管已经用了依赖倒转原则,但是‘抽象通知者’还是依赖‘抽象观察者’,也就是说,万一没有了抽象观察者这样的接口,我这通知的功能就完成不了了。另外就是每个具体观察者,它不一定是更新的方法都要调用啊,就像刚才说的,我希望是‘工具箱’隐藏,‘自动窗口’打开,这根本就不是同名的方法。这应该就是不足之处吧。”
“是啊,如果通知者和观察者之间根本就互相不知道,由客户端来决定通知谁,那就好了。来,多们先来把原来的代码改造一下。”
“看股票观察者”和“看NBA观察者”类,去掉了父类“抽象观察者”,所以补上一些代码,并将“更新”方法名改为各自适合的方法名。为了使用.NET的委托机制,代表用C#实现。
//看股票的同事 class StockObserver { private string name; private Subject sub; public StockObserver(string name, Subject sub) { this.name = name; this.sub = sub; } //关闭股票行情 public void CloseStockMarket() { Console.WriteLine("{0} {1} 关闭股票行情,继续工作!", sub.SubjectState, name); } } //看NBA的同事 class NBAObserver { private string name; private Subject sub; public NBAObserver(string name, Subject sub) { this.name = name; this.sub = sub; } //关闭NBA直播 public void CloseNBADirectSeeding() { Console.WriteLine("{0} {1} 关闭NBA直播,继续工作!", sub.SubjectState, name); } } //抽象通知者由于不希望依赖于抽象观察者,所以增加和减少的就没有必要了,抽象观察者已经不存在了,通知者接口如下 //通知者接口 interface Subject { void Notify(); string SubjectState { get; set; } }
“下面就是如何处理老板类和前台类的问题了,它们当中通知方法有了对观察者遍历,所以不可小视之。但如果在.NET中,我们可以用一个非常好的技术来处理这个问题,它叫…”
“我顶你个肺,快说。”
“它叫委托。”
“委托啊,我都认真学过好几次了,但还是不太懂,同学中几个也都差不多,反正就不太明白,它到底是怎么回事,如何用。”
“先别管委托是怎么回事。我们看看如何做。”
声明一个委托,名称叫“EventHandler(事件处理程序)”,无参数,无返回值。
//事件处理程序的委托 delegate void EventHandler(); //老板类和前台秘书类 class Secretary : Subject { //声明一事件Update,类型为委托EventHandler public event EventHandler Update; private string action; public void Notify() { Update(); } public string SubjectState { get { return action; } set { action = value; } } } class Boss : Subject { //声明一事件Update,类型为委托EventHandler public event EventHandler Update; private string action; public void Notify() { Update(); } public string SubjectState { get { return action; } set { action = value; } } } //客户端代码 class Program { static void Main(string[] args) { //老板胡汉三 Boss huhansan = new Boss(); //看股票的同事 StockObserver tongshi1 = new StockObserver("魏关姹", huhansan); //看NBA的同事 NBAObserver tongshi2 = new NBAObserver("易管查", huhansan); huhansan.Update += new EventHandler(tongshi1.CloseStockMarket); huhansan.Update += new EventHandler(tongshi2.CloseNBADirectSeeding); //老板回来 huhansan.SubjectState = "我胡汉三回来了!"; //发出通知 huhansan.Notify(); Console.Read(); } } 运行结果: 老板回来了! 魏关姹关闭股票行情,继续工作! 老板回来了! 易管查关闭股票行情,继续工作!
“现在可以来解释一下,委托是什么了。委托就是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。委托可以看作是对函数的抽象,是函数的类,委托的实例将代表一个具体的函数。”
“你的意思就是说,delegate void EventHandler()可以理解为声明了一个特殊的类,而public event EventHandler Update可以理解为声明了一个类的变量。”
“哈哈,应该是声明了一个事件委托变量叫‘更新’。”
“你说的委托的实例将代表一个具体的函数,意思就是说new EventHandler(tongshi1.CloseStockMarket)其实就是一个委托的实例,而它就等于将tonshi1.CloseStockMarket这个方法委托给huhansan.Update这个方法了。”
“对的,就是这个意思,我刚才不是说过了吗,一旦为委托分配了方法,委托将与该方法具有完全相同的行为。而且,一个委托可以搭载多个方法,所有的方法被依次唤起。更重要的是,它可以使得委托对象所搭载的方法并不需要属于同一个类。你还不明白?”
“我明白了,这样就使得,本来是在老板类中的增加和减少的抽象观察者集合以及通知时遍历的抽象观察者都不必要了。转到客户端让委托搭载多个方法,这样就解决了本来与抽象观察者的耦合问题。”
“但委托也是有前提的,那就是委托对象所搭载的所有方法必须具有相同的原型和形式,也就是拥有相同的参数列表和返回值类型。”
“如果参数列表都不同那还瞎掺和啥。太强大了,当时那些牛人是怎样设计出这东东来的,本来观察者模式已经把依赖倒转原则做的非常的好了,现在看来,委托和事件,岂不是更加的优秀,解决的问题更加的优雅?”
“注意,是先有观察者模式,再有委托事件技术的,再说,它们各有优缺点,你不妨去看看MSDN,讲的已经很详细了。”
“我现在对委托和事件有些了解了,相信再去研究MSDN也就不难了。这时再看看刚才举的那个例子,当点‘运行’时,所谓的‘工具箱隐藏’、‘错误列表隐藏’、‘自动窗口打开’、‘命令窗口打开’不过就是‘运行’时注册的四个事件的触发而已。别说就四个,就是四十个完全不同的控件,也都能通知到位了。”
“夸张是有点夸张,不过确实也是如此。”
注:委托和事件在附录一中的1.13节中还有讲解,可参考。
突然小菜的手机响了。
“小菜,我是石守吉,昨天我手机丢了,没办法,只得重买一个。原来的那个号也没法办回来,还好我记得你的手机,所以用这个新号打给你了。你能不能把我们班级同学的号码抄一份发邮件给我?”
“哦,这个好办,不过班级这么多人,我要是抄起来,也容易错。而且,如果现在同学有急事要找你,不就找不到了吗?我们这样办吧…一用观察者模式。”
“你说什么?我听不懂呀。什么观察者模式?”
“哈,其实就是我在这里给我们班级所有同学群发一条短消息,通知他们,你石守吉已换新号,请大家更新号码,有事可及时与石守吉联系。”
“好办法,你可记得一定要给李MM、张MM、王MM发哦。”
“你小子,首先想着的就是MM。放心吧,我才不管谁呢,凡是在我手机里存的班级同学,我都会循环遍历一遍,群发给他们的。”
“小菜怎么张口闭口都是术语呀,好的,你就循环遍历一下吧。这事就委托给你了,谢谢哦!”