设计模式--观察者模式

说到观察者模式-从字面意思的理解就是某些个实物观察着另一个实物,当被观察的那个实物发生变化时,那些观察它的实物会收到通知或者会相应的发生变化。

Alt text

生活中的例子也是到处都是啊,比如平常生活中经常订阅一些报纸来阅读,有的人可能喜欢体育,那么就订阅体育竞技相关的报纸;有的人可能喜欢美食,那么就订阅美食相关的报纸;还比如手机手机上面的天气预报显示,我们可以只显示查看我们感兴趣的城市,那么这也是订阅形式的一种,所以我们也可以把观察者模式理解成为订-阅模式,就相当于上面举的例子场景一样。

一、模拟报纸订阅来实现观察者模式

先说明一下我们的需求:

  • 一般报社都提供【报纸订阅服务】,不管你通过什么方式订阅都是可以的

  • 报纸订阅服务一般包括【订阅】、【取消】、【订阅回执】、【新报纸发布】、【通知订阅者】等等功能

  • 订阅者除了可以使用报纸订阅的相关服务外,每一个订阅者还需要提供一个【被通知】的服务,不然新报纸发布、订阅成功后怎么收到通知呢?就好比去银行存钱,存钱成功后会收到一个回执单一样。

  • 当订阅者订阅某一类型报纸后,可以收到订阅回执提示;当有新类型的报纸发布出品后,需要通知相关订阅者,这有点类似个性化推荐,比如某一个订阅者订阅了美食相关的报纸,那么当我们出版了《食材天下》新报纸时需要通知美食类的报纸订阅者,因为美食和食材是有关联的嘛,这就是个性化推荐。

由于一个报社不可能只出版一个类型的报纸,所以报纸订阅服务应该是通用的吧,所以我们定义一个报纸订阅接口,然后让不同类型的报纸订阅服务都实现这个通用的订阅接口。

由于每一个订阅者都需要提供一个被通知的方法,所以我们就独立定义一个订阅者接口,并且在接口中定义一个被通知的方法,后续的每一个订阅者都实现这个通用的订阅者接口。

1. 订阅者通用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 订阅者通用接口,后续每一个订阅者都需要实现该接口
* @author mapingsheng
*
*/
public interface SubscriberService {
/**
* 被通知的公共方法
* @param notice -通知的提示语
*/
public void notice(String notice);
}

2. 订阅者-张三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 张三订阅者,实现了通用的订阅者接口
* @author mapingsheng
*
*/
public class SubscriberZhangsan implements SubscriberService {
/**
* 张三被通知的方法
*/
@Override
public void notice(String notice) {
System.out.println("zhangsan收到通知: "+notice);
}
}

3. 订阅者-王五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 王五订阅者,实现了通用的订阅者接口
* @author mapingsheng
*/
public class SubscriberWangwu implements SubscriberService {
/**
* 王五被通知的方法
*/
@Override
public void notice(String notice) {
System.out.println("wangwu收到通知: "+notice);
}
}

4. 通用报纸订阅服务接口

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
34
35
36
**
* 报纸订阅服务接口,任何一种报纸发布出品都需要实现该接口
* @author mapingsheng
*/
public interface NewsPaperService {
/**
* 注册订阅者服务方法--订阅
* @param subscriberService 订阅者接口
*/
public void register(SubscriberService subscriberService);
/**
* 移除订阅者服务方法--取消订阅
* @param subscriberService 订阅者接口
*/
public void remove(SubscriberService subscriberService);
/**
* 订阅成功后通知订阅者的方法--订阅回执
* @param subscriberService
*/
public void noticeForRegister(SubscriberService subscriberService);
/**
* 新报纸发布出品方法
* @param name 报纸类型名称
*/
public void newsPaperPublish(String paperName);
/**
* 单个通知订阅者方法,主要针对新报纸发布时调用
* @param name
*/
public void noticeForNewPaperPublish();
}

5. 《美食天下》-报纸订阅服务

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 美食天下-报纸订阅服务类
* @author mapingsheng
*/
public class NewsPaperFoodServiceImpl implements NewsPaperService {
private String pageName;//报纸类型
/**
* 订阅者列表,凡是通过register方法订阅的订阅者都会被加入集合中
*/
static List<SubscriberService> subscriberList = new LinkedList<SubscriberService>();
/**
* 订阅方法
*/
public void register(SubscriberService subscriberService){
subscriberList.add(subscriberService);//将订阅者添加到订阅成员集合中
noticeForRegister(subscriberService);//调用订阅回执服务方法通知该订阅者
}
/**
* 取消订阅
*/
public void remove(SubscriberService subscriberService){
/**
* 先查找改订阅者是否订阅过,如果订阅过则移除该订阅者
*/
int index = subscriberList.indexOf(subscriberService);
if(index!=-1){
subscriberList.remove(index);
}
}
/**
* 新报纸发布出品-通知订阅集合列表中所有的订阅者
*/
public void noticeForNewPaperPublish(){
for(int i=0;i<subscriberList.size();i++){
SubscriberService subscriberService = subscriberList.get(i);
String notice = "【新报纸发布】您好,我报社最近出版了 《"+pageName+"》 欢迎订阅";
subscriberService.notice(notice); //调用订阅者提供的“被通知”方法,这里是通过接口调用,可以提高兼容性(不同的订阅者)
}
}
/**
* 订阅回执-当订阅者订阅成功后调用订阅者的被通知方法去通知订阅者
*/
@Override
public void noticeForRegister(SubscriberService subscriberService) {
int i = subscriberList.indexOf(subscriberService);
String notice = "【订阅回执】您好,恭喜您订阅《天下美食》专栏报纸,一共 52 元,截止目前一共有 "+subscriberList.size()+" 人订阅";
subscriberService.notice(notice);
}
/**
* 新报纸发布
*/
@Override
public void newsPaperPublish(String name){
this.pageName = name; //设置报纸名称
noticeForNewPaperPublish(); //通知该类型报纸相关的订阅列表中的订阅者
}
}

6. 《体育速递》-报纸订阅服务

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 体育速递-报纸订阅服务
* @author mapingsheng
*/
public class NewsPaperSprotServiceImpl implements NewsPaperService {
private String pageName;//报纸类型名称
/**
* 订阅者列表,凡是通过register方法订阅的订阅者都会被加入集合中
*/
static List<SubscriberService> subscriberList = new LinkedList<SubscriberService>();
/**
* 订阅方法
*/
public void register(SubscriberService subscriberService){
subscriberList.add(subscriberService);//将订阅者添加到订阅成员集合中
noticeForRegister(subscriberService);//调用订阅回执服务方法通知该订阅者
}
/**
* 取消订阅
*/
public void remove(SubscriberService subscriberService){
/**
* 先查找改订阅者是否订阅过,如果订阅过则移除该订阅者
*/
int index = subscriberList.indexOf(subscriberService);
if(index!=-1){
subscriberList.remove(index);
}
}
/**
* 新报纸发布出品-通知订阅集合列表中所有的订阅者
*/
public void noticeForNewPaperPublish(){
for(int i=0;i<subscriberList.size();i++){
SubscriberService subscriberService = subscriberList.get(i);
String notice = "【新报纸发布】您好,我报社最近出版了 《"+pageName+"》 欢迎订阅";
subscriberService.notice(notice); //调用订阅者提供的“被通知”方法,这里是通过接口调用,可以提高兼容性(不同的订阅者)
}
}
/**
* 订阅回执-当订阅者订阅成功后调用订阅者的被通知方法去通知订阅者
*/
@Override
public void noticeForRegister(SubscriberService subscriberService) {
int i = subscriberList.indexOf(subscriberService);
String notice = "【订阅回执】您好,恭喜您订阅《体育速递》专栏报纸,一共 30 元,截止目前一共有 "+subscriberList.size()+" 人订阅";
subscriberService.notice(notice);
}
/**
* 新报纸发布
*/
@Override
public void newsPaperPublish(String name){
this.pageName = name; //设置报纸名称
noticeForNewPaperPublish(); //通知该类型报纸相关的订阅列表中的订阅者
}
}

7. 测试–订阅者调用订阅服务、并且收到订阅回执

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
SubscriberZhangsan zhangsan = new SubscriberZhangsan();//实例化-张三订阅者
SubscriberWangwu wangwu = new SubscriberWangwu();//实例化-王五订阅者
//实例化-《美食天下》报纸订阅服务
NewsPaperService foodService = new NewsPaperFoodServiceImpl();
foodService.register(wangwu);//王五订阅美食天下报纸
foodService.register(zhangsan);//张三订阅美食天下报纸
//实例化-《体育速递》报纸订阅服务
NewsPaperService sportService = new NewsPaperSprotServiceImpl();
sportService.register(zhangsan);//张三订阅体育速递报纸
}
}

以上代码运行结果:

wangwu收到通知: 【订阅回执】您好,恭喜您订阅《天下美食》专栏报纸,一共 52 元,截止目前一共有 1 人订阅

zhangsan收到通知: 【订阅回执】您好,恭喜您订阅《天下美食》专栏报纸,一共 52 元,截止目前一共有 2 人订阅

zhangsan收到通知: 【订阅回执】您好,恭喜您订阅《体育速递》专栏报纸,一共 30 元,截止目前一共有 1 人订阅

可以看到张三、王五都订阅了美食天下报纸,所以都收到了【订阅回执】,并且各自订阅时都统计出当前的订阅人数;而仅仅张三订阅了【体育速递】报纸,所以也仅仅张三收到了订阅回执。

8. 测试–订阅者订阅报纸、然后取消订阅、新报纸发布出版并通知相关订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
SubscriberZhangsan zhangsan = new SubscriberZhangsan();//实例化-张三订阅者
SubscriberWangwu wangwu = new SubscriberWangwu();//实例化-王五订阅者
//实例化-《美食天下》报纸订阅服务
NewsPaperService foodService = new NewsPaperFoodServiceImpl();
foodService.register(wangwu); //王五订阅美食天下报纸
foodService.register(zhangsan); //张三订阅美食天下报纸
foodService.remove(wangwu);//王五【取消订阅】了美食天下报纸
//新报纸-《食材中国》发布出版,理论上应该个性化推荐给《美食天下》报纸的订阅者
foodService.newsPaperPublish("食材中国");
}
}

以上代码运行结果:

wangwu收到通知: 【订阅回执】您好,恭喜您订阅《天下美食》专栏报纸,一共 52 元,截止目前一共有 1 人订阅

zhangsan收到通知: 【订阅回执】您好,恭喜您订阅《天下美食》专栏报纸,一共 52 元,截止目前一共有 2 人订阅

zhangsan收到通知: 【新报纸发布】您好,我报社最近出版了 《食材天下》 欢迎订阅

注:首先张三、王五都订阅了美食天下报纸,所以都收到了订阅回执通知;后来由于王五取消订阅了美食天下的报纸,所以下面的新报纸发布出版–《食材中国》发布时,应该通知的美食天下报纸的订阅者中没有王五,仅仅通知了张三

截至目前,我们已经通过观察者模式实现了报纸订阅的场景,要注意的是上面代码中,我们都是面向接口实现的,比如注册订阅者、通知订阅者等等。

1
2
3
4
5
public void register(SubscriberService subscriberService){
subscriberList.add(subscriberService);//将订阅者添加到订阅成员集合中
noticeForRegister(subscriberService);//调用订阅回执服务方法通知该订阅者
}

上述方法中我们传递参数都是通用订阅者接口-SubscriberService;这样的话我们在后续新增其他订阅者后,只要后续的订阅者实现了通用的订阅者接口,那么就可以直接调用订阅服务,不需要做任何代码变化,所以我们需要了解以下设计原则:

找出程序中会变化的方面,然后将其和固定不变的方面相分离—在观察者模式中,会改变的是主题(报纸发布)状态,以及观察者的数目和类型。用这个模式,你可以改变依赖于主题状态的对象,却不必改变主题

针对接口变化,不针对实现编程—主题(报纸订阅)与观察者(订阅者)都使用接口观察者利用主题的接口调用主题接口的方法进行注册,而主题利用观察者通用接口中的被通知方法通知观察者,这样可以让两者之间运行正常,又同时具有松耦合的优点。

多用组合,少用继承—观察者模式利用“组合”将许多观察者组合进主题中,对象之间的这种关系不是通过继承产生的,而是运行时利用组合的方式产生的