Java进阶篇设计模式之十三 - 观察者模式和空对象模式
前言
在上一篇中我们学习了行为型模式的备忘录模式(Memento Pattern)和状态模式(Memento Pattern)。本篇则来学习下行为型模式的最后两个模式,观察者模式(Observer Pattern)和空对象模式模式(NullObject Pattern)。
观察者模式
简介
观察者模式又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。。
其主要目的是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式主要由这四个角色组成,抽象主题角色(Subject)、具体主题角色(ConcreteSubject)、抽象观察者角色(Observer)和具体观察者角色(ConcreteObserver)。
- 抽象主题角色(Subject):它把所有观察者对象的引用保存到一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
- 具体主题角色(ConcreteSubject):将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。
- 抽象观察者角色(Observer):主要是负责从备忘录对象中恢复对象的状态。
示例图如下:
我们这里用一个示例来进行说明吧。
我们在视频网站进行看剧追番的时候,一般会有一个订阅功能,如果对某个番剧点了订阅,那么该番剧在更新的时候会向订阅该番剧的用户推送已经更新的消息,如果取消了订阅或者没有订阅,那么用户便不会收到该消息。
那么我们可以根据这个场景来使用备忘录模式来进行开发。
首先定义一个抽象主题, 将观察者(订阅者)聚集起来,可以进行新增、删除和通知,这里就可以当做番剧。
代码如下:
interface BangumiSubject{ void toThem(UserObserver user); void callOff(UserObserver user); void notifyUser(); }
然后再定义一个抽象观察者,有一个主要的方法update,主要是在得到通知时进行更新,这里就可以当做是用户。
代码如下:
interface UserObserver{ void update(String bangumi); String getName(); }
然后再定义一个具体主题,实现了抽象主题(BangumiSubject)接口的方法,同时通过一个List集合保存观察者的信息,当需要通知观察者的时候,遍历通知即可。
代码如下:
class Bangumi implements BangumiSubject { private List<UserObserver> list; private String anime; public Bangumi(String anime) { this.anime = anime; list = new ArrayList<UserObserver>(); } @Override public void toThem(UserObserver user) { System.out.println("用户"+user.getName()+"订阅了"+anime+"!"); list.add(user); } @Override public void callOff(UserObserver user) { if(!list.isEmpty()) System.out.println("用户"+user.getName()+"取消订阅"+anime+"!"); list.remove(user); } @Override public void notifyUser() { System.out.println(anime+"更新了!开始通知订阅该番剧的用户!"); list.forEach(user-> user.update(anime) ); } }
最后再定义了一个具体观察者,实现抽象观察者(UserObserver)接口的方法。
代码如下:
class User implements UserObserver{ private String name; public User(String name){ this.name = name; } @Override public void update(String bangumi) { System.out.println(name+"订阅的番剧: " + bangumi+"更新啦!"); } @Override public String getName() { return name; } }
编写好之后,那么我们来进行测试。
这里我们定义两个用户角色,张三和xuwujing,他们都订阅了<冰菓>和<fate/zero>番剧,当番剧更新的时候,他们就会收到通知。 如果他们取消了该番剧的订阅,那么他就不会收到该番剧的通知了。
相应的测试代码如下:
public static void main(String[] args) { String name1 ="张三"; String name2 ="xuwujing"; String bingguo = "冰菓"; String fate = "fate/zero"; BangumiSubject bs1 = new Bangumi(bingguo); BangumiSubject bs2 = new Bangumi(fate); UserObserver uo1 = new User(name1); UserObserver uo2 = new User(name2); //进行订阅 bs1.toThem(uo1); bs1.toThem(uo2); bs2.toThem(uo1); bs2.toThem(uo2); //进行通知 bs1.notifyUser(); bs2.notifyUser(); //取消订阅 bs1.callOff(uo1); bs2.callOff(uo2); //进行通知 bs1.notifyUser(); bs2.notifyUser(); }
输出结果:
用户张三订阅了冰菓! 用户xuwujing订阅了冰菓! 用户张三订阅了fate/zero! 用户xuwujing订阅了fate/zero! 冰菓更新了!开始通知订阅该番剧的用户! 张三订阅的番剧: 冰菓更新啦! xuwujing订阅的番剧: 冰菓更新啦! fate/zero更新了!开始通知订阅该番剧的用户! 张三订阅的番剧: fate/zero更新啦! xuwujing订阅的番剧: fate/zero更新啦! 用户张三取消订阅冰菓! 用户xuwujing取消订阅fate/zero! 冰菓更新了!开始通知订阅该番剧的用户! xuwujing订阅的番剧: 冰菓更新啦! fate/zero更新了!开始通知订阅该番剧的用户! 张三订阅的番剧: fate/zero更新啦!
观察者模式优点:
解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。
观察者模式缺点
如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景:
需要关联行为的场景;
事件需要创建一个触发链的场景,比如监控;
跨系统的消息交换场景,比如消息队列、事件总线的处理机制。
注意事项:
如果顺序执行,某一观察者错误会导致系统卡壳,建议采用异步方式。
空对象模式
简介
空对象模式(NullObject Pattern)主要是通过一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。 这样的Null 对象也可以在数据不可用的时候提供默认的行为。
其主要目的是在进行调用是不返回Null,而是返回一个空对象,防止空指针异常。
空对象模式,作为一种被基本遗忘的设计模式,但却有着不能被遗忘的作用。为什么说这么说呢,因为这种模式几乎难以见到和使用,不是它不够好用,也不是使用场景少 ,而是相比于简单的空值判断,使用它会显得比较复杂,至于为什么这么说,我们可以通过以下示例来进行说明。
假如我们要根据用户在已存的数据中进行查找相关信息,并且将它的信息给返回回来的话,那么一般我们是通过该用户的名称在数据库中进行查找,然后将数据返回,但是在数据库中进行查找时,很有可能没有该用户的信息,因此返回Null,如果稍不注意,就会出现空指针异常。这时我们一般的做法是,查询之后判断该数据是否为Null,如果为Null,就告知客户端没有这条数据,虽然这么做可以防止空指针异常,但是类似该方法过多,并且返回的信息实体为同一个的时候,我们每次都需要判断,就有点过于繁琐。那么这时我们就可以使用空对象模式来实现这方面的功能。
首先定义一个抽象角色,有获取姓名和判断是否为空的方法,这个抽象类的代码如下:
interface AbstractUser { String getName(); boolean isNull(); }
定义好该抽象类之后,我们再来定义具体实现类。这里定义两实现个类,一个表示是真实的用户,返回真实的姓名,一个是不存在的用户,用另一种方式返回数据,可以告知客户端该用户不存在,预防空指针。
代码如下:
class RealUser implements AbstractUser { private String name; public RealUser(String name) { this.name = name; } @Override public String getName() { return name; } @Override public boolean isNull() { return false; } } class NullUser implements AbstractUser { @Override public String getName() { return "user is not exist"; } @Override public boolean isNull() { return true; } }
然后在来定义一个工厂角色,用于对客户端提供一个接口,返回查询信息。
代码如下:
class UserFactory { public static final String[] names = { "zhangsan", "lisi", "xuwujing" }; public static AbstractUser getUser(String name) { for (int i = 0; i < names.length; i++) { if (names[i].equalsIgnoreCase(name)) { return new RealUser(name); } } return new NullUser(); } }
最后再来进行测试,测试代码如下:
public static void main(String[] args) { AbstractUser au1 = UserFactory.getUser("wangwu"); AbstractUser au2 = UserFactory.getUser("xuwujing"); System.out.println(au1.isNull()); System.out.println(au1.getName()); System.out.println(au2.isNull()); System.out.println(au2.getName()); }
输出结果:
true user is not exist false xuwujing
空对象优点:
可以加强系统的稳固性,能有效防止空指针报错对整个系统的影响;
不依赖客户端便可以保证系统的稳定性;
空对象缺点:
需要编写较多的代码来实现空值的判断,从某种方面来说不划算;
使用场景:
需要大量对空值进行判断的时候;