问题提出:
在生活实际中,经常会遇到多种对象关注一个对象数据变化的情况。例如,生活中有温度记录仪,当温度发生变化时,需要完成如下功能:记录温度日志,显示温度变化曲线,当温度越界时扬声器发出声音。可能写出以下程序段。
While(温度变化){
记录温度日志;
显示温度变化曲线;
当温度越界时扬声器发出声音;
}
这种方法把所有功能集成字一起,但是当需求发生变化,例如新增新的温度监测功能或者要删除某种功能,程序都得修改,这就是我们不希望看到的结果。观察者设计模式则是解决这类问题的有效办法。
观察者模式设计两种角色:主题和观察者。在上面的例子中,温度无疑就是主题,而记录温度日志,显示温度变化曲线,当温度越界时扬声器发出声音 即是三个观察者。观察者需要时刻“关注”主题的变化而作出不同的工作,就好像程序员都要围绕着开发需求一样编写代码,需求一改,我们需要立马改代码!明白了这两种角色之后,下面来仔细看看这两者之间的关系需要有什么功能。
开发需求是经理定的,对于经理来说,他需要知道有哪几个程序员为它工作,并且它根据需求可以新增或者剔除为它工作的程序员。那由此可以得出下面几个重要结论。
1)主题要知道有哪些观察者对其进行监测,所以主题类里面需要有集合类成员集合。
2)既然包含观察者对象集合,那么观察者必须是多态的,这就要求有共同的接口。
3)主题应该有的功能:添加观察者,撤销观察者,并向观察者发送消息,特别是“推数据”(下文会讨论)的模式。这三个功能固定,主题类可以从固定接口派生。
根据以上编写观察者设计模式,需要完成以下功能类。
1.主题ISubject接口定义
2.主题类编写
3.观察者接口IObserve定义
4.观察者类实现
UML图如下
关键代码如下
1)观察者接口IObserver
public interface IObserver { //观察者接口 public void refresh(String data); }
2)主题接口ISubject
public interface ISubject{ public void register(IObserver obs); //注册观察者 public void unregister(IObserver obs); //撤销观察者 public void notifyObservers(); //通知所有观察者 }
3)主题实现类Subject
public class Subject implements ISubject { private Vector<IObserver> vec = new Vector<IObserver>(); private String data; public String getData(){ return data; } public void setData(String data){ this.data = data; } public void register(IObserver obs){ //主题添加观察者 vec.add(obs); } public void unregister(IObserver obs){ //主题撤销观察者 vec.remove(obs); } public void notifyObservers(){ //主题通知所有观察者进行数据响应 for(int i=0;i<vec.size();i++){ IObserver obs = vec.get(i); obs.refresh(data); } } }
4)具体观察者Observer
public class Observer implements IObserver { public void refresh(String data){ System.out.println("I have received the data:" + data); } }
5)测试类Test
public class Test { public static void main(String[] args) { IObserver obs = new Observer(); Subject subject = new Subject(); subject.register(obs); subject.setData("Hello World!"); subject.notifyObservers(); } }
有了基本的了解之后,下面再深入地剖析一下观察者模式ba
1.推数据与拉数据
推数据,简单理解就是当主题的数据变动时主动发送数据给观察者,提醒观察者数据有所变动。而拉数据,顾名思义也就是观察者主动索取主题的数据,并不由主题主动发送。那上面我们的代码样例,你说是推数据还是拉数据?当然是推数据(这应该不难看出来)。
在拉数据模式中,观察者子类对象必须能获取主题Subject对象,代码示例如下。
IObserver
public interface IObserver{ //观察者接口 public void refresh(ISubject obj); //采用“拉”数据方式 }
ISubject 同上这里就不再重复列出
Subject
public class Subject implements ISubject { private Vector<IObserver> vec = new Vector<IObserver>(); private String data; public String getData(){ return data; } public void setData(String data){ this.data = data; } public void register(IObserver obs){ //主题添加观察者 vec.add(obs); } public void unregister(IObserver obs){ //主题撤销观察者 vec.remove(obs); } public void notifyObservers(){ //主题通知所有观察者进行数据响应 for(int i=0;i<vec.size();i++){ IObserver obs = vec.get(i); obs.refresh(this); //这里有所不同 } } }
Observer
public class Observer implements IObserver { public void refresh(ISubject obj){ Subject subject = (Subject)obj; System.out.println("I have received the data:" + subject.getData(); } }
UML如下
2.增加抽象类层AbstractSubject
在前面我们已经分析了主题应该有的功能,而大部分主题都有类似的功能,因为是比较通用的方法。那么每个主题类的代码就显得重复了,所以用一个中间层来解决代码重复问题是一个比较好的方法。
public abstract class AbstractSubject implements ISubject{ Vector<IObserver> vec = new Vector<IObserver>(); public void register(IObserver obs){ if(!vec.contains(obs)){ vec.add(obs); } } public void unregister(IObserver obs){ if(vec.contains(obs)){ vec.remove(obs); } } public void notifyObservers(){ for(int i=0;i<vec.size();i++){ IObserver obs = vec.get(i); obs.refresh(this); } } }
3.泛型的设计
上面的代码中,有一个欠缺的问题就是,主题的数据data并不一定是String类型,于是我们想到应该把接口代码改为泛型。不仅仅主题ISubject需要更改,当然IObserver也要改为泛型接口。这里就不演示代码了。
4.JDK中的观察者设计模式
JDK的java.util包提供了系统的主题类Observable类以及观察者Observer,其(部分)UML类图如下
很明显,Observer类相当于上面的IObserver观察者接口类,其中的update方法中第一个参数是Observable类型,表明采用“拉”数据方式;Observable相当于上面的主题类Subject。需要主要的是hasChange()方法主要是设置或获得changed成员变量的值或者状态,changed为true时表明主题中心的数据发生了变化。
下面我们利用JDK中的Observer,Observable完成观察者模式
Subject类
public class Subject extends java.util.Observable { String data; public String getData(){ return data; } public void setData(String data){ this.data = data; //更新数据 setChanged(); //置更新数据标志 notifyObservers(null); //通知各个具体观察者 } }
OneObserver
public class OneObserver implements java.util.Observer { public void update(Observable arg0,Object arg1){ Subject subject = (Subject)arg0; System.out.println("The data is :" + subject.getData()); } }
简单测试
public class Test { public static void main(String[] args) throws Exception{ java.util.Observer obj = new OneObserver(); Subject s = new Subject(); s.addObserver(obj); s.setData("Hello World!"); } }
最后,当然是给一个应用场景(机房温度监测仿真功能)
监测一个机房的温度数据。要求 1.定间距采集温度数值 2.记录采集温度数值 3.标识异常温度数据 4.当温度连续超过比较值n次,发送报警信息
分析:监测功能是以温度为中心的,因此观察者模式实现程序架构比较方便。
总体思想如下:温度作为主体类,两个观察者,一个负责记录数据,另一个观察者负责处理异常。将时间采样间距数据,温度异常值等记录在xml配置文件中,报警信息的发送邮件处理。
mysql的数据表设计 normal表记录所有温度记录,abnormal表记录异常温度记录。这样的好处是abnormal表的记录远比normal表的记录少得多,将来查询异常记录信息会非常快。数据的产生器采用反射技术。具体代码如下
mysql表的简单设计
create table normal( wenduvalue int, recordtime Date ) create table abnormal( abnormalvalue int, recordtime Date )
mysql封装处理类
import java.sql.*; import java.util.List; import java.util.Vector; /** * Created by lenovo on 2017/4/18. */ public class DbProc { private String strDriver = "com.mysql.jdbc.Driver"; private String strDb = "jdbc:mysql://localhost:3306/buildModel"; private String strUser = "root"; private String strPwd = ""; //注意测试时候strPwd要加上自己本地mysql的账户密码 private Connection conn; public Connection connect() throws Exception{ Class.forName(strDriver); conn = DriverManager.getConnection(strDb,strUser,strPwd); return conn; } public int executeUpdate(String strSQL) throws Exception{ Statement stm = conn.createStatement(); int n = stm.executeUpdate(strSQL); stm.close(); return n; } public List executeQuery(String strSQL) throws Exception{ List l = new Vector(); Statement stm = conn.createStatement(); ResultSet rst = stm.executeQuery(strSQL); ResultSetMetaData rsmd = rst.getMetaData(); while(rst.next()){ Vector unit = new Vector(); for(int i=1;i<=rsmd.getColumnCount();i++){ unit.add(rst.getString(i)); } l.add(unit); } return l; } public void close() throws Exception{ conn.close(); } }
info.xml
<?xml version="1.0" encoding="utf-8" standalone="no" ?> <!DOCTYPE properties SYSTEM "Http://java.sun.com/dtd/properties.dtd"> <properties> <comment>Observer</comment> <entry key="range">2</entry> <entry key="limit">30</entry> <entry key="nums">5</entry> <entry key="address"></entry> <!--邮箱地址自己填写--> <entry key="reflect">DataRandom</entry> </properties>
条件类factor
package Observer.Example; /** * Created by lenovo on 2017/4/19. */ //用于主题向观察者传送条件对象 public class Factor { private int limit; //温度预警值 private int times; //连续越过预警值次数极限值 private String address; //邮件地址 public Factor(int limit, int times, String address) { this.limit = limit; this.times = times; this.address = address; } public int getLimit() { return limit; } public int getTimes() { return times; } public String getAddress() { return address; } }
主题类Subject
package Observer.Example; /** * Created by lenovo on 2017/4/19. */ public class Subject extends java.util.Observable { private int data; private Factor factor; public void setFactor(Factor factor){ //设置条件对象 this.factor = factor; } public int getData(){ return data; } public void setData(int data){ this.data = data; setChanged(); //observable类中的方法 notifyObservers(factor); //将条件对象广播给各观察者 } }
数据记录观察者类DataObserver
package Observer.Example; import BuildModel.Example.DbProc; import java.util.Observable; import java.util.Observer; /** * Created by lenovo on 2017/4/19. */ public class DataObserver implements Observer { //将采集到的所有数据保存到normal表中 public void update(Observable obj,Object factor){ Subject subject = (Subject)obj; String strSQL = "insert into normal values(" + subject.getData() + ",now())"; DbProc dbobj = new DbProc(); try{ dbobj.connect(); dbobj.executeUpdate(strSQL); dbobj.close(); } catch (Exception ex){ ex.printStackTrace(); } } }
异常数据观察者类AbnormalObserver
package Observer.Example; import BuildModel.Example.DbProc; import javax.mail.Message; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import java.util.Observable; import java.util.Observer; import java.util.Properties; /** * Created by lenovo on 2017/4/19. */ public class AbnormalObserver implements Observer { private int c = 0; //温度异常值累积 public void update(Observable obj,Object factor){ Subject subject = (Subject)obj; Factor fac = (Factor)factor; if(subject.getData()<fac.getLimit()){ //若采集温度值<条件温度预警值 c = 0; return ; } c ++ ; saveToAbnormal(subject); //将越界数据保存到异常数据表 if(c == fac.getTimes()){ //如越界累积次数=条件极限次数 sendEmail(fac); //则发送邮件 c = 0; //重新开始累积 } } private void saveToAbnormal(Subject subject){ String strSQL = "insert into abnormal values(" + subject.getData() + ",now())"; DbProc dbProc = new DbProc(); try{ dbProc.connect(); dbProc.executeUpdate(strSQL); dbProc.close(); } catch (Exception ex){ ex.printStackTrace(); } } private void sendEmail(Factor factor){ String host = "smtp.163.com"; String from = ""; //发件人地址 String to = factor.getAddress(); String userName = ""; String pwd = ""; Properties props = new Properties(); props.put("mail.smtp.host",host); props.put("mail.smtp.auth","true"); Session session = Session.getDefaultInstance(props); session.setDebug(true); MimeMessage msg = new MimeMessage(session); try{ msg.setFrom(new InternetAddress(from)); msg.addRecipient(Message.RecipientType.TO , new InternetAddress(to)); msg.setSubject("温度预警信息"); msg.setText("机房处于异常状态"); msg.saveChanges(); Transport transport = session.getTransport("smtp"); transport.connect(host,userName,pwd); transport.sendMessage(msg,msg.getRecipients(Message.RecipientType.TO)); } catch (Exception ex){ ex.printStackTrace(); } } }
仿真数据生成器类均从ISimuData自定义接口派生。
ISimuData接口
public interface ISimuData<T> { void open(); void close(); boolean hasNext(); T next(); }
DataRandom 继承ISimuData接口,生成数据的方法多种多样,这里读取record.txt文件的数据
public class DataRandom implements ISimuData<Integer>{ Scanner input = null; public void open(){ File file = new File("record.txt"); try { input = new Scanner(file); } catch (Exception ex){ ex.printStackTrace(); } } public void close() { input.close(); } public boolean hasNext() { if(input.hasNext()){ return true; } return false; } public Integer next() { return Integer.parseInt(input.next()); } }
Test测试类
package Observer.Example; import java.io.FileInputStream; import java.util.Observer; import java.util.Properties; /** * Created by lenovo on 2017/4/19. */ public class Test { public static void main(String[] args) throws Exception{ // File directory = new File("");//设定为当前文件夹 // System.out.println(directory.getCanonicalPath()); FileInputStream in = new FileInputStream("info.xml"); Properties p = new Properties(); p.loadFromXML(in); int range = Integer.parseInt(p.getProperty("range")); String reflectClassName = p.getProperty("reflect"); int limit = Integer.parseInt(p.getProperty("limit")); int nums = Integer.parseInt(p.getProperty("nums")); String address = p.getProperty("address"); Factor factor = new Factor(limit,nums,address); in.close(); Subject s = new Subject(); Observer obj = new DataObserver(); Observer obj2 = new AbnormalObserver(); s.addObserver(obj); s.addObserver(obj2); s.setFactor(factor); //主题设置条件,以备广播给观察者对象用 //利用反射技术数据仿真 ISimuData<Integer> sdobj = (ISimuData)Class.forName(reflectClassName).newInstance(); sdobj.open(); while(sdobj.hasNext()){ int value = sdobj.next(); s.setData(value); try{ Thread.sleep(range*1000); } catch (Exception e){ e.printStackTrace(); } } sdobj.close(); } }