Java设计模式:观察者模式

问题提出:

在生活实际中,经常会遇到多种对象关注一个对象数据变化的情况。例如,生活中有温度记录仪,当温度发生变化时,需要完成如下功能:记录温度日志,显示温度变化曲线,当温度越界时扬声器发出声音。可能写出以下程序段。

While(温度变化){

  记录温度日志;

  显示温度变化曲线;

  当温度越界时扬声器发出声音;

}

这种方法把所有功能集成字一起,但是当需求发生变化,例如新增新的温度监测功能或者要删除某种功能,程序都得修改,这就是我们不希望看到的结果。观察者设计模式则是解决这类问题的有效办法。

观察者模式设计两种角色:主题和观察者。在上面的例子中,温度无疑就是主题,而记录温度日志,显示温度变化曲线,当温度越界时扬声器发出声音 即是三个观察者。观察者需要时刻“关注”主题的变化而作出不同的工作,就好像程序员都要围绕着开发需求一样编写代码,需求一改,我们需要立马改代码!明白了这两种角色之后,下面来仔细看看这两者之间的关系需要有什么功能。

开发需求是经理定的,对于经理来说,他需要知道有哪几个程序员为它工作,并且它根据需求可以新增或者剔除为它工作的程序员。那由此可以得出下面几个重要结论。

1)主题要知道有哪些观察者对其进行监测,所以主题类里面需要有集合类成员集合。

2)既然包含观察者对象集合,那么观察者必须是多态的,这就要求有共同的接口。

3)主题应该有的功能:添加观察者,撤销观察者,并向观察者发送消息,特别是“推数据”(下文会讨论)的模式。这三个功能固定,主题类可以从固定接口派生。

根据以上编写观察者设计模式,需要完成以下功能类。

1.主题ISubject接口定义

2.主题类编写

3.观察者接口IObserve定义

4.观察者类实现

UML图如下

Java设计模式:观察者模式

关键代码如下

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();
    }
}
View Code

有了基本的了解之后,下面再深入地剖析一下观察者模式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如下

Java设计模式:观察者模式

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);
        }
    }
}
View Code

3.泛型的设计

上面的代码中,有一个欠缺的问题就是,主题的数据data并不一定是String类型,于是我们想到应该把接口代码改为泛型。不仅仅主题ISubject需要更改,当然IObserver也要改为泛型接口。这里就不演示代码了。

4.JDK中的观察者设计模式

JDK的java.util包提供了系统的主题类Observable类以及观察者Observer,其(部分)UML类图如下

Java设计模式:观察者模式

很明显,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!");
    }
}
View Code

最后,当然是给一个应用场景(机房温度监测仿真功能)

监测一个机房的温度数据。要求 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
)
View Code

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();
    }
}
View Code

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>
View Code

条件类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;
    }
}
View Code

主题类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);                //将条件对象广播给各观察者
    }
}
View Code

数据记录观察者类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();
        }
    }
}
View Code

异常数据观察者类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();
        }
    }
}
View Code

仿真数据生成器类均从ISimuData自定义接口派生。

ISimuData接口

public interface ISimuData<T> {
    void open();
    void close();
    boolean hasNext();
    T next();
}
View Code

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());
    }
}
View Code

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();
    }
}
View Code