菜鸟谈设计模式----观察者模式

       刚踏进编程的大门,就已经知道两道菜鸟很难逾越的大门:算法和设计模式。算法得看是哪个领域,用于解决什么样的问题,越是复杂的问题,算法自然就会越复杂。至于设计模式,道理很浅显,因为它们是编程领域中智慧和经验的结晶,而程序员的天性就是想要更加简单的解决问题。可惜的是,这种经验并不是菜鸟一开始就能学习到的,就像是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对象,然后转化为对该对象的方法的调用。

     采取哪种方式比较方便呢,就得看具体的使用环境了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值