目录
一、观察者模式介绍
二、实例讲解
三、在Srping 中整合“观察者模式”
四、扩展性体现
五、Java自带观察者模式支持
引言
在java项目开发中,经常会把一些重要的数据放到数据库里,但如果这些数据在程序中会经常被使用到,就会频繁的查询数据库,导致数据库压力增加。常见的做法是把这些数据放到缓存里,比如redis等全局共享缓存,这样每次使用的使用时候不用查询数据库。
但如果使用redis作为缓存,每次读取都有网络开销。如果数据量不大的情况下,可以在程序启动时把这部分数据加载到jvm内存中(比如,public static的变量),每次使用这些数据的时候,直接从本地jvm内存中获取 性能方面势必会好很多,而且减少了外部依赖,系统稳定性方面也会好一些。但也有一个缺点:现在的程序都是多实例部署(多个jvm实例),每个jvm实例内存中都有一份数据,如果数据有修改,要同步更新每个jvm内存中数据 会有些困难。
常见的解决方案是引入配置管理系统(类似淘宝的diamond),多个jvm实例会向“配置管理系统”中的某个配置文件进行注册,当这些配置文件发送变化时,就会通知各个jvm实例拉取最新的配置。其实这个场景就是一个“观察者模式”的放大化使用,可以把“配置管理系统”中的某个配置文件看做是“主题”,多个jvm实例看做是“观察者”。当主题发生变化时,会通知观察者拉取最新的内容。
一、观察者介绍
定义了对象之间的一对多依赖关系,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。其中“一”的一方称为“主题对象”,“多”的一方称为“观察者对象”。采用此模式的优点:
1、“主题对象”和“观察者对象”之间相互隔离,彼此内部业务有修改,互不干扰。
2、“观察者对象”之间相互隔离,新增或删除“观察者对象”,不会影响其他“观察者对象”,具备良好的扩展性。
观察者模式中有4类角色:“抽象的主题”(接口 Subject)、“抽象的观察者”(接口 Observer)、“具体的主题”(实现 ConcreteSubject)、“具体的观察者”(实现 ConcreteObserver,可以有多个)。类图关系如下:
(来自互联网)
“推送型”和“拉取型”
根据数据的同步方式,“观察者模式”又分为“推送型”和“拉取型”:
1、“推送型”:观察者的update方法参数中包含所有的业务数据,“具体的观察者”根据自己的业务获取自己需要的数据。优点是数据隔离性好,不会对主题对象造成破坏;缺点是 如果参数过多看起来会比较臃肿,有些观察者不需要的数据也会被推送。
2、“拉取型” :在“具体的主题对象”实现中提供一些获取数据的public方法,在update方法中直接调用这些方法获取自己需要的数据。优点是 自己需要什么数据自己去拿,缺点是 如果“具体的主题对象”实现设计不当,容易暴露一些私有的数据和方法。
当然也不是绝对的只有这两种,有些主题里的数据量很少,有可能只需要一个状态变化来触发“具体的观察者”对象的update方法做一些业务操作即可,不需要获取主题中的数据。下面实例讲解中的案例,就属于这种情况。
二、实例讲解
“观察者模式”最简单明了的例子是《Head First》设计模式一书中“气象观察站”的例子,可以网上搜索了解下,这里不细讲。实际项目中,很难遇到与此场景完全匹配的例子,但整体思路是一致的,根据实际情况调整即可。
这里引用最近项目中的一个实际例子进行讲解,本实例完整代码详见github:https://github.com/gantianxing/observer-test.git。
回到文章开头的场景,最近一个项目中需要把一些常用的数据放到jvm内存,引入配置管理系统进行数据同步。由于配置数据项较多,本项目中没有把配置数据直接以配置文件的形式放到“配置管理系统”,而是把具体的数据放到数据库,再把每项数据是否修改做出开关配置放到“配置管理系统”。数据同步流程为:
1、程序启动时,从数据库读取数据放到jvm内存。
2、当数据库中某条数据发生变化时:“数据管理后台”发布新数据到数据库,同时修改“配置管理系统”中的配置开关。这里有一个“配置开关”列表,对应数据库中的多个数据项。
3、各个jvm实例获取到变化的配置开关,触发再次读取数据库中的新数据 同步到jvm内存。
这里的“配置开关”列表即为“具体的主题对象”,“多个数据项”对应的多个同步类即为:“具体的观察者”。上述流程关系如下:
本次讲解忽略“数据管理后台”相关操作,主要关注 当开关列表发生变化时,触发对应的“同步类”从数据库拉取最新数据。该部分流程,采用“观察者模式”实现:
1、抽象的主题(ConfigSubject):
public interface ConfigSubject { void registerObserver(String key, ConfigRloadService o);//注册观察者 void removeObserver(String key);//删除观察者 void notifyObervers(String conf);//通知观察者 }
2、抽象的观察者(ConfigRloadService):
public interface ConfigRloadService { void reload(); }
3、具体的主题(ConfigSubjectImpl):
@Component("subject") public class ConfigSubjectImpl implements ConfigSubject { //观察者列表 private Mapobservers = new HashMap(); @Override public void registerObserver(String key,ConfigRloadService o) { observers.put(key, o); } @Override public void removeObserver(String key) { observers.remove(key); } /** * 通知对应的观察者 * @param conf */ @Override public void notifyObervers(String conf) { if(conf!=null && conf.length()>0){ String [] configAray=conf.split("\\r\\n"); //回车换行做为配置分割,一行一个配置项 for(String one:configAray){ String [] oneArray = one.split("="); if(oneArray.length !=2){ continue; } String key = oneArray[0]; String value = oneArray[1]; int iv_new = Integer.parseInt(value); int iv_old = SwitchEnum.valueOf(key).getValue(); if(iv_new != iv_old){//如果配置开关不相等,说明配置内容已经更改,需要重新reload数据 ConfigRloadService service = observers.get(key); if(service != null){ service.reload(); } SwitchEnum.valueOf(key).setValue(iv_new); } } } } }
4、具体的观察者(两个:ActPcRloadServiceImpl、ActMobRloadServiceImpl)
@Component public class ActPcRloadServiceImpl implements ConfigRloadService { @Resource private ConfigSubject subject; public ActPcRloadServiceImpl(ConfigSubject subject){ this.subject = subject; subject.registerObserver(SwitchEnum.actPcConf.name(),this); } @Override public void reload() { //查询数据库加载最新的配置内容到jvm内存,代码逻辑省略 System.out.println("pc版活动配置reload"); } }
@Component public class ActMobRloadServiceImpl implements ConfigRloadService { @Resource private ConfigSubject subject; public ActMobRloadServiceImpl(ConfigSubject subject){ this.subject = subject; subject.registerObserver(SwitchEnum.actMobConf.name(),this); } @Override public void reload() { //查询数据库加载最新的配置内容到jvm内存,代码逻辑省略 System.out.println("移动端活动配置reload"); } }
辅助常量枚举类,对应多个配置开关,这里只有两个,分别对应上述两个“具体的观察者”:
public enum SwitchEnum { actMobConf(0), actPcConf(0); private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } SwitchEnum(int value){ this.value = value; } }
测试类:
public class Main { public static void main(String[] args) { String conf = "actMobConf=1\r\nactPcConf=0";//模拟开关配置发生改变 //step1 初始化主题和观察者,在spring中可以用spring ioc自动注入代替 ConfigSubject subject = new ConfigSubjectImpl();//创建主题 ConfigRloadService actMobRloadServiceImpl = new ActMobRloadServiceImpl(subject);//创建观察者 ConfigRloadService actPcRloadServiceImpl = new ActPcRloadServiceImpl(subject);//创建观察者 //step2 模拟开关配置改变,触发观察者reload方法 subject.notifyObervers(conf); } }
其中String conf = "actMobConf=1\r\nactPcConf=0"; 这里有两个配置开关,分别对应SwitchEnum中的两个配置开关,修改String conf中配置值与SwitchEnum中默认的默认值不同,即可触发指定的“同步类”从数据库拉取新数据。这里把actMobConf改为了1,按逻辑会触发ActMobRloadServiceImpl的reload方法执行。运行上述main方法,查看结果:
移动端活动配置reload
说明测试结果与预期相符。
三、在Srping 中整合“观察者模式”
上一节使用的main方法来实例化bean,在实战中我们一般都是使用的Spring ioc容器,使用起来更方便,直接添加@Component注解即可。这里就不细讲,可以直在tomcat中运行github: https://github.com/gantianxing/observer-test.git中的代码,启动后访问http://localhost
,修改配置项 观察控制台日志看效果。
观察控制台日志:
四、扩展性体现
假设现在业务新增一项数据 需要同步到jvm内存,此时只需要三步操作:
1、新增一个同步类XXXRloadServiceImpl(实现ConfigRloadService接口),实现自己的数据加载方法reload。
2、在枚举类SwitchEnum中新增一个配置项xxxPcConf(0)。
3、在配置管理系统的 配置开关列表中,新增一个开关xxxPcConf=1,即可实现数据同步。
可见整个扩展过程,对原有逻辑没有任何影响。
五、Java自带观察者模式支持
由于观察者模式是一个常见的设计模式,Java api提供的两个工具类对“观察者模式”进行支持,java.util包中的Observable类 和Observer接口:
Observable类对应“抽象的主题”,只是这里不是接口,“具体的主题”需要继承该类。
Observer接口对应“抽象的观察者”,“具体的观察者”实现该接口即可。
有些简单的场景可以直接使用,但有一定局限,比如:
1、Observable主题类中的“观察者”列表,是用的Vector存储。上述示例中需要用Map就无能为力。
private Vector
2、notifyObservers方法,遍历所有的“观察者”,执行其update方法。上述示例中如果需要过滤部分“观察者”执行其update方法,也无能为力。
其实观察者模式实现起来也比较简单,根据自己的业务自己实现也不难,java自带的可以作为参考即可,尤其是的Observable类的设计思想。
转载请注明出处: