zoukankan      html  css  js  c++  java
  • Java设计模式:观察者模式

    问题提出:

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

    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();
        }
    }
    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如下

    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类图如下

    很明显,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
  • 相关阅读:
    Spring:@ConfigurationProperties配置绑定
    Linux:性能诊断
    【第二章】:深浅拷贝剖析
    【第二章】:模块和运算符
    python 扩展注册功能装饰器举例
    python 函数 之 用户注册register()
    python 之 函数 基础
    python 函数
    python 文件操作
    python 的 数据类型
  • 原文地址:https://www.cnblogs.com/lateink/p/6736581.html
Copyright © 2011-2022 走看看