设计模式五(难逃法眼-观察者模式)
前言
提起观察者模式,LZ略微有点小小的激动,因为LZ在工作中接触的第一个项目就用到了观察者模式,虽然LZ当时处于一种懵逼状态,完全是复制粘贴代码去完成业务需求,但那个项目的接触,也让LZ对观察者模式有了一个初步的了解,此后一直对观察者模式的理解处于朦胧状态,似懂非懂,直到前些天看到左盟主的博客才恍然大悟,有种相见恨晚的感觉。
先来简单描述一下LZ当时项目中是什么样的业务用到观察者模式的。业务大致是这样的,项目当时牵扯到一些txt文件的上传,文件的组要内容是通信基站的一些状态信息和描述,文件大多是批量上传,上传后要自动解析并持久化到数据库,前台没有显示的解析按钮和请求,就意味着所有文件的上传只要请求触发就要通知解析功能对其进行自动解析。很明显这样的业务使用观察者模式更合适不过了,LZ完全后知后觉,先来看看观察者模式吧!
定义
观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实作事件处理系统。
上面的定义当中,主要有这样几个意思,首先是有一个目标的物件,通俗点讲就是一个类,它管理了所有依赖于它的观察者物件,或者通俗点说是观察者类,并在它自己状态发生变化时,主动发出通知。
简单点概括成通俗的话来说,就是一个类管理着所有依赖于它的观察者类,并且它状态变化时会主动给这些依赖它的类发出通知。
针对以上描述,我们先来看看百度给的类图
2017-11-19_142825.png
我们可以看到,被观察类Observable中只持有一个观察者Observer的列表,当被观察者自己状态发生改变时,调用notifyObservers方法通知自己持有的这些观察者列表中的对象,具体的观察者Observer都是什么,被观察者是不关心也不需要知道的。
上面就将观察者和被观察者二者的耦合度降到很低了,而我们具体的观察者是必须要知道自己观察的是谁,所以它依赖于被观察者。
我们可以看到观察者模式中有三个角色,一个是观察者的抽象接口,一个是观察者的具体实现,一个是被观察者,根据上边的类图我们来写一段测试案例
(1)一个抽象的观察者接口(用于定义观察者发现被观察者改变后所作出的相应动作)
/**
* 观察者接口
*/
public interface IObserver {
void update(Observable o);
}
(2)一个被观察者
/**
* 被观察者类
*/
public class Observable {
//观察者列表
List<IObserver> observers = new ArrayList<>();
//给被观察者添加观察者
public void addObserver(IObserver o){
observers.add(o);
}
public void change(){
System.out.println("被观察者已经发送改变~~~~~");
//通知观察者们
notifyObservers();
}
private void notifyObservers() {
for(IObserver o:observers){
o.update(this);
}
}
}
(3)若干个观察者的具体实现类
/**
* 观察者A
*/
public class ObserverA implements IObserver {
public void update(Observable o) {
System.out.println("观察者A观察到" + o.getClass().getSimpleName() + "发生变化");
System.out.println("观察者A做出相应");
}
}
/**
* 观察者B
*/
public class ObserverB implements IObserver {
public void update(Observable o) {
System.out.println("观察者B观察到" + o.getClass().getSimpleName() + "发生变化");
System.out.println("观察者B做出相应");
}
}
(4)测试类
public class TestDemo {
@Test
public void fun1(){
//创建一个观察者
Observable observable = new Observable();
//给观察者添加被观察者
observable.addObserver(new ObserverA());
observable.addObserver(new ObserverB());
//观察者执行改变方法
observable.change();
}
}
2017-11-19_145944.png
以上就是一个符合观察者模式思想的简单demo,要注意的是,这里的被观察者需要持有一个观察者的列表,列表中填装了观察他的观察者,它还需要有一个添加观察者的方法,来吧需要观察他的观察者添加到观察者列表中,最后还需要有一个通知方法notifyObservers,当自己改变时通知那些观察他的观察者。
我们可以想象生活中的一个这个例子,买房子,买房者就是观察者,房子就是被观察者,当房子价格变动时通知购房者,我们来试着写一写这个例子:
(1)首先我们定义一个购房者的抽象接口人(观察者接口)
/**
* 定义一个观察者接口(人)
*/
public interface People {
//人有一个买房的动作
void buy(Home home);
}
(2)定义一个房子类(被观察者类)
/**
* 一个房子类(被观察者)
*/
public class Home {
//一个装载购买者的列表(观察者列表)
List<People> list = new ArrayList<>();
//一个添加购房者的方法
public void addPeople(People people){
list.add(people);
}
//一个自身的价格改变行为
public void change(){
System.out.println("房子价格有变动!");
notifyObservers();//通知那些观察者(买房的人)
}
//一个通知购房者的方法
public void notifyObservers(){
for (People people : list) {
people.buy(this);
}
}
}
(3)若干个想购买房子的人(观察者实例)
/**
* 路人甲(观察者1)
*/
public class PeopleA implements People {
public void buy(Home home) {
System.out.println("购房者甲观察到" + home.getClass().getSimpleName() + "发生变化");
System.out.println("购房者甲做出了相应决定");
}
}
/**
* 路人乙(观察者2)
*/
public class PeopleB implements People {
public void buy(Home home) {
System.out.println("购房者乙观察到" + home.getClass().getSimpleName() + "发生变化");
System.out.println("购房者乙做出了相应决定");
}
}
(4)测试类
public class App {
public static void main(String[] args) {
//创建一个房子
Home home = new Home();
//给房子添加观察者
home.addPeople(new PeopleA());
home.addPeople(new PeopleB());
//房子价格有所变动
home.change();
}
}
2017-11-19_154754.png
好了,相比看了以上贴切生活的案例大家都对观察者模式都有一个更深的了解,针对上边的例子,大家有没有发现一个问题,我们的被观察者是房子,通知我们价格变动的通知者也是房子,问题就在这里,房子本身不会说话,如何通知我们,所以我们联想到了销售部或者房屋中介,所以这里还缺一个角色,被观察者的管理者。以此引入java中自带的观察者模式
JDK为了方便开发人员开发,已经写好了现成儿的观察者接口和被观察者接口,下面我们看看有关java中提供的观察者接口和被观察者接口的源码
(1)观察者接口
//观察者接口,每一个观察者都必须实现这个接口
public interface Observer {
//这个方法是观察者在观察对象产生变化时所做的响应动作,从中传入了观察的对象和一个预留参数
void update(Observable o, Object arg);
}
(2)被观察者接口
//被观察者类
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);
}
//notifyObservers(Object arg)的重载方法
public void notifyObservers() {
notifyObservers(null);
}
//通知所有观察者,被观察者改变了,你可以执行你的update方法了。
public void notifyObservers(Object arg) {
//一个临时的数组,用于并发访问被观察者时,留住观察者列表的当前状态,这种处理方式其实也算是一种设计模式,即备忘录模式。
Object[] arrLocal;
//注意这个同步块,它表示在获取观察者列表时,该对象是被锁定的
//也就是说,在我获取到观察者列表之前,不允许其他线程改变观察者列表
synchronized (this) {
//如果没变化直接返回
if (!changed)
return;
//这里将当前的观察者列表放入临时数组
arrLocal = obs.toArray();
//将改变标识重新置回未改变
clearChanged();
}
//注意这个for循环没有在同步块,此时已经释放了被观察者的锁,其他线程可以改变观察者列表
//但是这并不影响我们当前进行的操作,因为我们已经将观察者列表复制到临时数组
//在通知时我们只通知数组中的观察者,当前删除和添加观察者,都不会影响我们通知的对象
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();
}
}
下面我们用jdk提供的观察者模式来修改一下上边的购房案例
(1)一个观察者people(java要求观察者实现Observer接口)
/**
* 一个想购买房子的观察者
*/
public class People implements Observer {
private String name;
public People(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
//关注
public void subscribe(String homeName){
HomeAgency.getInstance().getHome(homeName).addObserver(this);
}
//取消关注
public void unsubscribe(String homeName){
HomeAgency.getInstance().getHome(homeName).deleteObserver(this);
}
//当楼盘房子价格有所变动,就去通知购房者
public void update(Observable o, Object arg) {
if(o instanceof Home){
Home home = (Home) o;
System.out.println(name + "知道了" + home.getName() + "价格变为: " + home.getLastPrice() + ", 决定去买房子!");
}
}
}
(2)一个被观察者Home(java要求被观察者需要继承Observable类)
/**
* 一个被观察者(房子)
*/
public class Home extends Observable {
//楼盘房子的名字
private String name;
//最后价格
private Integer lastPrice;
public Home(String name) {
super();
this.name = name;
HomeAgency.getInstance().add(this);
}
public void changePrice(Integer price){
System.out.println(name + "的价格有所改变, 改变后价格为: " + price + "平方米/元");
lastPrice = price;
//标志改变和提醒
setChanged();
notifyObservers();
}
public String getName() {
return name;
}
public Integer getLastPrice() {
return lastPrice;
}
}
(3)一个管理被观察者的管理器(房屋中介)
/**
* 被观察者的管理器(房屋中介)
*/
public class HomeAgency {
private Map<String,Home> homeMap = new HashMap<>();
public void add(Home home){
homeMap.put(home.getName(), home);
}
public Home getHome(String name){
return homeMap.get(name);
}
//管理器单例
private HomeAgency(){}
public static HomeAgency getInstance(){
return HomeAgencyInstance.instance;
}
private static class HomeAgencyInstance{
private static HomeAgency instance = new HomeAgency();
}
}
(4)测试类
public class App {
public static void main(String[] args) {
//定义两个楼盘 五个购房者
Home home1 = new Home("万科-楼盘");//毛坯
Home home2 = new Home("恒大-楼盘");//精装修
People people1 = new People("东邪");
People people2 = new People("西毒");
People people3 = new People("南帝");
People people4 = new People("北丐");
People people5 = new People("中神通");
//东南西北关注了恒大的楼盘(没有钱,只能买毛坯房)
people1.subscribe("万科-楼盘");
people2.subscribe("万科-楼盘");
people3.subscribe("万科-楼盘");
people4.subscribe("万科-楼盘");
//中神通比较有钱 关注了精装修的恒大楼盘
people5.subscribe("恒大-楼盘");
home1.changePrice(9000);
home2.changePrice(13000);
//后来,中神通没钱了,他就放弃了关注恒大楼盘,转去关注万科楼盘
people5.unsubscribe("恒大-楼盘");
people5.subscribe("万科-楼盘");
//恒大楼盘再次涨价将不会通知中神通,反之万科楼盘价格变动会通知中神通
home1.changePrice(9100);
home2.changePrice(150000);
}
}
运行结果
2017-11-19_165827.png
我们使用观察者模式的用意是为了让房子不在关系他价格变动都去通知谁,更重要的是他不需要关系他通知的购房者还是其他人,他只知道这个人实现了观察者接口,即我们的观察者依赖的只是一个抽象的观察者接口,而不关心观察者的具体实现。
另外让购房者来选择自己关注的楼盘,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有N多观察者,所以让被观察者自己维护这个列表会很艰难,这就像一个老师被许多学生认识,那么是所有的学生都记住老师的名字简单,还是让老师记住N多学生的名字简单?答案显而易见,让学生们都记住一个老师的名字是最简单的。
以上就是设计模式中的观察者模式