老李:小张啊,最近忙嘛呢?下班就跑
小张:昨天买了彩票,今天去看下自己是否财务自由了
老李:官网注册个账号,坐等中奖号码通知不香吗?
小张:我就是不想加班…,嗯确实香,老李你先把刀放下
老李:哪天心灰意冷了,就注销掉,别辜负了工作给你的热情
小张:…
关于观察者模式的定义,我就直接引用HeadFirst书中的描述了:观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。我们通常把有状态的对象称为主题,收到主题通知的依赖者对象称为观察者。主题和观察者定义了一对多的关系,观察者依赖于主题,只要主题状态一有变化,观察者就会被通知。类图见下:
我们来用程序语音来描述下,以彩票官网为例,彩民可以自由的向其注册或取消注册,当中奖号码更新后,即官网此状态改变后,每个注册过的彩民都会收到官网传来的通知。这里的官网就相当于我们所说的主题,彩民相当于我们的观察者,我们可先创建一个主题接口类:
/**
* 这是主题类。
*
* 用户只要向其注册,主题状态改变后,
* 就可以收到官网发送来的彩票信息。
*/
public interface ISubject {
// 给彩民用户提供的注册和移除方法
void registerObserver(IObserver o);
void removeObserver(IObserver o);
// 给用户发送“彩票信息变化通知”
void notifyLottery();
}
为什么要使用接口,而不是直接使用具体的主题类,因为不想主题与观察者过分耦合,要努力使对象之间的互相依赖降到最低,这样才能够应付变化,建立有弹性的OO系统。 这是一个彩民接口即观察者接口,这个接口只有一个updateLottery(Lottery lottery)方法,当主题的状态改变时它就会被调用。
/**
* 观察者接口类。
*
* 所有的观察者都必须实现该接口,关于观察者的一切
* 主题只知道观察者实现了当前接口即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);
}
}
/**
* 智能彩票机开始摇号。
*
* 这里模拟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);
}
}