菜鸟谈设计模式----观察者模式
刚踏进编程的大门,就已经知道两道菜鸟很难逾越的大门:算法和设计模式。算法得看是哪个领域,用于解决什么样的问题,越是复杂的问题,算法自然就会越复杂。至于设计模式,道理很浅显,因为它们是编程领域中智慧和经验的结晶,而程序员的天性就是想要更加简单的解决问题。可惜的是,这种经验并不是菜鸟一开始就能学习到的,就像是RPG游戏中的神级装备,角色本身也必须具有一定的等级和能力才能使用。但所有大的东西都是从小的方面积累起来,多思考,多尝试,练怪练多了,等级自然就会上去了。
观察者模式的出发意图很简单:定义对象间的一对多依赖,这样当一个对象的改变状态时,它的所有依赖者都会收到通知并自动更新。
依赖是面向对象编程中对象间的一种重要关系。我们需要对象依赖,但必须尽量减少对象之间的依赖程度。这里并不适合详谈这个话题,因为针对这个话题就有很多设计模型了。观察者模式的意图一目了然:多个对象拥有同一个对象的引用,当这个对象的状态改变时,希望这些对象都能收到消息并且自动更新其拥有的该对象的状态。这就像是一个广播,源源不断的向订阅该节目的客户发送节目消息。
观察者模式的重点就是被广播的对象,我们称为主题。主题首先必须是一个具有状态的对象。所谓的状态,在面向对象编程中,就是指它封装的数据。主题要想广播给观察者们,必须具有以下几个条件:
1.允许观察者们订阅该主题或者取消订阅该主题;
2.主题的任何改变都必须确保及时发送给订阅该主题的所有观察者们。
Java对观察者模式有内置的支持,当然,我们也可以自己实现观察者模式。
先从一个简单的例子说起。
假设我们现在是在开发军用机器人,就像高达。我们现在有一批试验机,想要测试一下它们对各种武器的反应。这里的主题就是各种武器,我们可以这样子:
public class Enemy extends Observable { public Enemy() { } //开始进攻 public void attack() { setChanged(); notifyObservers(); } //改变进攻的方式 public void setAttackMethod(Attack attack) { this.mAttackNumber = attack.getAttackNumber(); this.mMethod = attack.attackMethod(); attack(); } //进攻方法的编号 public int getAttackNumber() { return mAttackNumber; } //进攻方式的说明 public String getMethod() { return mMethod; } private int mAttackNumber = 0;; private String mMethod; }
这是一个可以使用各种武器的敌人,每种进攻方式都有自己的编号和说明,方便我们的试验机识别该进攻方式并且做出相应的应对。
任何主题都必须继承自Observable类,这是一个让人很好奇的东西,为什么选择继承自一个基类呢?如果可以,选择实现一个接口,我们的扩展性就会更高,尤其是当我们的主题本身就已经是某个类的子类的时候,这也是为什么我们有时候需要自己实现观察者模式的主要原因。
值得注意的是,attack()中有两个关键的方法:setChanged()和notifyObservers()。
回到我们之前提出的条件:主题的任何改变都必须确保及时发送给订阅该主题的所有观察者们。实现这样的条件,我们可以分为两步:
(1)通过setChanged()标记主题状态已经改变。该方法可以让我们选择什么时候通知观察者们。我们可以先设置一个布尔值作为标记,默认为false,当我们认为需要通知观察者们的时候,就可以将该标记设为true,然后调用setChanged();
(2)notifyObservers()顾名思义,就是通知观察者们。这里有个问题,就是观察者们是如何接收该通知并且得到改变的状态呢?观察者们有两种接收通知的方式:主题直接推送数据或者把数据当做对象传递给带参数的notifyObservers(Object arg)方法。到底该采用哪种方式呢?其实,不带参数的notifyObservers()需要观察者们直接向主题要数据,所以,我们可以看到,在我的代码中,有两个getter方法,并且在Robot类中也有它们相应的调用。如果采用带参数的方式,首先得说明,那个arg就是Robot中update()中的arg!我们可以直接使用arg(前提当然是先强制转化为相应的数据类型)。
至于我为什么采用不带参数的方式,就是因为我需要推送的数据有两个:进攻编号和进攻说明。当然,我可以采用一个字典来解决这个问题,但主要就是我的设计并不好,进攻方式的说明完全可以放在Robot中,但作为一个简单的例子,还请见谅,但也充分说明了一个问题:需要推送的数据一旦超过一个以上,我们就得考虑使用不带参数的方式,并且主题必须提供相应的getter方法。
接下来就是攻击方式了:
public interface Attack { String attackMethod(); int getAttackNumber(); } public class RocketAttack implements Attack { @Override public String attackMethod() { return mMethod; } @Override public int getAttackNumber() { return 2; } private String mMethod = "我正在用火箭炮攻击你"; } public class FireAttack implements Attack { @Override public String attackMethod() { return mMethod; } @Override public int getAttackNumber() { return 0; } private String mMethod = "我正在用火焰攻击你"; } public class LightAttack implements Attack { @Override public String attackMethod() { return mMethod; } @Override public int getAttackNumber() { return 1; } private String mMethod = "我正在用光束军刀攻击你"; }
接下来就是实现我们的观察者们了:
public class Robot implements Observer { private Observable mObservable; private int number = 0; private String mVersion; public Robot(Observable observable, String version) { this.mObservable = observable; this.mVersion = version; mObservable.addObserver(this); } //实现状态的自动更新 @Override public void update(Observable o, Object arg) { if (o instanceof Enemy) { Enemy enemy = (Enemy) o; this.number = enemy.getAttackNumber(); if (number == 0) { System.out.println("敌人:" + mVersion + "," + enemy.getMethod() + "\n" + mVersion + ":我用水来防御" + "\n"); } else if (number == 1) { System.out.println("敌人:" + mVersion + "," + enemy.getMethod() + "\n" + mVersion + ":我用光盾来防御" + "\n"); } else if (number == 2) { System.out.println("敌人:" + mVersion + "," + enemy.getMethod() + "\n" + mVersion + ":我马上逃跑!" + "\n"); } } } }
该类的重点就是我们拥有一个主题的引用,并且向该主题注册了自己。
现在我们可以开始测试了!
public class RobotTest { public static void main(String[] args) { Attack[] randomAttacks = { new FireAttack(), new LightAttack(), new RocketAttack(), }; Enemy enemy = new Enemy(); Robot robot1 = new Robot(enemy, "机器人1号"); Robot robot2 = new Robot(enemy, "机器人2号"); int randomNumber = 0; for (int i = 0; i < 5; i++) { randomNumber = new Random().nextInt(3); enemy.setAttackMethod(randomAttacks[randomNumber]); }
} }
敌人会根据随机数采取随机的攻击方式,我们的Robot必须能够根据不同的攻击方式采取不同的措施,测试结果如下:
(1)敌人:机器人2号,我正在用火箭炮攻击你 机器人2号:我马上逃跑!
敌人:机器人1号,我正在用火箭炮攻击你 机器人1号:我马上逃跑!
(2)敌人:机器人2号,我正在用火焰攻击你 机器人2号:我用水来防御
敌人:机器人1号,我正在用火焰攻击你 机器人1号:我用水来防御
(3)敌人:机器人2号,我正在用光束军刀攻击你 机器人2号:我用光盾来防御
敌人:机器人1号,我正在用光束军刀攻击你 机器人1号:我用光盾来防御
(4)敌人:机器人2号,我正在用光束军刀攻击你 机器人2号:我用光盾来防御
敌人:机器人1号,我正在用光束军刀攻击你 机器人1号:我用光盾来防御
(5)敌人:机器人2号,我正在用火箭炮攻击你 机器人2号:我马上逃跑!
敌人:机器人1号,我正在用火箭炮攻击你 机器人1号:我马上逃跑!
嗯,测试的结果还是不错的,这批机器人现在可以派往前线作战了!
还记得开头的第一个条件吗?我们还需要实现观察者对主题订阅的取消。这样的动作可以通过一个方法来实现:
enemy.deleteObserver(robot2);
这样就可以取消robot2的测试了。
正如其他模式一样,观察者模式可以帮助我们减少对象之间的依赖。主题只知道观察者实现了一个接口(Observer),根本就不需要知道它到底是谁,上面的例子同样可以适用于其他对象,只要它实现了Observer接口并且订阅了该主题。而且使用观察者模式,我们只需要一个主题来保存数据,而不是多个对象同时拥有该数据。
Java对观察者模式的内置支持帮了我们很大的忙,但是它的局限也是非常明显的:setChanged()竟然是protected的!这样就强制要求我们主题必须继承自Observable!正如上面指出的,主题很可能是另一个基类的子类,这时我们该怎么做呢?方法有两种:自己实现一个Observable接口或者是拥有一个Observable对象,然后转化为对该对象的方法的调用。
采取哪种方式比较方便呢,就得看具体的使用环境了。