定义:观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实作事件处理系统。
一:简单观察者模式
观察者接口:该接口里面只有一个方法,用于被观察者发生变化,做出相应的回应。
package com.liuyq.designpatter.observer.easy;
/**
* Created by liuyq on 2017/12/8.
* 这个是观察者 发现被观察者发生了变化,则做出相应的回应
*/
public interface Observer {
void update(Observable observable);
}
具体的观察者
/**
* Created by liuyq on 2017/12/8.
* 观察者实例
*/
public class Observer1 implements Observer {
public void update(Observable observable) {
System.out.println("观察者1号发现"+observable.getClass().getSimpleName());
}
}
下面是被观察者,它有一个观察者的列表,并且有一个通知所有观察者的方法,通知的方式就是调用观察者通用的接口行为update方法。下面我们看它的代码。
**
* Created by liuyq on 2017/12/8.
* 这个是被观察者,当自己发生变化的时候会通知所有观察他的人
*/
public class Observable {
List<Observer> observerList = Lists.newArrayList();
//添加观察者
public void addObserver(Observer observer){
observerList.add(observer);
}
//自己改变,通知所有观察者
public void change(){
System.out.println("被观察者发生了改变");
notifyObservers(); //通知所有观察者
}
public void notifyObservers(){
for(Observer observer : observerList){
observer.update(this);
}
}
}
二:真实的观察者实例
比如我们经常看的小说网站,都有这样的功能,就是读者可以订阅作者,这当中就有明显的观察者模式案例,就是作者和读者。他们的关系是一旦读者关注了一个作者,那么这个作者一旦有什么新书,就都要通知读者们,这明显是一个观察者模式的案例,所以我们可以使用观察者模式解决。
观察者接口
/**
* Created by Administrator on 2017-12-09.
* 观察者接口,每个观察者都必须实现这个接口
*/
public interface Observer {
//这个方法是观察者在观察对象产生变化时所做的响应动作,从中传入了观察的对象和一个预留参数
void update(Observable o, Object arg);
}
具体的观察者
/**
* Created by Administrator on 2017-12-09.
* 读者(观察者),实现观察者接口
*/
public class Reader implements Observer {
private String name;
public Reader(String name){
super();
this.name = name;
}
//读者可以关注某一位作者,关注则代表吧自己加到作者的观察这列表里
//也就是往被观察中加观察者
public void subscribe(String writerName){
WriterManager.getInstance().getWriter(writerName).addObserver(this);
}
//读者可以取消关注某一位作者,取消关注则代表吧自己从作者的观察者列表删除
//删除观察者
public void unsubscribe(String writerName){
WriterManager.getInstance().getWriter(writerName).deleteObserver(this);
}
//当关注的作者发表新小说时,会通知读者去看
public void update(Observable o, Object arg) { //Observable被观察者
if(o instanceof Writer){
Writer writer = (Writer)o;
System.out.println(name+"知道"+writer.getName()+"发布了新书《" + writer.getLastNovel() + "》,非要去看!");
}
}
}
被观察者基类
/**
* Created by Administrator on 2017-12-09.
* 被观察者
*/
public class Observable {
//这里一个改变标识,来标记被观察这有没有改变
private boolean changed = false;
//持有观察者列表
//Vector类实现了一个动态数组。和ArrayList和相似,
// 但是两者是不同的:
// Vector是同步访问的。
// Vector包含了许多传统的方法,这些方法不属于集合框架。
private Vector vector;
public Observable(){
vector = new Vector(); //和ArrayList一样,默认10个
}
//添观察者
public synchronized void addObserver(Observer o){
if(o == null) throw new NullPointerException();
if(!vector.contains(o)){
vector.addElement(o); //将指定的组件添加到此向量的末尾,将其大小增加 1。
}
}
//删除观察者
public synchronized void deleteObserver(Observer o) {
vector.removeElement(o);
}
public void notifyObservers() {
notifyObservers (null);
}
//通知所有观察者,被观察者发生变化就,可以执行update方法
public void notifyObservers(Object arg){
//一个临时的数组,用于并发访问被观察者时,留住观察者列表的当前状态,这种处理方式其实也算是一种设计模式,即备忘录模式。
Object[] arrLocal;
//这个代码块,表示在获取观察这列表是,该对象是被锁定的
//也就说,在我读取到被观察者列表之前,不允许其他线程改变观察这列表
synchronized (this){
//如果(被观察这)没变化,无需通知其他观察者,直接返回
if(!changed)return;
//改变了
//将当前观察者列表放在临时数组
arrLocal = vector.toArray();
//将改变标识重新置回未改变
clearChanged();
}
//注意这个for循环没有在同步块,此时已经释放了被观察者的锁,其他线程可以改变观察者列表
//但是这并不影响我们当前进行的操作,因为我们已经将观察者列表复制到临时数组
//在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象
for(int i = arrLocal.length-1;i>=0;i--){
try {
((Observer) arrLocal[i]).update(this, arg);
}catch (Exception e){
e.printStackTrace();
}
}
}
//删除所有观察这
public synchronized void deleteObservers(){
vector.removeAllElements();
}
//标识被观察者被改变过
protected synchronized void setChanged(){
changed = true;
}
//标识被观察者没改变
protected synchronized void clearChanged() {
changed = false;
}
//返回被观察者是否改变
public synchronized boolean hasChanged() {
return changed;
}
//返回观察者数量
public synchronized int countObservers() {
return vector.size();
}
}
具体被观察者
/**
* Created by Administrator on 2017-12-09.
* 作者类,要继承自被观察者类
*/
public class Writer extends Observable{
private String name; //作者的名称
private String lastNovel;//记录作者最新发布的小说
public Writer(String name){
super();
this.name = name;
WriterManager.getInstance().add(this); //生成一个被观察者就往观察者管理器里放一个被观察者
}
//作者发布了小说了,要通知所有关注自己的读者
public void addNovel(String novel){
System.out.println(name+"发布了新书《"+novel+"》!");
lastNovel = novel;//最新发布的书
setChanged(); //发生改变
notifyObservers();//通知所有观测他的人
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLastNovel() {
return lastNovel;
}
public void setLastNovel(String lastNovel) {
this.lastNovel = lastNovel;
}
}
被观察者管理器:存储所有被观察者,管理类器是单例的
//管理器,保持一份独有的作者列表
public class WriterManager {
private Map<String, Writer> writerMap = new HashMap<String, Writer>();
//添加作者
public void add(Writer writer){
writerMap.put(writer.getName(), writer);
}
//根据作者姓名获取作者
public Writer getWriter(String name){
return writerMap.get(name);
}
//单例
private WriterManager(){}
public static WriterManager getInstance(){
return WriterManagerInstance.writerManager;
}
private static class WriterManagerInstance{
private static WriterManager writerManager = new WriterManager();
}
}
我们使用观察者模式的用意是为了作者不再需要关心他发布新书时都要去通知谁,更重要的是他不需要关心他通知的是读者还是其它什么人,他只知道这个人是实现了观察者接口的,即我们的被观察者依赖的只是一个抽象的接口观察者接口,而不关心具体的观察者都有谁都是什么,比如以后要是游客也可以关注作者了,那么只要游客类实现观察者接口,那么一样可以将游客列入到作者的观察者列表中。
另外,我们让读者自己来选择自己关注的对象,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有N多观察者,所以让被观察者自己维护这个列表会很艰难,这就像一个老师被许多学生认识,那么是所有的学生都记住老师的名字简单,还是让老师记住N多学生的名字简单?答案显而易见,让学生们都记住一个老师的名字是最简单的。
另外,观察者模式分离了观察者和被观察者二者的责任,这样让类之间各自维护自己的功能,专注于自己的功能,会提高系统的可维护性和可重用性。
三:事件驱动模型
首先事件驱动模型与观察者模式勉强的对应关系可以看成是,被观察者相当于事件源,观察者相当于监听器,事件源会产生事件,监听器监听事件。所以这其中就搀和到四个类,事件源,事件,监听器以及具体的监听器。
监听器
//点击监听器
public interface ClickListener extends EventListener{
void click(ClickEvent clickEvent);
}
//双击监听器
interface DblClickListener extends EventListener{
void dblClick(DblClickEvent dblClickEvent);
}
//鼠标移动监听器
interface MouseMoveListener extends EventListener{
void mouseMove(MouseMoveEvent mouseMoveEvent);
}
按钮事件基类
//按钮事件基类
public abstract class ButtonEvent extends EventObject{
public ButtonEvent(Object source) {
super(source);
}
public Button getButton(){
return (Button) super.getSource();
}
}
//点击事件
class ClickEvent extends ButtonEvent{
public ClickEvent(Object source) {
super(source);
}
}
//双击事件
class DblClickEvent extends ButtonEvent{
public DblClickEvent(Object source) {
super(source);
}
}
//鼠标移动事件
class MouseMoveEvent extends ButtonEvent{
//鼠标移动事件比较特殊,因为它需要告诉监听器鼠标当前的坐标是在哪,我们记录为x,y
private int x;
private int y;
public MouseMoveEvent(Object source, int x, int y) {
super(source);
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
button元素
public class Button {
private String id;//这相当于id属性
private String value;//这相当于value属性
private ClickListener onclick;//我们完全模拟原有的模型,这个其实相当于onclick属性
private DblClickListener onDblClick;//同理,这个相当于双击属性
private MouseMoveListener onMouseMove;//同理
//按钮的单击行为
public void click(){
onclick.click(new ClickEvent(this));
}
//按钮的双击行为
public void dblClick(){
onDblClick.dblClick(new DblClickEvent(this));
}
//按钮的鼠标移动行为
public void mouseMove(int x,int y){
onMouseMove.mouseMove(new MouseMoveEvent(this,x,y));
}
//相当于给id赋值
public void setId(String id) {
this.id = id;
}
//类似
public void setValue(String value) {
this.value = value;
}
//这个相当于我们在给onclick添加函数,即设置onclick属性
public void setOnclick(ClickListener onclick) {
this.onclick = onclick;
}
//同理
public void setOnDblClick(DblClickListener onDblClick) {
this.onDblClick = onDblClick;
}
//同理
public void setOnMouseMove(MouseMoveListener onMouseMove) {
this.onMouseMove = onMouseMove;
}
//以下get方法
public String getId() {
return id;
}
public String getValue() {
return value;
}
public ClickListener getOnclick() {
return onclick;
}
public DblClickListener getOnDblClick() {
return onDblClick;
}
public MouseMoveListener getOnMouseMove() {
return onMouseMove;
}
只有button元素的jsp页面
/模仿一个只有button的jsp页面
public class ButtonJsp {
private Button button;
public ButtonJsp(){
super();
button = new Button();//这个可以当做我们在页面写了一个button元素
button.setId("submitButton");
button.setValue("提交");//提交按钮
button.setOnclick(new ClickListener() {//我们给按钮注册点击监听器
public void click(ClickEvent clickEvent) {
System.out.println("--------单击事件代码---------");
System.out.println("if('表单合法'){");
System.out.println("\t表单提交");
System.out.println("}else{");
System.out.println("\treturn false");
System.out.println("}");
}
});
button.setOnDblClick(new DblClickListener() {
public void dblClick(DblClickEvent dblClickEvent) {
//双击的话我们提示用户不能双击“提交”按钮
System.out.println("--------双击事件代码---------");
System.out.println("alert('您不能双击"+dblClickEvent.getButton().getValue()+"按钮')");
}
});
button.setOnMouseMove(new MouseMoveListener() {
//这个我们只简单提示用户鼠标当前位置,示例中加入这个事件
//目的只是为了说明事件驱动中,可以包含一些特有的信息,比如坐标
public void mouseMove(MouseMoveEvent mouseMoveEvent) {
System.out.println("--------鼠标移动代码---------");
System.out.println("alert('您当前鼠标的位置,x坐标为:"+mouseMoveEvent.getX()+",y坐标为:"+mouseMoveEvent.getY()+"')");
}
});
}
public Button getButton(){
return button;
}
}
观察者模式所欠缺的是设计上的问题,即观察者和被观察者是多对一的关系,那么反过来的话,就无法支持了。
各位可以尝试将二者位置互换达到这个效果,这算是设计模式的活用,很简单,就是让被观察者做成一个接口,提供是否改变的方法,让观察者维护一个被观察者的列表,另外开启一个线程去不断的测试各个被观察者是否改变。
观察者模式还有一个缺点就是,每一个观察者都要实现观察者接口,才能添加到被观察者的列表当中,假设一个观察者已经存在,而且我们无法改变其代码,那么就无法让它成为一个观察者了,不过这个我们依然可以使用适配器模式解决。但是还有一个问题就不好解决了,就是假如我们很多类都是现成的,当被观察者发生变化时,每一个观察者都需要调用不同的方法,那么观察者模式就有点捉襟见肘的感觉了,我们必须适配每一个类去统一他们变化的方法名称为update,这是一个很可怕的事情。
对于事件驱动就没有这样的问题,我们可以实现多个监听器来达到监听多个事件源的目的,但是它的缺点刚才已经说过了,在事件源或者事件增加时,监听器和事件类通常情况下会成对增加,造成系统的复杂性增加,不过目前看来,事件驱动模型一般都比较稳定,所以这个问题并不太明显,因为很少见到无限增加事件的情况发生。
还有一个缺点就是我们的事件源需要看准时机触发自己的各个监听器,这也从某种意义上增加了事件源的负担,造成了类一定程度上的臃肿。
观察者模式与事件驱动除了业务场景的区别以外,在功能上主要有以下区别。
1,观察者模式中观察者的响应理论上讲针对特定的被观察者是唯一的(说理论上唯一的原因是,如果你愿意,你完全可以在update方法里添加一系列的elseif去产生不同的响应,你应该忘掉elseif),而事件驱动则不是,因为我们可以定义自己感兴趣的事情,比如刚才,我们可以监听作者发布新书,我们还可以在监听器接口中定义其它的行为。再比如tomcat中,我们可以监听servletcontext的init动作,也可以监听它的destroy动作。
2,虽然事件驱动模型更加灵活,但也是付出了系统的复杂性作为代价的,因为我们要为每一个事件源定制一个监听器以及事件,这会增加系统的负担,各位看看tomcat中有多少个监听器和事件类就知道了。
3,另外观察者模式要求被观察者继承Observable类,这就意味着如果被观察者原来有父类的话,就需要自己实现被观察者的功能,当然,这一尴尬事情,我们可以使用适配器模式弥补,但也不可避免的造成了观察者模式的局限性。事件驱动中事件源则不需要,因为事件源所维护的监听器列表是给自己定制的,所以无法去制作一个通用的父类去完成这个工作。
4,被观察者传送给观察者的信息是模糊的,比如update中第二个参数,类型是Object,这需要观察者和被观察者之间有约定才可以使用这个参数。而在事件驱动模型中,这些信息是被封装在Event当中的,可以更清楚的告诉监听器,每个信息都是代表的什么