这世界没有一件事情是虚空而生的,站在光里,背后就会有阴影,这深夜里一片寂静,是因为你还没有听见声音
——马良《坦白书》
文章目录
- 定义
- 图纸
- 一个例子:在RPG游戏里应对善变的天气
- 定义元素
- Area & Weather
- 给 Area 和 Knight 建立联系
- 善变的天气
- 碎碎念
- 定时器的方案一无是处吗?
- 观察者和中介者
- 可以抽象出来的Subject和Observer
定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖他的对象都得到通知并被自动更新
二十年前如果你想知道明天的天气怎么样,你用不着隔几分钟就到气象台问问有没有最新的情报,而是可以在气象台登记一下你的电话号码。每当有最新的天气预报发布的时候,气象台会自动给所有进行登记过的电话号码发短信(这里面就包括你的)
这种模式其实就是观察者模式,而你就是被记录在册的观察者
图纸
一个例子:在RPG游戏里应对善变的天气
假定现在我们有一个RPG对战游戏,有这样的设定:
- 天气分为:晴天、大雾和下雨
- 玩家可以选择火元素、水元素或者风元素的骑士
- 骑士一定是在某个区域内活动,而区域有对应的天气。每种元素的骑士在不同的天气下会变化自己的属性
定义元素
很显然,天气是区域的一种属性,就像这样:
Area & Weather
/**
* 骑士可以活动的区域
*/
public class Area {
/**
* 当前区域的天气
*/
private Weather weather;
public Area(Weather weather) {
this.weather = weather;
}
public Weather getWeather() {
return weather;
}
public void setWeather(Weather weather) {
this.weather = weather;
}
}
public enum Weather {
sunny,fog,rain
}
骑士也应当有自己的类簇,就像这样:
/**
* 骑士
*/
public class Knight {
/**
* 攻击力
*/
private int attack;
/**
* 生命值
*/
private int healthPoint;
/**
* 骑士名称
*/
private String name;
/**
* 骑士所在区域
*/
private Area area;
public Knight(String name, Area area) {
this.name = name;
setAttack(10);//默认10点攻击力
setHealthPoint(100);//默认100点生命值
setArea(area);
}
public int getAttack() {
return attack;
}
public void setAttack(int attack) {
this.attack = attack;
}
public int getHealthPoint() {
return healthPoint;
}
public void setHealthPoint(int healthPoint) {
this.healthPoint = healthPoint;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Area getArea() {
return area;
}
public void setArea(Area area) {
this.area = area;
}
@Override
public String toString() {
return String.format("%s:攻击力=%s", name, attack);
}
}
/**
* 火属性骑士
*/
public class FireKnight extends Knight {
public FireKnight(Area area) {
super("火属性骑士", area);
}
}
/**
* 风属性骑士
*/
public class WindKnight extends Knight {
public WindKnight(Area area) {
super("风属性骑士", area);
}
}
/**
* 水属性骑士
*/
public class WaterKnight extends Knight {
public WaterKnight(Area area) {
super("水属性骑士", area);
}
}
我们创建了天气的枚举 Weather 用来表示所有当前可能出现的天气,并把 Weather 作为 Area 的内部属性
对于骑士,我们创建 Knight 根类用于存放所有的骑士都有的一些属性,再根据不同的元素分出三个子类
现在我们实现了前两步,至于最后一步,想必我们需要在 Area 和 Knight 之间建立一些联系
给 Area 和 Knight 建立联系
那你会说了,不对啊,Knight 里面有自己当前所处的 Area 的引用,这不就是很好的联系吗?
用 Knight 里面的引用,实现出来的效果是这样的:
/**
* 骑士
*/
public class Knight {
……
public void setArea(Area area) {
this.area = area;
updateByWeather();
}
protected void updateByWeather(){
//不实现,也不强制子类实现她,所以留空
}
/**
* 把属性复原
*/
protected void reset(){
setAttack(10);
}
}
/**
* 火属性骑士
*/
public class FireKnight extends Knight {
public FireKnight(Area area) {
super("火属性骑士", area);
}
@Override
protected void updateByWeather() {
reset();
Weather weather = getArea().getWeather();
if (weather.equals(Weather.sunny)) {
//如果是晴天,攻击力+10
setAttack(getAttack() + 10);
} else if (weather.equals(Weather.rain)) {
//如果是雨天,攻击力减半
setAttack(getAttack() / 2);
}
}
}
/**
* 风属性骑士
*/
public class WindKnight extends Knight {
public WindKnight(Area area) {
super("风属性骑士", area);
}
//什么天气都跟他没关系 所以不需要重写
}
/**
* 水属性骑士
*/
public class WaterKnight extends Knight {
public WaterKnight(Area area) {
super("水属性骑士", area);
}
@Override
protected void updateByWeather() {
reset();
Weather weather = getArea().getWeather();
if (weather.equals(Weather.rain) || weather.equals(Weather.fog)) {
//如果是雾天或者下雨,攻击力翻倍
setAttack(getAttack() * 2);
} else if (weather.equals(Weather.sunny)) {
//如果是晴天,攻击力降为1
setAttack(1);
}
}
}
采用这种方案,我们在 Knight 为自己设定 Area 的时候就读取了天气信息,同时更新自己的属性,使用 updateByWeather 方法
善变的天气
可是问题很快出现了,我们玩这个游戏的时候发现,所有的玩家都会根据即将进入的区域选择合适的骑士,没有人蠢到故意在晴天选水骑士,或者在下雨时选火骑士
所以为了增加可玩性,我们设定了第四点需求:
- 一个区域内的天气不是一成不变的,他会进行随机的变化
想法很好,实践起来却有点麻烦了
根据前面的设计,我们在set Area 的时候变化了自己的属性,之后 Area 里面的 Weather 会如何变化,Knight 是不知道的
怎么让他知道呢?我们有两种方案:
- 在 Knight 里面添加一个定时器,固定时间去查 Area 里面的 Weather 属性,如果出现了变化,更新自己
- 想个办法让 Area 在更新 Weather 的时候去通知 Knight,让 Knight 及时更新
一看就知道后者明显优于前者,那能做到吗?
可以的,就像我们之前注册迭代器一样。我们只需要在 Area 里面维护一个 Knight 列表,然后在 set Weather 的时候通知 Knight 就完事了,就像这样:
/**
* 骑士
*/
public class Knight {
……
public void setArea(Area area) {
//注销
if (this.area != null) {
this.area.removeKnight(this);
}
this.area = area;
//注册
area.addKnight(this);
//第一次执行
updateByWeather();
}
public void update(){
updateByWeather();
}
}
/**
* 骑士可以活动的区域
*/
public class Area {
……
public void setWeather(Weather weather) {
this.weather = weather;
notifyKnight();
}
private final List<Knight> knightList = new ArrayList<>();
public void addKnight(Knight knight){
knightList.add(knight);
}
public void removeKnight(Knight knight){
knightList.remove(knight);
}
public void notifyKnight(){
for (Knight knight : knightList) {
knight.update();
}
}
}
我们让 Area 去维护一个 knightList ,并在 Knight 设定 Area 的时候把自己写到 knightList 里面去。这就实现了一个可以从 Area 发指令给 Knight 的通道。接着,我们需要发送指令的时候,可以通过 notifyKnight 方法通知 knightList 里面所有的 Knight
这样一来,第四点需求得以实现,就像这样:
public static void main(String[] args) {
Area area = new Area(Weather.sunny);
Knight fire = new FireKnight(area);
Knight wind = new WindKnight(area);
Knight water = new WaterKnight(area);
System.out.println("晴天");
System.out.printf("%s \n%s \n%s \n", fire, wind, water);
System.out.println("*************************************************************");
area.setWeather(Weather.rain);
System.out.println("雨天");
System.out.printf("%s \n%s \n%s \n", fire, wind, water);
}
而这正是一个标准的观察者实现
观察者的结构和原理简单到一眼就能望到头,但是这个简单的结构解决了无数个问题。这有点像多线程里面的 生产-消费者模型,也是结构简单但极其实用。也许这就是大道至简吧
碎碎念
定时器的方案一无是处吗?
其实并不是的,上例的情况是因为 Knight 和 Area 可以双向主动向对方发起请求,所以可以用观察者,但是很多时候连接是单向的
比如说 http,这就是个无状态协议,除非用一些比较特殊的手法(比如 WebSocket),否则服务器是没办法主动向客户机发送请求的
这时候如果你有时效性不那么高的类似请求(游戏的时效性要求肯定不允许你用定时器),那么定时器和长连接就是你需要考虑的解决方案了
观察者和中介者
到了行为型模式这一篇,其实有很多模式关注的内容是类似的
比如前面讲过的 职责链(Chain of Responsibility) 和 命令(Command)
职责链和命令都是通过参数化请求,以求实现请求者和处理器之间的解耦
之后还会提到的 状态模式(State) 和 策略模式(Strategy) (状态模式简直就是策略模式水里的倒影)
以及现在要讲的 观察者(Observer) 和 中介者(Mediator)
观察者和中介者都是为对象和对象之间通讯而存在的
这种通讯相当于,对象A执行了某个动作(在面向对象中其实就是某个函数被调用),对象B就要针对这个行为执行自己的动作
这就像自行车的主动轮和从动轮之间的关系
假定我们现在有A和B两个对象,A发出通知,B接收A的通知并执行操作
在这种情况下,如果让A直接调用B,那就意味着他们之间建立紧耦合;如果想要解耦,那么对象之间的通信方式基本上有两种
- 在A和B之间建立一个平台,让A和B都去跟平台打交道而不知道对方的存在,这个平台就是 中介者
- 在A里面,维护一个监听者列表,形成一个 1→N 的关系,这时候我会把B写入A的监听者列表里(这时候建立的是抽象耦合)。当发生某个事件的时候,A会通知所有监听者进行更新(其中就包含B),这时候的B就是 观察者
先说两种方式的共同点,两种做法都可以解除A和B之间的紧耦合。A可以不知道这个通知会被传递到哪里去,可以不知道B的数量,甚至可以不知道B的具体类型
但两种设计模式又各有千秋:
- 中介者 内部的对象没有明确的主次,任何对象都可以通过平台发出信息或对某个信息进行响应
- 观察者 不需要这个平台,subject和observer之间存在明确的主次关系,信息传递的方向也永远是 S u b j e c t → O b s e r v e r Subject → Observer Subject→Observer
这是他们好的一面,而他们的缺点和优点一样明显:
-
中介者 的平台随着所要维护的对象数量增加,需要处理的关联也越来越多,这最终会让中介者平台变成一个庞然大物,所有的关联都集中到一种,最终形成一个错综复杂的线团,把他理清是很痛苦的事情
-
观察者 不需要第三方平台,这是便利,也是缺陷。因为subject和observer都对对方太不了解了,所以在后期维护的时候,如果不了解程序结构的人调用了subject里某个会在observer里产生副作用的方法,程序可能出现一些诡异的行为,而很难发现是哪个观察者的问题。这些诡异的行为包括但不限于:
-
调用一个更新数据的方法,但另一个看似风马牛不相及的视图也被更新了
-
subject通知observer进行操作,但是observer又会调用subject里的行为,到最后形成死循环
那你会说,我又不是傻,为什么要这样写?
现实是这种情况时有发生,因为整个项目的结构里不会只有 subject 和 observer 这两者。也许你通过subject 通知 observer 后,observer 又会去调用其他对象,其他对象又调用其他对象,以此往复,最终跑回 subject 来
-
可以抽象出来的Subject和Observer
你可能发现了,其实所有在观察者模式中发出信息的 变化主体
,或者说 subject
,在维护观察者列表的时候,都需要三个方法:
-
addObserver
增加观察者
-
removeObserver
删除观察者
-
notifyObserver
通知观察者进行更新
在观察者,或者说Observer
里,则需要用于更新的 update
方法
既然有通用的部分,那我们其实就可以把他们抽象出来,就像这样:
/**
* 被观察者 主体
**/
public class Subject {
private final List<Observer> observerList = new ArrayList<>();
public void notifyObserver() {
for (Observer observer : observerList) {
observer.update(this);
}
}
public void addObserver(Observer observer) {
observerList.add(observer);
}
public void removeObserver(Observer observer) {
observerList.remove(observer);
}
}
/**
* 观察者
**/
public interface Observer {
void update(Subject subject);
}
在Java里面,Subject和Observer甚至不需要你自己写,因为在 java.util 里面就有对应的工具类,可以直接继承
虽然说从Java9 开始这玩意就废弃了,我也是写这个的时候才发现
不过值得一提的是,Observer 通常可以作为接口存在,但如果你需要 Subject 帮你维护观察者列表,那么 Subject 至少得是一个抽象类,那就只能用继承
在Java这个单继承的语言里,使用继承要慎重,就比如上例的 Area,就算我有 Subject 工具类,我也不会让 Area 去继承她,我宁可自己写
因为这会破坏整体的语法,区域怎么可能是主体的子类呢?这会让继承我的代码的后辈产生误解,这虽然只是编程风格的问题,但是我坚信细节决定成败,所以该抠的地方还是严谨一点的好
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容