几分钟带你搞懂观察者模式

老李:小张啊,最近忙嘛呢?下班就跑
小张:昨天买了彩票,今天去看下自己是否财务自由了
老李:官网注册个账号,坐等中奖号码通知不香吗?
小张我就是不想加班…,嗯确实香,老李你先把刀放下
老李:哪天心灰意冷了,就注销掉,别辜负了工作给你的热情
小张:…

观察者模式

关于观察者模式的定义,我就直接引用HeadFirst书中的描述了:观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。我们通常把有状态的对象称为主题,收到主题通知的依赖者对象称为观察者。主题和观察者定义了一对多的关系,观察者依赖于主题,只要主题状态一有变化,观察者就会被通知。类图见下:
在这里插入图片描述

我们来用程序语音来描述下,以彩票官网为例,彩民可以自由的向其注册或取消注册,当中奖号码更新后,即官网此状态改变后,每个注册过的彩民都会收到官网传来的通知。这里的官网就相当于我们所说的主题,彩民相当于我们的观察者,我们可先创建一个主题接口类:

/**
 * 这是主题类。
 * 
 * <p>用户只要向其注册,主题状态改变后,
 * 就可以收到官网发送来的彩票信息。
 */
public interface ISubject {
	
	// 给彩民用户提供的注册和移除方法
	void registerObserver(IObserver o);
	void removeObserver(IObserver o);
	// 给用户发送“彩票信息变化通知”
	void notifyLottery();
}

为什么要使用接口,而不是直接使用具体的主题类,因为不想主题与观察者过分耦合,要努力使对象之间的互相依赖降到最低,这样才能够应付变化,建立有弹性的OO系统。 这是一个彩民接口即观察者接口,这个接口只有一个updateLottery(Lottery lottery)方法,当主题的状态改变时它就会被调用。

/**
 * 观察者接口类。
 * 
 * <p>所有的观察者都必须实现该接口,关于观察者的一切
 * 主题只知道观察者实现了当前接口即IObserver
 * 主题不需要知道观察者的具体类是谁、做了些什么或其他任何细节
 * 这就使主题和观察者之间的依赖程度非常低。
 */
public interface IObserver {
	
	// 当知道彩票信息更新后的处理方法
	void updateLottery(Lottery lottery);
}

这是一个具体的主题类,一个具体的主题总是实现主题接口,除了注册和取消注册方法之外,具体主题还实现了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);
		}
	}
	
	/**
	 *  智能彩票机开始摇号。
	 * 
	 *  <p>这里模拟5s为1天的情况,每5s彩票状态改变一次。
	 */
	public void beginWork() {
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				notifyInfo();
			}
		}, 0, 5000);
	}

    private void notifyInfo() {
        if(lottery == null)
		    lottery = new Lottery();
		// 添加日期
		lottery.setDate(new Date());
		// 添加中奖数字,这里测试只有五位数了
		lottery.setWinningCount(new Random().nextInt(90000)+10000);
		// 彩票状态改变,通知自己的所有依赖者进行更新
		notifyLottery();
    }
}

当彩票状态改变时,我们将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接口和注册具体主题,以便接收更新。

/**
 * 具体的观察者
 */
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());
	}
}

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

public class ObserverPatternTest {

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

三、内置观察者模式

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

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

/**
 * 具体的主题
 */
public class LotteryData extends Observable{
	// 彩票信息类
	private Lottery lottery;
	
	/**
	 * 这里模拟每5s彩票状态改变一次
	 */
	public void beginWork() {
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				notifyInfo();
			}
		}, 0, 5000);
	}
 
    public void notifyInfo() {
        if(lottery == null)
			lottery = new Lottery();
		// 添加日期
		lottery.setDate(new Date());
		// 添加中奖数字,这里测试只有五位数了
		lottery.setWinningCount(new Random().nextInt(90000)+10000);
		// 彩票状态改变,通知自己的所有依赖者进行更新
		updata();
    }
	
	/**
	 * 提供了
	 */
	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());
		}
	}
}

来测试一下这个设计吧。需要注意的是,内置的观察者模式,通知的次序不同于我们注册的次序,所以当我们对于通知顺序有要求的时候,不能使用内置的观察者模式

public class BuiltInObserverPatternTest {

	public static void main(String[] args) {
		// 声明一个具体主题
		final LotteryData lotteryData = new LotteryData();
		// 声明观察者1号并注册
		final LotteryBuyerOne lBuyerOne = new LotteryBuyerOne(lotteryData);
		lotteryData.beginWork();
		final Timer timer = new Timer();
		// 6s后彩民1号,因为总中不了奖失去了兴趣,取消注册了
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				lotteryData.deleteObserver(lBuyerOne);
				timer.cancel();
			}
		}, 6000);
	}
}

猜你喜欢

转载自blog.csdn.net/MingJieZuo/article/details/106333105
今日推荐