`
喻红叶
  • 浏览: 39552 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

Java与模式-观察者模式

 
阅读更多

在没有互联网的时代里,报纸是人们获取信息的一个非常重要的途径,很多人会定一份或者几份报纸。报社会保存订阅者的信息,每当出了新报纸时,立即送到订阅者手里。订阅者在这种关系中处于被动的状态,它依赖于报社的行为,只有新报纸出版了,它才有可能阅读其中的内容。在互联网时代,报社可以把它的新闻放到网上,订阅者通过RSS订阅,每当有新的新闻就推送给订阅者。问题是:报社如何把新闻推送给所有的订阅者?进一步抽象描述这个问题:当一个对象的状态发生改变时,如何让依赖于它的所有对象得到通知,并进行处理呢?

观察者模式是这个问题的合理解决方案。观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。使用观察者模式的解决思路:首先,报社应该持有一个订阅者的列表,允许随时添加订阅者,同时也可以删除订阅者;其次,每当新闻产生时,报社会把新闻发给每个订阅者,这样的话订阅者也需要提供了一个接受新闻的方法;报社调用每个订阅者的接受新闻的方法,这样报社的任务就完成了,至于订阅者怎么处理这些新闻,那就与报社无关了。在观察者模式中,订阅者就是观察者,报社就是目标,它被观察者观察,这也是观察者模式名字的由来。

结构

观察者模式有如下几个角色:

Subject:目标对象抽象,目标对象一般提供对观察者注册和退订的维护;当目标的状态发生改变时,目标负责所有注册的,有效的观察者(在观察者模式的变形中,有效的观察者是很重要的);

ConcreteSubject:具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册的、有效的观察者,让观察者提供相应的处理;

Observer:定义观察者接口,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里回调目标对象,以获取目标对象的数据;

ConcreteObserver:观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持与目标的相应状态一致。

目标的示意性代码:

/**
 * 被观察的目标,它应该提供如下功能:
 * 提供对观察者的注册和退订
 * 当目标状态发生改变时,通知所有的观察者
 * @author cxy
 */
public interface Subject {
	//注册观察者
	public void attach(Observer observer);
	//取消观察者
	public void dettach(Observer oserver);
	
	//状态发生改变,通知所有有效的观察者
	public void notifyObservers();
}

/**
 * 具体的目标,实现了Subject接口
 */
public class ConcreteSubject implements Subject {
	//持有注册的观察者
	private List<Observer> observers = new ArrayList<Observer>();
	private String content;
	
	@Override
	public void attach(Observer observer) {
		observers.add(observer);
	}
	
	@Override
	public void dettach(Observer observer) {
		if(observers.contains(observer))
			observers.remove(observer);
	}

	@Override
	public void notifyObservers() {
		for(Observer observer : observers)
			observer.update(this);//把自己提供给观察者,这是拉模型
	}
	
	//状态发生了改变
	public void setContent(String content) {
		this.content = content;
		notifyObservers();
	}
	
}
观察者的示意性代码:
/**
 * 观察者接口,定义接收通知的处理方法
 * @author cxy
 *
 */
public interface Observer {
	public void update(Subject subject);
}

public class ConcretObserver implements Observer {
	private String state;
	
	//接收通知的处理方法,一般需要更新自身的状态与目标的状态保持一致
	public void update(Subject subject) {
		state = ((concreteSubject)subject).xxx();
	}
}
使用观察者模式解决新闻订阅问题,我们需要一个具体定义报社接口,它是目标对象的抽象,还应该有具体的报社,它是目标的具体实现;然后定义订阅者的接口,观察者对象的抽象,然后定义具体的订阅者,它是观察者的具体实现。
import java.util.ArrayList;
import java.util.List;

/**
 * 报社接口,它应该提供如下功能:
 * 提供对订阅者的注册和退订
 * 当出版新报纸时,通知所有的订阅者
 * @author cxy
 */
public interface NewsPaper {
	//注册观察者
	public void attach(Reader reader);
	//取消观察者
	public void dettach(Reader reader);
	
	//状态发生改变,通知所有有效的观察者
	public void notifyObservers();
}

/**
 * 具体的报纸,实现NewsPaper接口
 * @author cxy
 *
 */
public class NewsPaperImp implements NewsPaper {
	//持有注册的观察者
	private List<Reader> readers = new ArrayList<Reader>();
	private String content;
	
	@Override
	public void attach(Reader reader) {
		readers.add(reader);
	}
	
	@Override
	public void dettach(Reader reader) {
		if(readers.contains(reader))
			readers.remove(reader);
	}

	@Override
	public void notifyObservers() {
		for(Reader reader : readers)
			reader.update(this);
	}
	
	//状态发生了改变
	public void setContent(String content) {
		this.content = content;
		notifyObservers();
	}
	
	public String getContent() {
		return this.content;
	}
	
}

/**
 * 订阅者接口,当报社出新报纸时,处理相关业务
 *
 */
public interface Reader {
	public void update(NewsPaper news);
}

/**
* 具体的订阅者,为了简单起见,仅仅是打印一下内容
*
*/
public class ReaderImp implements Reader {
	private String name;
	
	public ReaderImp(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	//一般来说修改自己的状态和目标保持一直
	public void update(NewsPaper news) {
		System.out.println(name + "收到了报纸,本期内容是:" + ((NewsPaperImp)news).getContent());
	}	
}

演示代码:

public class Client {
	public static void main(String[] args) {
		NewsPaperImp news = new NewsPaperImp();
		Reader one = new ReaderImp("张三");
		Reader two = new ReaderImp("李四");
		Reader three = new ReaderImp("王五");
		
		//注册
		news.attach(one);
		news.attach(two);
		news.attach(three);
		
		news.setContent("观察者模式");

		//退订
		news.dettach(one);
		news.setContent("有人退订");
	}

}

目标与观察者之间的关系

目标与观察者之间是一对多的关系,如果只有一个观察者,这个时候就退化成了一对一的关系,这是一种特殊情况,也可以使用观察者模式。观察者和目标是单向的依赖,观察者依赖于目标,主动权完全掌握在目标手中,观察者只能被动的等待通知。

一个观察者其实是可以观察多于一个的目标,它可以接受来自多个目标的通知,在这种情况下,观察者应该为每一个目标定义不同的通知更新方法。如果仅仅定义一个update()方法,在update()方法内部区分不同的目标,这是不妥的,极不优雅,而且出错的概率很高。应该为不同的目标定义不同的处理方法,这样逻辑就比较清晰了。

public interface Observer {	
	//处理目标A的方法
	public void updateForA(A a);
	//处理目标B的方法
	public void updateForB(B b);
	//处理目标C的方法
	public void updateForC(C c);
}

目标和观察者之间可能会相互观察,比如有两个类A和B,在一套逻辑下,A是目标,B是观察者;在另一套逻辑下,B是目标,A是观察者。这种情况有可能在实际的应用中出现,但是要特别小心处理死循环的情况:A的改变会引起B的变化,如果B的这种变化恰巧又能引起A的变化,那么就有可能进入死循环了。

有效的观察者

在一般的观察者模式中,一旦目标的状态发生了改变,它会一视同仁的对待所有的观察者,通知它们更新。但是在实际应用中,往往需要根据具体的情况去通知相应的观察者。比如对同一个事情,不同岗位上的人做出的反应是不同的,以公安局举例,一般的个人纠纷直接找民警就可以解决了,这个时候没必要通知局领导吧,但是如果发生了恶劣的连续绑架案件,那么就应该通知局领导了。在观察者模式中也是如此,有些状态的改变只需要通知一部分观察者,这个时候就应该在目标的通知方法里进行判断,以确定通知哪些通知者,我个人觉得这甚至都不能算是观察者模式的变形,这是普遍存在的问题。在这种情况下,需要通知的观察者就是有效观察者,必须通知这些观察者,其他的观察者就不应该通知它们,以免引起误更新。

//进行逻辑判断,确定是否需要通知
public void notifyObserver() {
	for(Observer obs : observers) {
		if(判断是否需要通知该观察者)
			obs.update(this);
		//其他处理
	}
}
推模型和拉模型

推模型:目标知道观察者需要的数据,在通知观察者更新的时候,目标主动向观察者推送详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或者部分数据,相当于在广播通信。在新闻的例子中,目标知道观察者需要的就是新闻的详细信息,所以它可以在通知观察者更新时,直接把新闻信息作为参数传递过去;观察者获得新闻信息后,就可以直接使用了,如下所示:

//目标直接把新闻信息(content)作为参数传递给订阅者
@Override
public void notifyObservers() {
	for(Reader reader : readers)
		reader.update(content);
}

//订阅者可以直接使用新闻信息
public void update(String content) {
	System.out.println(name + "收到了报纸,本期内容是:" + content);
}
拉模型:目标在通知观察者时,只传递少量的信息,如果观察者需要更多的信息,需要观察者主动去目标那里“拉”。一般情况下,目标把自身传递过去,观察者可以通过目标的引用来获取想要的信息了。如下所示:
//目标把自身传递过去
@Override
public void notifyObservers() {
	for(Reader reader : readers)
		reader.update(this);
}
//观察者需要什么数据,就通过目标引用主动去“拉”
public void update(NewsPaper news) {
	System.out.println(name + "收到了报纸,本期内容是:" + ((NewsPaperImp)news).getContent());
}
在拉模型中,目标把自己传递出去了,这基本上是目标对象能传递的最大数据集合了,基本上可以适应各种情况的需要。

总结

观察者模式的本质是:触发联动。当目标对象发生改变时,就会出发相应的通知,然后调用观察者相应的方法。而且这个联动还是动态的,可以随时添加和删除观察者。一定要理解观察者模式的本质,设计模式不是从来就有,也不是条条框框,我愿意把它理解为一种经验,一种大致正确的解决思路。

观察者模式的优点

(1)实现了观察者和目标之间的抽象耦合。通过定义观察者接口,目标只知观察者接口,不知具体的观察者,这样具体的观察者就与目标之间解耦了。

(2)动态联动。

(3)广播通信,目标通知的信息要对所有有效的观察者进行广播。

观察者模式的缺点:可能会引起无谓的操作。每次目标都对所有的观察者发通知,不管这个观察者是否需要,这有可能引起观察者的误更新,所以一定要区分对待观察者

使用场景:

(1)一个抽象模型有两个方面,其中一个方面的操作依赖于另一个方面的状态变化。

(2)在更新一个对象的时候,需要连带更新其他对象,而且还不知道到底有多少个这样的对象。

(3)一个对象必须通知其他对象,同时又想跟被通知的对象解耦,这个时候就可以使用观察者模式,通过抽象一个观察者接口,就可以不跟具体的被通知对象打交道了。

Java中的观察者模式

Java本身就包含了观察者模式,在java.util包里提供了观察者接口:Observer,它只有一个方法:void update(Observable o, Object arg);当目标发生变化时,会调用它的update方法,这跟上面自己实现的观察者接口是一致的。同时它还提供了一个具体的类Observable,它就是目标对象,摘取其源码一窥究竟:

public class Observable {
	//保存目标的状态,标示目标是否改变
    private boolean changed = false;
    //保存观察者,竟然用Vector,真土
    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 synchronized void deleteObservers() {
        obs.removeAllElements();
    }
    //如果目标发生了改变,需要调用此方法
    //注意它是protected,专为子类而使用    
    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();
    }
    
    //通知所有观察者,可以看出它相当于notifyObservers(null)
    public void notifyObservers() {
        notifyObservers(null);
    }
    
    /**
     * 在这里进行了同步,它获取了此对象的Monitor,在它执行这段代码时,会有两个问题:
     * (1)在这时添加观察者是不行的,必须得等它执行完,也就是说一个新添加的观察者没有得到通知
     * (2)在这时删除观察者也是不行的,必须得等它执行完,也就是说一个应该删除的观察者获得了通知
     * 首先会判断是否发生了改变,如果发生了改变,则把所有的观察者读到一个数组里
     * 并清楚改变标记,然后挨个通知所有的观察者
     * 看了这段源码,感觉实现的真不咋的
     * @param arg
     */
    public void notifyObservers(Object arg) {
       
        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);
    }
}
关于Observable的行为上面都有注释,我们可以发现Java默认的是拉模型,如果使用notifyObservers(),它会把目标本身传递过去,如果使用notifyObservers(arg),它同样会把目标本身传递过去。有一点需要注意:Observable在通知观察者之前,会先检查changed的状态,所以子类在调用notifyObservers()方法之前,必须要先调用setChanged()方法。如果继承了这个类,那就不能继承其他类了,这是一个限制,可以考虑使用内部类来继承此类。只能说这代码实现还真挺土的,竟然还用Vector,竟然没用泛型。感觉完全没必要使用Java提供了默认实现。

转载请注明:喻红叶《Java与模式-观察者模式》

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics