有一个模式可以帮助你的对象知悉现况,不会错过该对象感兴趣的事,对象甚至在运行时可以决定是否要继续被通知,如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式 。观察者模式也是JDK中使用最多的一个设计模式,而我们本章讨论的就是它。
那么首先,我们先来看一看此模式的定义:
定义:观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实作事件处理系统
接下来请LZ用一个例子展出今天的内容:
ex:有这样一个需求,刘能订阅了报刊的报纸,以后每周只要有新报纸发布,刘能就能得到报纸,而刘能也可以选择退订,这时将不再得到报纸。报刊(Newspapers) ,订阅者刘能(Subscriber)。
我们来分析一下,首先,订阅者可以订阅报刊,也可以退订,所以我们需要写两个方法,分别为 registerObserver(订阅) ,removeObserver(退订),我们用一个list来存订阅者,
public class Newspapers {//报刊
private List<Subscriber> subscriber = new ArrayList<Subscriber>(); private String paper; public void registerObserver(Subscriber subs){//订阅
subscriber.add(subs); } public void removeObserver(Subscriber subs){//退订
subscriber.remove(subs); } public void setPaper(String paper){//这里控制新出报纸
this.paper = paper; measurementsChanged();//一旦有新报纸出现调用此方法
}
public void measurementsChanged(){ subscriber.get(0).update(paper); } }
在订阅者中,我们设计成只订阅不退订,我们需要一个Newspapers对象,用来订阅,
class Subscriber{//订阅者刘能
private String paper; private Newspapers newspapers; public Subscriber(Newspapers newspapers) { this.newspapers = newspapers; newspapers.registerObserver(this); } public void update(String paper){ this.paper = paper; display(); } public void display(){ System.out.println("paper :"+paper); } }
接着我们写一个测试类
public class Test { public static void main(String[] args) { Newspapers newspapers = new Newspapers(); Subscriber subscriber = new Subscriber(newspapers); newspapers.setPaper("今日说法"); } }
测试结果:报刊出现新的报纸,然后提醒它的所有订阅者
这个功能到此就算是实现完了,但是,这时候我们接到了报社的电话:报社告诉我们谢大脚和王小蒙也订阅了报纸,当报社有新的报纸时我们不仅要给刘能发,还要给谢大脚和王小蒙也发一份=。=
我们发现,这时我们需要再分别写两个类来代表谢大脚和王小蒙,但是Newspapers类中我们怎么来写?我们的订阅和注册已经写死了只支持刘能!其实订阅报纸本就是一个一对多的关系,而我们经过了上一章的学习,发现这种实现方式有很多地方是不对的,针对具体实现编程,会导致我们以后在订阅或退订时必须修改程序,所以我们应该想到使用接口。在这里,我们把报刊称为“主题”(Subject),而订阅者称为
“观察者(Observer)”,让我们来看的更仔细点:
主题和观察者定义了一对多的关系,观察者依赖于此主题,只要主题状态一有改变,观察者就会被通知。根据通知的风格,观察者可能因此而更新。
① 我们定义一个主题接口Subject,它除了注册和撤销方法之外,还拥有notifyObservers()方法,此方法用于在状态改变时更新所有当前观察者。对象只有使用此接口注册为观察者,或者把自己从观察者中删除。
② 接下来我们定义一个观察者接口Observer,所有潜在的观察者必须实现此接口,它只拥有一个公共方法update,当主题状态改变时,它被调用。具体的观察者可以是实现此接口的任意类,观察者必须注册具体主题,以便接受更新。
③此外,我们还需要一个接口DisplayElement用来显示
下面我们来实现一下:
public interface Subject{ public void registerObserver(Observer observer);//注册观察者
public void removeObserver(Observer observer);//移除观察者
public void notifyObservers();//当主题状态改变,调用这个方法通知观察者
}
public interface Observer { public void update(String paper);//此方法当产生改变的时候由主题调用,
}
public interface DisplayElement{ public void display();//显示
}
下面是主题实现类:
//主题(报社)
public class Newspapers implements Subject{ private ArrayList<Observer> observers ; private String paper; public Newspapers() { observers = new ArrayList<Observer>(); } @Override public void registerObserver(Observer observer) { observers.add(observer); } @Override public void removeObserver(Observer observer) { int index = observers.indexOf(observer); if(index >= 0){ observers.remove(index); } } public void setMeasurements(String paper){ this.paper = paper; measurementsChanged();//当有新报纸时调用此方法
} public void measurementsChanged(){ notifyObservers(); } @Override public void notifyObservers() {//调用此方法通知所有观察者
for(int i=0;i<observers.size();i++){ Observer observer = observers.get(i); observer.update(paper);//更新报纸
} } }
观察者:
//观察者刘能
public class SubscriberLiuNeng implements Observer,DisplayElement{ private String paper; private Subject subject; public SubscriberLiuNeng(Subject subject) { this.subject = subject; subject.registerObserver(this); } @Override public void update(String paper) { this.paper = paper; display(); } public void display(){ System.out.println("刘能,"+paper+"新报纸出来了"); } } //观察者谢大脚
class SubscriberXieDaJiao implements Observer,DisplayElement{ private String paper; private Subject subject; public SubscriberXieDaJiao(Subject subject) { this.subject = subject; subject.registerObserver(this); } @Override public void update(String paper) { this.paper = paper; display(); } public void display(){ System.out.println("谢大脚,"+paper+"新报纸出来了"); } } //观察者王小蒙
class SubscriberWangXiaoMeng implements Observer,DisplayElement{ private String paper; private Subject subject; public SubscriberWangXiaoMeng(Subject subject) { this.subject = subject; subject.registerObserver(this); } @Override public void update(String paper) { this.paper = paper; display(); } public void display(){ System.out.println("王小蒙,"+paper+"新报纸出来了"); } }
这里我们保存subject引用是为了如果想要取消注册会非常方便。接着我们写个测试类:
public static void main(String[] args) { Newspapers newspapers = new Newspapers(); SubscriberLiuNeng subscriberLiuNeng = new SubscriberLiuNeng(newspapers); SubscriberXieDaJiao subscriberXieDaJiao = new SubscriberXieDaJiao(newspapers); SubscriberWangXiaoMeng subscriberWangXiaoMeng = new SubscriberWangXiaoMeng(newspapers); newspapers.setMeasurements("今日说法"); }
结果:
我们可以发现,通过这种实现方式,主题和观察者之间依然可以互相交互,但是并不清除彼此的细节。关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主题不需要知道观察者的具体类是谁,做了些神马或其他任何细节。任何时候我们都可以增加新的观察者,因为主题唯一依赖的东西是一个实习Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可以用新的观察者取代现有的观察者,主题不会受到任何影响,同样的,也可以在任何时候删除某些观察者。
当有新类型的观察者出现时,主题的代码不需要修改,假如我们有个新的具体类需要
当观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所有实现了观察者借口的对象。
我们可以独立地复用主题或观察者,如果我们在其他地方需要使用主题或观察者,可以轻易地复用,因为二者并非紧耦合。下面我们来看一下观察者模式的类图,是不是很熟悉?
回到上面的例子,其实这是一个“推”的例子,数据是由主题推给观察者的,而不是由观察者自己获取,其实Java有自己内置的Observer模式不仅支持“推”,还支持“拉”。
java.util包内包含最基本的Observer接口(相当于上面的Subject接口)与Observable类(相当于上面写的Observer接口)
而java内置的观察者模式运作方式,和我们的实现类似,但有一些小差异,其中最明显的差异是Newspapers(也就是我们的主题)现在扩展自Observable类,并继承到一些增加,删除,通知观察者的方法(以及其他的方法),废话不多说,直接上Observable类源码:
public class Observable { private boolean changed = false; private Vector obs; public Observable() { obs = new Vector(); } public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } public void notifyObservers() { notifyObservers(null); } public void notifyObservers(Object arg) { /* * a temporary array buffer, used as a snapshot of the state of * current Observers. */ Object[] arrLocal; synchronized (this) { if (!changed) return; arrLocal = obs.toArray(); clearChanged(); } for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } public synchronized void deleteObservers() { obs.removeAllElements(); } protected synchronized void setChanged() { changed = true; } protected synchronized void clearChanged() { changed = false; } public synchronized boolean hasChanged() { return changed; } public synchronized int countObservers() { return obs.size(); } }
注意setChanged()方法用来标记状态已经改变的事实,好让notifyObservers()知道当它被调用时应该更新观察者。如果在notifyObservers()之前没有先调用setChanged()方法,那么观察者就不会被通知。这样做有其必要性,setChanged()方法可以让你在更新观察者时,有更多的弹性,你可以更适当的通知观察者。比方说,我们的报社是如此的敏锐,以致于报纸刚写了十分之一就会更新,这会导致Newspaper对象持续不断的通知观察者,我们显然不愿意这种情况发生,我们希望做到的是当报纸刚刚印刷出来,我们才更新,就可以在报纸刚印刷出的时候调用setChanged()方法,进而有效的更新。
下面是我们使用java内置的观察者模式实现:
import java.util.ArrayList; import java.util.Observable; //主题
public class Newspapers extends Observable{ private String paper; public void setMeasurements(String paper){ this.paper = paper; measurementsChanged();//当有新报纸时调用此方法
} public void measurementsChanged(){ setChanged(); notifyObservers(); } public String getPaper() { return paper; } }
这里我们不需要创建list数据结构来存储观察者了,而在measurementsChanged()方法中我们不需要调用notifyObservers()方法来传送数据对象,因为这次我们采用的是“拉”的做法,由观察者自己获取。所以我们为此也提供了getPaper方法。
接下来看观察者:
//观察者刘能
public class SubscriberLiuNeng implements Observer,DisplayElement{ private String paper; private Observable observable; public SubscriberLiuNeng(Observable observable) { this.observable = observable; observable.addObserver(this); } @Override public void update(Observable obs, Object arg) { if(obs instanceof Newspapers){ Newspapers newspapers =(Newspapers) obs; this.paper = newspapers.getPaper(); display(); } } public void display(){ System.out.println("刘能,"+paper+"新报纸出来了"); } } //观察者刘能
class SubscriberXieDaJiao implements Observer,DisplayElement{ private String paper; private Observable observable; public SubscriberXieDaJiao(Observable observable) { this.observable = observable; observable.addObserver(this); } @Override public void update(Observable obs, Object arg) { if(obs instanceof Newspapers){ Newspapers newspapers =(Newspapers) obs; this.paper = newspapers.getPaper(); display(); } } public void display(){ System.out.println("谢大脚,"+paper+"新报纸出来了"); } } //观察者刘能
class SubscriberWangXiaoMeng implements Observer,DisplayElement{ private String paper; private Observable observable; public SubscriberWangXiaoMeng(Observable observable) { this.observable = observable; observable.addObserver(this); } @Override public void update(Observable obs, Object arg) { if(obs instanceof Newspapers){ Newspapers newspapers =(Newspapers) obs; this.paper = newspapers.getPaper(); display(); } } public void display(){ System.out.println("王小蒙,"+paper+"新报纸出来了"); } }
可以看到,在update方法中,我们先确定可观察者属于Newspapers类型,然后调用get方法获取paper接下来利用display()方法显示出来。测试类与之前一样,但是为了能让大家看的明白点,这里我再写一遍:
public static void main(String[] args) { Newspapers newspapers = new Newspapers(); SubscriberLiuNeng subscriberLiuNeng = new SubscriberLiuNeng(newspapers); SubscriberXieDaJiao subscriberXieDaJiao = new SubscriberXieDaJiao(newspapers); SubscriberWangXiaoMeng subscriberWangXiaoMeng = new SubscriberWangXiaoMeng(newspapers); newspapers.setMeasurements("今日说法"); }
注意看结果:
这是怎么回事?文字输出顺序居然不一样了。这其实是因为Observable实现了它的notifyObservers()方法,这导致了通知观察者的次序不同于我们先前的次序,其实谁都没有错,只是双方选择不同的方式实现罢了。
但是,如果我们的代码依赖这样的次序,就是错的,为什么呢?因为一旦观察者/可观察者的实现有所改变,通知次序就会改变,很可能产生错误的结果,这绝对不是我们所认为的松耦合。
java.util.Observable的黑暗面
想必你早已注意到了,可观察者是一个“类”而不是一个“接口”,更糟的是,它甚至没有实现一个接口。而且,它的实现有许多问题,限制了它的使用和复用,虽然它提供了有用的功能,但是LZ依然想提醒大家注意一个事实:Observable是一个类
这违背了我们的原则,会造成什么问题呢?你必须设计一个类去继承它,如果某类想同时具有Observable类和另一个基类的行为,就会陷入两难,因为java不支持多继承。再者,因为没有Observable接口,你无法建立自己的实现,和java内置的ObserverAPI搭配使用,也无法将java.util的实现换成另一套做法的实现(比方说,Observable将关键的方法保护起来,通过上面LZ放出的源码,你会发现setChanged()方法是protected类型的)。这意味着:除非你继承自Observable,否则你无法创建Observable示例并组合到自己的对象中,这违反了我们的设计原则:多用组合,少用继承。
所以,如果你能够扩展Observable,那么它“可能”可以符合你的要求,否则,你就需要像LZ一开始那样自己手动实现这一整套观察者模式,不过都无所谓,不管使用哪一种方法,我们都已经熟悉了观察者模式了。
通过LZ的讲解,想必各位都已经明了了观察者模式的具体实现方式,也清楚了它是如何做到解耦的。观察者模式定义了对象之间的一对多依赖,这样依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新,另外,观察者模式分离了观察者和被观察者二者的责任,这样让类之间各自维护自己的功能,专注于自己的功能,会提高系统的可维护性和可重用性.
下面我们来升华一下观察者模式。在JDK中有这样一个类PropertyChangeSupport,
它用来监听bean的属性是否发生改变,当bean的属性发生变化时,使用PropertyChangeSupport对象的firePropertyChange方法,它会将一个事件发送给所有已经注册的监听器。该方法有三个参数:属性的名字、旧的值以及新的值。属性的值必须是对象,如果是简单数据类型,则必须进行包装。我们来看一下PropertyChangeSupport的源代码(由于源代码过长,LZ只裁取了一部分这里有用的)
public class PropertyChangeSupport implements Serializable { private PropertyChangeListenerMap map = new PropertyChangeListenerMap(); public PropertyChangeSupport(Object sourceBean) { if (sourceBean == null) { throw new NullPointerException(); } source = sourceBean; } public void addPropertyChangeListener(PropertyChangeListener listener) { if (listener == null) { return; } if (listener instanceof PropertyChangeListenerProxy) { PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy)listener; // Call two argument add method.
addPropertyChangeListener(proxy.getPropertyName(), proxy.getListener()); } else { this.map.add(null, listener); } } public void removePropertyChangeListener(PropertyChangeListener listener) { if (listener == null) { return; } if (listener instanceof PropertyChangeListenerProxy) { PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy)listener; // Call two argument remove method.
removePropertyChangeListener(proxy.getPropertyName(), proxy.getListener()); } else { this.map.remove(null, listener); } } public PropertyChangeListener[] getPropertyChangeListeners() { return this.map.getListeners(); } public void addPropertyChangeListener( String propertyName, PropertyChangeListener listener) { if (listener == null || propertyName == null) { return; } listener = this.map.extract(listener); if (listener != null) { this.map.add(propertyName, listener); } } public void removePropertyChangeListener( String propertyName, PropertyChangeListener listener) { if (listener == null || propertyName == null) { return; } listener = this.map.extract(listener); if (listener != null) { this.map.remove(propertyName, listener); } } public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { return this.map.getListeners(propertyName); } public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { firePropertyChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue)); } } public void firePropertyChange(PropertyChangeEvent event) { Object oldValue = event.getOldValue(); Object newValue = event.getNewValue(); if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { String name = event.getPropertyName(); PropertyChangeListener[] common = this.map.get(null); PropertyChangeListener[] named = (name != null) ? this.map.get(name) : null; fire(common, event); fire(named, event); } } private static void fire(PropertyChangeListener[] listeners, PropertyChangeEvent event) { if (listeners != null) { for (PropertyChangeListener listener : listeners) { listener.propertyChange(event); } } } }
LZ带着大家来一步步分析源码,从源码中不难看出map的类型是<String,PropertyChangeListener>,而这个PropertyChangeListener我们稍后说。首先构造器不用多说了,获取一个bean。而addPropertyChangeListener方法大家有没有觉得眼熟?这个就相当于我们的注册,PropertyChangeListenerProxy是PropertyChangeListener的实现类
而PropertyChangeListener相当于观察者接口,我们另观察者实现此接口或者继承PropertyChangeListenerProxy类都可以,removePropertyChangeListener相当于撤销,下面我们看最重要的方法firePropertyChange(),这里的oldSource是改变之前的属性值,newValue是改变后的属性值,而propertyName相当于属性名,也就是key。这里将参数封装到了PropertyChangeEvent类中调用firePropertyChange(PropertyChangeEvent event)方法,我们发现,最后遍历所有的观察者,调用观察者的propertyChange()方法,而这个方法是PropertyChangeListener 接口中的,不管我们采用实现接口还是继承PropertyChangeListenerProxy的方式,都需要我们亲自实现这个方法。
public interface PropertyChangeListener extends java.util.EventListener { /** * This method gets called when a bound property is changed. * @param evt A PropertyChangeEvent object describing the event source * and the property that has changed. */
void propertyChange(PropertyChangeEvent evt); }
下面我们来写一个demo:
建立一个MyBean,相当于我们说的主题,我们利用PropertyChangeSupport构造器将bean对象传入。
public class MyBean{ private String source = "hello"; private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); public void setSource(String newSource) { String oldSource = source; source = newSource; propertyChangeSupport.firePropertyChange("source", oldSource, newSource); } public String getSource() { return source; } public void addPropertyChangeListener(PropertyChangeListener listener){ propertyChangeSupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener){ propertyChangeSupport.removePropertyChangeListener(listener); } }
这里需要我们手动实现addPropertyChangeListener和removePropertyChangeListener方法,因为我们的主题并没有通过继承其他类而获得这两个方法。
下面我们写出测试类来监听主题的Source属性是否改变:
public class ChangeListener implements PropertyChangeListener{ public static void main(String[] args) { MyBean mybean = new MyBean(); mybean.addPropertyChangeListener(new ChangeListener()); mybean.setSource("WOW"); } @Override public void propertyChange(PropertyChangeEvent evt) { MyBean mybean = (MyBean) evt.getSource(); if(evt.getPropertyName().equals(mybean.getSource())); System.out.println("BeanTest 的 name 属性变化!"); } }
结果:
下期预告:装饰者模式