一、定义
关于观察者模式的定义,设计模式书中肯定都有,我就直接引用了:
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。 –摘自《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);
}
}
最终运行结果:
根据结果我们知道,内置的观察者模式,通知的次序不同于我们注册的次序,所以当我们对于通知顺序有要求的时候,不能使用内置的观察者模式。