14.老板回来, 我不知道 - 观察者模式(Observer)

14.1 老板回来? 我不知道!

  • 时间   4月12日21点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     小菜对大鸟说: “今天白天真的笑死人了, 我们一同事在上班时间看股票行情.被老板当场看到, 老板很生气, 后果很严重.”

     “最近股市这么火, 也应该是可以理解的, 你们老板说不定也炒股.”
     “其实最近项目计划安排得紧, 是比较忙的. 而最近的股市有特别火, 所以很多人都在偷偷的通过网页看行情…….”

14.2 双向耦合的代码

     “你说的这件事的情形, 是一个典型的观察者模式.” 大鸟说, “你不妨把其间发生的事情写成程序看看.”
     “哦, 好的, 我想想看.” 小才开始在纸上画起来.
     半分钟后, 小菜给大鸟程序.

前台秘书类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Secretary {
// 同事列表
private List<StockObserver> observers = new ArrayList<>();
private String action;
// 增加
public void attach(StockObserver observer) {
observers.add(observer);
}
// 通知
public void notifyObservers() {
for (StockObserver o :
observers) {
o.update();
}
}
// 前台状态
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}

看股票的同事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 + "关闭股票行情, 继续工作!");
}
}

客户端程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.notifyObservers();
}

运行结果如下

1
2
老板回来了!魏观姹关闭股票行情, 继续工作!
老板回来了!易管查关闭股票行情, 继续工作!

     “写的不错, 把整个事情都包括了. 现在的问题是, 你有没有发现, 这个 ‘前台’ 类和这个 ‘看股票者’ 类之间怎么样?”
     “恩, 你是不是之相互耦合? 我写的时候就感觉到了, 前台类要增加观察者, 观察者类需要前台的状态.”
     “对呀, 你想想看, 如果观察者当中还有人是想看 NBA 的网上直播(由于时差关系, 美国 NBA 篮球比赛通常都是在北京时间的上午开始), 你的 ‘前台’ 类代码怎么办?”
     “那就得改动了.”
     “你都发现这个问题了, 你说该怎么办? 想想我们的设计原则?”
     “我就知道, 已有要提醒我了. 首先是开放—封闭原则, 修改原有的代码就说明设计不好. 其次是依赖倒转原则, 我们应该让程序都依赖抽象, 而不是互相依赖. OK, 我去改改, 应该不难的.”

14.3 解耦实践一

     半小时后, 小菜给出了第二版
增加了抽象的观察者

1
2
3
4
5
6
7
8
9
10
11
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 updateObserver();
}

增加了两个具体观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 看股票的同事
public class StockObserver extends Observer {
public StockObserver(String name, Secretary sub) {
super(name, sub);
}
@Override
public void updateObserver() {
System.out.println(sub.getAction() + name + "关闭股票行情, 继续工作!");
}
}
// 看 NBA 的同事
public class NBAObserver extends Observer {
public NBAObserver(String name, Secretary sub) {
super(name, sub);
}
@Override
public void updateObserver() {
System.out.println(sub.getAction() + name + "关闭NBA直播, 继续工作!");
}
}

     “这里让两个观察者去继承 ‘抽象观察者’, 对于 ‘UpdateObserver(更新)’ 的方法做重写操作.”
     “下面是前台秘书类的编写, 把所有的与具体观察者耦合的地方都改成了 ‘抽象观察者’ .”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Secretary {
// 同事列表
private List<Observer> observers = new ArrayList<>();
private String action;
// 增加
public void attach(Observer observer) {
// 针对抽象编程, 减少与具体类的耦合
observers.add(observer);
}
public void detach(Observer observer) {
// 针对抽象编程, 减少与具体类的耦合
observers.remove(observer);
}
// 通知
public void notifyObservers() {
for (Observer o :
observers) {
o.updateObserver();
}
}
// 前台状态
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}

     客户端代码同前面的一样.
     “小菜, 你这样写只完成了一半.”
     “为什么, 我不是已经增加了一个抽象的观察者了吗?”
     “你小子, 考虑问题为什么就不能全面一点呢, 你仔细看看, 在具体观察者中, 有没有与具体的类耦合的?”
     “恩?这里有么?哦!我明白了, 你的意思是 ‘前台秘书’ 是一个具体的类, 也应该抽象出来.”
     “对呀, 你想想看, 你们公司最后一次, 你们老板回来, 前台来不及电话了, 于是通知大家的任务变成谁来做?”
     “是老板, 对的, 其实老板也好, 前台也好, 都是具体的通知者, 这里观察这也不应该以来具体的实现, 而是一个抽象的通知者.”
     “另外, 就算是你们的前台, 如果某一个同事和他闹矛盾, 她生气了, 于是不再通知这件事情, 此是, 他是不是应该把这个对象从她加入的观察者列表中删除?”
     “这个容易, 调用 ‘detach’ 方法将其减去就可以了.”
     “好的, 再去写写看.”

14.4 解耦实践二

     半小时后, 小菜又给出了第三版.
     增加了通知者的抽象接口.

1
2
3
4
5
6
7
8
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObserver();
String getSubjectState();
void setSubjectState();
}

     具体的通知者类可能是前台, 也可能是老板, 他们也许会有各自的一些方法, 但对于通知者来说, 他们是一样的, 所以他们都会去实现这个接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Boss implements Subject {
private List<Observer> observers = new ArrayList<>();
private String action;
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObserver() {
for (Observer o : observers) {
o.updateObserver();
}
}
@Override
public String getSubjectState() {
return action;
}
@Override
public void setSubjectState(String action) {
this.action = action;
}
}

     前台秘书类与老板类类似, 略.
     对于具体的观察者, 需要更改的地方就是把与 ‘前台’ 耦合的地方都改成针对的通知者.

1
2
3
4
5
6
7
8
9
10
11
public class StockObserver extends Observer {
public StockObserver(String name, Subject sub) {
super(name, sub);
}
@Override
public void updateObserver() {
System.out.println(sub.getSubjectState() + name + "关闭股票行情, 继续工作!");
}
}

     客户端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
Subject huhansan = new Boss();
// 看股票的同事
Observer tongshi1 = new StockObserver("魏观姹", huhansan);
// 看NBA 的同事
Observer tongshi2 = new NBAObserver("易管查", huhansan);
huhansan.attach(tongshi1);
huhansan.attach(tongshi2);
huhansan.detach(tongshi1);
// 老板回来
huhansan.setSubjectState("我胡汉三回来了 ");
// 发出通知
huhansan.notifyObserver();
}

     运行结果

1
我胡汉三回来了易管查关闭NBA直播, 继续工作!

     “由于 ‘魏观姹’ 么有被通知到, 所以他被当场 ‘抓获’, 下场很惨.” 小菜说道, “现在我做到了两者都不耦合了.”
     “写得好, 把结构图画出来看看.”
     “这不难.”
     小菜画出了代码的结构图.



     “哈, 小菜非常好, 你已经把观察者模式的计划都写出来了, 现在我们看看什么叫观察者模式.”

14.5 观察者模式(Observer Pattern)

     观察者模式又叫做发布—订阅模式(Publish/Subscribe) 模式.

观察者模式定义了一种一对多的依赖关系, 让多个观察者的对象同时监听某一个主题对象. 这个主题对象在在状态发生变化是, 会通知所有的观察者对象, 让他们能够自动更新自己.[DP]



     Subject 类, 可翻译为主题或者抽象通知者, 一般用一个抽象类或者一个借口实现. 它把所有对观察者对象的引用保存在一个聚集里, 每个主题都可以有任何数量的观察者. 抽象主题提供一个借口, 可以增加和删除观察者对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Subject {
private List<Observer> observers = new ArrayList<>();
// 增加观察者
public void attach(Observer observer) {
observers.add(observer);
}
// 移除观察者
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (Observer o : observers) {
o.update();
}
}
}

     Observer 类, 抽象观察者, 为所有的具体观察者定义一个接口, 在得到主题的通知时更新自己, 这个接口叫做更新接口. 抽象观察者一般用一个抽象类或者一个接口实现. 更新接口通常包含一个update() 方法, 这个方法叫做更新方法.

1
2
3
public abstract class Observer {
public abstract void update();
}

     ConcreteSubject 类, 叫做具体主题或者具体通知者, 将有关状态存入具体观察者对象; 再具体主题的内部状态改变时, 给所有登记过的观察者发出通知. 具体主题角色通常用一个具体子类实现.

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcreteSubject extends Subject {
private String subjectState;
// 具体发布者/被观察者的状态
public String getSubjectState() {
return subjectState;
}
public void setSubjectState(String subjectState) {
this.subjectState = subjectState;
}
}

     ConcreteObserver 类, 具体观察者, 实现抽象观察者角色所要求的更新接口, 以便本身的状态与主体的状态相协调. 具体观察者角色可以保存一个指向具体主题的引用. 具体观察者角色通常用一个具体子类实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
@Override
public void update() {
observerState = subject.getSubjectState();
System.out.println("观察者" + name + "的状态是" + observerState);
}
public String getObserverState() {
return observerState;
}
public void setObserverState(String observerState) {
this.observerState = observerState;
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
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.notifyObservers();
}

     结果显示

1
2
3
观察者X的状态是ABC
观察者Y的状态是ABC
观察者Z的状态是ABC

14. 6 观察者模式的特点

     “用观察者模式的动机是什么?” 小菜问道.
     “问得好, 将一个系统分隔成相互协作的一系列类有一个很不好的副作用, 那就是需要维护相关对象的一致性. 我们不希望为了维持一致性而使各类紧密耦合, 这样会给维护, 扩展, 重用带来不便[DP]. 而观察者模式的主要对象是 Subject 主题和观察者 Observer, 一个 Subject 可以有任意数目的依赖他的 Observer, 一旦 Subject 的状态发生了改变, 所有的 Observer 都会得到通知. Subject 发出通知时并不需要知道谁是他的观察者, 也就是说, 具体的观察这是谁, 他跟不就不需要知道. 而任何一个观察者也不需要知道其他具体观察者的存在.”
     “什么时候考虑使用观察者模式呢?”
     “你说什么时候应当使用?” 大鸟反问道.
     “当一个对象的改变需要同时改变其他对象的时候
     “补充一下, 而且他不知道有多少具体的对象需要改变时, 应该考虑使用观察者模式. 还有吗?”
     “我感觉当一个抽象模型有两个方面, 其中一方面依赖于另一方面, 这时用观察者模式可以使他们封装在两个独立的对象中以便独立的改变和复用.”
     “非常好, 总的来讲, 观察者模式的工作就是在解耦合. 让耦合的双方都依赖于抽象, 而不是依赖于具体. 从而各自的变化都不会影响另一边的变化.”
     “啊, 这就是依赖倒转原则的最佳实现啊” 小菜感慨道.
     “我问你, 在抽象观察者时, 你的代码用的是抽象类, 为什么不用接口?”
     “因为我觉得两个具体的观察者, 看股票观察者和看NBA 观察者类是相似的, 所以用了抽象类, 这样可以公用一些代码, 用接口只是方法上的实现, 没有什么太大的意义.”
     “那么抽象观察者可不可以用接口来定义?”
     “用接口, 我不知道, 应该没有必要吧.”
     “哈, 那是应为你不知道观察者模式的应用都是什么样的. 现实编程中, 具体的观察者完全可能是风马牛不想接的类, 但是他们都是根据通知者的通知做出来的 update() 的操作, 所以让他们都实现下面这一个接口就可以实现这个想法了.”

1
2
3
interface Observer{
void update();
}

     “嘿, 大鸟说的好, 还是用接口比较好.” 小菜傻笑道, 突然表情一变, “等等, 这里还是有问题, 这些空间要么是 .net 类库, 要么是其他人事先写好的控件, 他如何在能去实现拥有update() 的 Observer 接口呢?”

14.7 观察模式的不足

     大鸟微笑着说: “举个例子给我听听.”
     “这样的例子很好举啊,” 小蔡说, “比如, vs2005, 当你点击运行的时候, 整个界面是这个样子的.”



     “当运行程序后, 除了弹出一个控制台的窗体外, 工具栏发生了变化, 工具箱不见了, ‘错误列表’ 变成了 ‘自动窗口’ 和 ‘命令窗口’ 打开. 仅仅是点击一个 ‘运行’ 按钮, 就发生了这么多的变化, 而且这些变化都涉及到不同的控件.”



     “我觉得没办法让每个空间都去实现一个 Observer 接口, 因为这些空间早就被他们的制造商给封装了.”
     “小菜聪明, 你问到点子上了, 尽管点击 ‘运行’ 按钮时, 确实是在通知相关的空间产生变化, 但是他们是不可能用观察者模式实现.”
     “那怎么办呀?”
     “是呀, 那怎么办呢?”
     “凉拌呗”. 大鸟得意至极.
     “快说, 摆的什么架子”
     “还是回到刚才那个 ‘老板, 前台与同事’的例子, 你看看他有没有什么不足之处.”
     “和刚才说的一样, 尽管使用了依赖倒转原则, 但是’抽象的通知者’ 还是依赖于 ‘抽象的观察者’ . 也就是说, 万一没有了抽象观察者这样的接口, 我们的通知功能就实现不了了. 另外就是, 每一个具体的观察者, 它不一定是 ‘更新’ 方法的调用啊, 就像我刚才说的, 我希望是 ‘工具箱’ 隐藏, ‘自动窗口’ 打开, 这根本就是不同命的方法. 这应该就是不足之处吧.”
     “是呀, 如果通知者和观察者之间根本谁都不知道谁, 有客户端来决定通知谁, 那就好了. 来, 我们把原来的代码改造一下.”

14.8 事件委托的实现

     “看股票观察者” 类和 “看NBA 观察者” 类, 去掉了父类 “抽象观察者”, 所以补上一些代码, 并将 “更新” 方法名改为自己适合的方法名.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 看股票的同事
public class StockObserver {
private String name;
private Subject sub;
public StockObserver(String name, Subject sub) {
this.name = name;
this.sub = sub;
}
public void closeStockMarket() {
System.out.println(sub.getSubjectState() + name + "关闭股票行情, 继续工作!");
}
}
// 看 NBA 的同事
public class NBAObserver {
private String name;
private Subject sub;
public NBAObserver(String name, Subject sub) {
this.name = name;
this.sub = sub;
}
public void clockNBADirectSeeding() {
System.out.println(sub.getSubjectState() + name + "关闭NBA直播, 继续工作!");
}
}

     “现实中就是这样, 方法名本就不相同.” 小菜点头说.
     “抽象通知者” 由于不希望依赖于 “抽象观察者”, 所以 “增加” 和 “减少” 的方法就没有必要了(抽象观察者已经不存在了).

1
2
3
4
5
6
7
// 通知者接口
public interface Subject {
void notifyObserver();
String getSubjectState();
void setSubjectState(String action);
}

     “下面就是如何处理 ‘老板’ 类和 ‘前台’ 类的问题, 他们当中 ‘通知’ 方法有了对 ‘观察者’ 遍历, 所以不可小视. 但是如果在 .net 中, 我们有一个非常好的方式实现这个问题. 它叫…”
     “快说.” 小蔡眼睛瞪大, 盯着大鸟.
     “他叫委托.”
     “哦, 就是委托呀, 我都认真的学过好几次了, 但是还是不太懂, 同学几个也差不多, 反正就是不太明白, 他到底是怎么回事, 如何用.”
     “先别管委托怎么回事, 我们先看看他如何用.”
     在 Java 中, 并不像 .net 那样支持委托这种实现形式, 但是我们可以通过注解和反射实现委托操作, 在android 开发中, 有一个非常好的类库 EventBus 已经帮我们实现了很多功能. 可以直接拿来使用. 参考一下, java中委托的实现机制.

14.9 事件委托的说明

     “现在来解释一下, 委托是什么. 委托就是一种引用方法的类型. 一旦委托分配了方法, 委托与该方法具有完全相同的行为. 委托方法的使用可以像其他任何方法一样, 具有参数返回值.委托可以看做是对函数的抽象, 是函数的 ‘类’ , 委托的实例将代表一个具体的函数.
     ….
     “对了, 就是这个意思, 我刚才不是说, 一档委托分配了方法, 委托将于该方法具有完全相同的行为. 而且, 一个委托可以打在多个方法, 所有的方法依次唤起. 更重要的是, 它可以使得委托对象所搭载的方法并不需要属于同一个类. 你还不明白?”
     “啊, 我明白了, 我明白了,” 小菜连笑开了花, “这样就使得, 本来是在 ‘老板’ 类中的增加和减少的抽象观察者集合以及通知时遍历的抽象观察者都不必要了. 转到客户端来让委托搭载多个房发,这就解决了本来与抽象观察者耦合的问题.”
     “但是委托也是有前提的, 那就是委托对象所搭载的方法必须有相同的原型和形式, 也就是相同的参数列表和返回值类型.
     “如果参数列表都不相同那还瞎掺和啥.” 小菜惊叹道, “太强了, 当时那些牛人是怎么设计出这些东西的, 本来观察者模式已经把依赖倒转原则做得非常好了, 现在看来, 委托和事件岂不是更加优秀, 解决问题岂不是更加优雅.”
     “注意, 是先有观察者模式, 再有委托技术的, 再说他们各有优缺点, 你不妨去看看官方文档.”

~感谢捧场,您的支持将鼓励我继续创作~