设计模式 观察者模式

一、定义

关于观察者模式的定义,设计模式书中肯定都有,我就直接引用了:

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。 –摘自《Head First 设计模式》

我们通常把有状态的对象称为主题,收到主题通知的依赖者对象称为观察者。主题和观察者定义了一对多的关系,观察者依赖于主题,只要主题状态一有变化,观察者就会被通知。听起来有点不直观,别急,慢慢往下看,之后再回头慢慢咀嚼。

二、需求方案1

现在打算要研发一款智能彩票机,彩民可以自由向其注册或取消注册,当彩票机的状态改变后,每个注册过的彩民都会收到彩票机传来的通知。

在接到需求之后,捋顺下我们就能知道智能彩票机相当于我们所说的主题,彩民相当于我们的观察者,所以我们先创建一个主题接口类:

/**
 * 这是一个智能彩票机(即主题),用户只要向其注册,就可以收到彩票机发送的彩票信息
 */
public interface ISubject {

    //智能彩票机给彩民用户提供的注册和移除方法
    void registerObserver(IObserver o);
    void removeObserver(IObserver o);
    //智能彩票机给用户发送“彩票信息变化通知”
    void notifyLottery();
}

为什么要使用接口,而不是直接使用具体的主题类,因为不想主题与观察者过分耦合,要努力使对象之间的互相依赖降到最低,这样才能够应付变化,建立有弹性的OO系统

这是一个彩民接口即观察者接口,这个接口只有一个updateLottery(Lottery lottery)方法,当主题的状态改变时它就会被调用。

/**
 * 这是需要获取彩票信息的彩民,即观察者
 */
public interface IObserver {

    //当知道彩票信息更新后的处理方法
    void updateLottery(Lottery lottery);
}

所有的观察者都必须实现该接口,关于观察者的一切,主题只知道观察者实现了当前接口即IObserver,主题不需要知道观察者的具体类是谁、做了些什么或其他任何细节,这就使主题和观察者之间的依赖程度非常低。

这是一个具体的智能彩票机主题类,一个具体的主题总是实现主题接口,除了注册和取消注册方法之外,具体主题还实现了notifyLottery()方法,此方法用于在状态改变时更新所有当前观察者,即彩票信息改变时,将彩票的当前信息通知给彩民。

/**
 * 这是智能彩票机的类
 */
public class LotteryData implements ISubject{

    //持有彩民(观察者)的类
    private ArrayList<IObserver> list = new ArrayList<>();
    //彩票类
    private Lottery lottery;

    @Override
    public void registerObserver(IObserver o) {
        list.add(o);
    }

    @Override
    public void removeObserver(IObserver o) {
        int index = list.indexOf(o);
        if(index != -1){
            list.remove(index);
        }
    }

    @Override
    public void notifyLottery() {
        for(IObserver o : list){
            o.updateLottery(lottery);
        }
    }

    /**
     * 智能彩票机开始摇号
     * 这里模拟5s为1天的情况,每5s彩票状态改变一次
     */
    public void beginWork() {
        Timer timer = new Timer();
        //每5s刷新一下中奖号码
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if(lottery == null){
                    lottery = new Lottery();
                }
                //添加日期
                lottery.setDate(new Date());
                //添加中奖数字,这里测试只有五位数了
                lottery.setWinningCount(new Random().nextInt(90000)+10000);
                //彩票状态改变,通知自己的所有依赖者进行更新
                notifyLottery();
            }
        }, 0, 5000);
    }
}

当彩票状态改变时,我们将Lottery数据直接推(push)给了观察者,但是有的观察者可能只需要一点点数据(如只需要获奖数字不需要时间),并不想被强迫的收到所有数据。这时我们可以考虑让观察者自己从主题中拉(pull)数据,主题只需要提供公开的get方法即可。

这是彩票的实体类,包括彩票的所属日期和当前中奖号码,可以根据需要随意增添。

/**
 * 这个是彩票类
 */
public class Lottery {

    //彩票的日期
    private Date date;
    //彩票的获奖数字
    private int winningCount;

    public Date getDate() {
        return date;
    }
    public void setDate(Date date) {
        this.date = date;
    }
    public int getWinningCount() {
        return winningCount;
    }
    public void setWinningCount(int winningCount) {
        this.winningCount = winningCount;
    }
}

这是具体的观察者彩民1号,观察者必须实现IObserver接口和注册具体主题,以便接收更新。

/**
 * 这是彩民1号
 * @author zmj
 */
public class LotteryBuyerOne implements IObserver{

    public LotteryBuyerOne(ISubject s) {
        //注册智能彩票机
        s.registerObserver(this);
    }

    @Override
    public void updateLottery(Lottery lottery) {
        System.out.println("我是彩民1号  彩票日期:"+lottery.getDate()+" 中奖号码为:"+lottery.getWinningCount());
    }
}

这是具体的观察者彩民2号

/**
 * 这是彩民2号
 */
public class LotteryBuyerTwo implements IObserver{

    public LotteryBuyerTwo(ISubject s) {
        //注册智能彩票机
        s.registerObserver(this);
    }

    @Override
    public void updateLottery(Lottery lottery) {
        System.out.println("我是彩民2号   彩票日期:"+lottery.getDate()+" 中奖号码为:"+lottery.getWinningCount());
    }
}

根据需要我们可以随意添加观察者,因为观察者和主题之间是松耦合的,所以我们改变观察者或者主题其中一方,并不会影响另一方。

测试一下我们的这个设计吧

/**
 * 测试类
 * @author zmj
 */
public class ObserverPatternTest {

    public static void main(String[] args) {
        //声明一个智能彩票机(主题)
        final LotteryData subject = new LotteryData();
        //往智能彩票机内注册彩民用户
        final LotteryBuyerOne loOne = new LotteryBuyerOne(subject);
        new LotteryBuyerTwo(subject);
        //启动智能彩票机
        subject.beginWork();
        final Timer timer = new Timer();
        //6s后彩民1号,因为总中不了奖失去了兴趣,取消注册了
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                subject.removeObserver(loOne);
                timer.cancel();
            }
        }, 6000);
    }
}

最终的运行结果

这里写图片描述

三、需求方案2

除了我们自己实现一整套观察者模式,java还提供了内置的观察者模式。java.util包(package)内包含最基本的Observer接口和Observable类,这和我们的Observer接口和Subject接口很相似。同样的需求我们用内置观察者模式看下:

这是一个具体的主题类,因为Observable是个具体类而不是接口,所以在扩展性上不灵活,限制了Observable的复用潜力。

/**
 * 这是智能彩票机的类,负责逻辑的处理
 */
public class LotteryData extends Observable{

    private Lottery lottery;

    /**
     * 智能彩票机开始摇号
     * 这里模拟5s为1天的情况,每5s彩票状态改变一次
     */
    public void beginWork() {
        Timer timer = new Timer();
        //每5s刷新一下中奖号码
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if(lottery == null){
                    lottery = new Lottery();
                }
                //添加日期
                lottery.setDate(new Date());
                //添加中奖数字,这里测试只有五位数了
                lottery.setWinningCount(new Random().nextInt(90000)+10000);
                //彩票状态改变,通知自己的所有依赖者进行更新
                updata();
            }
        }, 0, 5000);
    }

    public Date getDate() {
        return lottery.getDate();
    }

    public int getWinningCount() {
        return lottery.getWinningCount();
    }

    private void updata() {
        setChanged();
        notifyObservers(this);
    }
}

Observable为我们提供了notifyObservers()方法和notifyObservers(Object arg)方法,所以如果你想推(push)数据给观察者,直接可以把数据对象传递给一个参数的更新方法,而如果你想让观察者拉(pull)数据,只需要调用无参数更新方法,同时提供公开的get方法即可。

这是具体的观察者彩民1号

/**
 * 这是彩民1号,通过向智能彩票机注册,当彩票状态发生改变获得通知
 */
public class LotteryBuyerOne implements Observer{

    public LotteryBuyerOne(Observable observable) {
        observable.addObserver(this);
    }
    @Override
    public void update(Observable o, Object arg) {
        //当彩票状态改变的时候,彩民需要获得通知更新
        if(o != null && o instanceof LotteryData){
            LotteryData lotteryData = (LotteryData)o;
            System.out.println("我是彩民1号  彩票日期:"+lotteryData.getDate()+" 中奖号码为:"+lotteryData.getWinningCount());
        }
    }
}

这是具体的彩民2号

/**
 * 这是彩民2号
 */
public class LotteryBuyerTwo implements Observer{

    public LotteryBuyerTwo(Observable observable) {
        observable.addObserver(this);
    }
    @Override
    public void update(Observable o, Object arg) {
        //当彩票状态改变的时候,彩民需要获得通知更新
        if(o != null && o instanceof LotteryData){
            LotteryData lotteryData = (LotteryData)o;
            System.out.println("我是彩民2号  彩票日期:"+lotteryData.getDate()+" 中奖号码为:"+lotteryData.getWinningCount());
        }
    }
}

这是彩票的实体类

/**
 * 这个是彩票类
 */
public class Lottery {

    //彩票的日期
    private Date date;
    //彩票的获奖数字
    private int winningCount;

    public Date getDate() {
        return date;
    }
    public void setDate(Date date) {
        this.date = date;
    }
    public int getWinningCount() {
        return winningCount;
    }
    public void setWinningCount(int winningCount) {
        this.winningCount = winningCount;
    }
}

测试一下我们的这个设计吧


/**
 * 根据java内置的观察者模式
 */
public class BuiltInObserverPatternTest {

    public static void main(String[] args) {
        //声明一个智能彩票机
        final LotteryData lotteryData = new LotteryData();
        //声明彩民1号和2号,并向彩票注册
        final LotteryBuyerOne lBuyerOne = new LotteryBuyerOne(lotteryData);
        new LotteryBuyerTwo(lotteryData);
        //启动智能彩票机
        lotteryData.beginWork();
        final Timer timer = new Timer();
        //6s后彩民1号,因为总中不了奖失去了兴趣,取消注册了
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                lotteryData.deleteObserver(lBuyerOne);
                timer.cancel();
            }
        }, 6000);
    }
}

最终运行结果:

这里写图片描述

根据结果我们知道,内置的观察者模式,通知的次序不同于我们注册的次序,所以当我们对于通知顺序有要求的时候,不能使用内置的观察者模式。

观察者模式源码下载

java内置观察者模式源码下载

猜你喜欢

转载自blog.csdn.net/MingJieZuo/article/details/79802348