12.1  场景问题
12.1.1  订阅报纸的过程
来考虑实际生活中订阅报纸的过程,这里简单总结了一下,订阅报纸的基本流程如下:
首先按照自己的需要选择合适的报纸,具体的报刊杂志目录可以从邮局获取。
选择好后,就到邮局去填写订阅单,同时交上所需的费用。
至此,就完成了报纸的订阅过程,接下去的就是耐心等候,报社会按照出报时间推出报纸,然后报纸会被送到每个订阅人的手里。
       画个图来描述上述过程,如图12.1所示:
 
图12.1  订阅报纸的过程示意图
虽然看起来订阅者是直接跟邮局在打交道,但实际上,订阅者的订阅数据是会被邮局传递到报社的,当报社出版了报纸,报社会按照订阅信息把报纸交给邮局,然后由邮局来代为发送到订阅者的手中。所以在整个过程中,邮局只不过起到一个中转的作用,为了简单,我们去掉邮局,让订阅者直接和报社交互,如图12.2所示:
 
图12.2  简化的订阅报纸过程示意图
12.1.2  订阅报纸的问题
       在上述过程中,订阅者在完成订阅后,最关心的问题就是何时能收到新出的报纸。幸好在现实生活中,报纸都是定期出版,这样发放到订阅者手中也基本上有一个大致的时间范围,差不多到时间了,订阅者就会看看邮箱,查收新的报纸。
要是报纸出版的时间不固定呢?
那订阅者就麻烦了,如果订阅者想要第一时间阅读到新报纸,恐怕只能天天守着邮箱了,这未免也太痛苦了吧。
继续引申一下,用类来描述上述的过程,描述如下:
订阅者类向出版者类订阅报纸,很明显不会只有一个订阅者订阅报纸,订阅者类可以有很多;当出版者类出版新报纸的时候,多个订阅者类如何知道呢?还有订阅者类如何得到新报纸的内容呢?
把上面的问题对比描述一下:
 
       进一步抽象描述这个问题:当一个对象的状态发生改变的时候,如何让依赖于它的所有对象得到通知,并进行相应的处理呢?
       该如何解决这样的问题?

解决方案 »

  1.   

     
    12.2  解决方案
    12.2.1  观察者模式来解决
    用来解决上述问题的一个合理的解决方案就是观察者模式。那么什么是观察者模式呢?
    (1)观察者模式定义
     
    (2)应用观察者模式来解决的思路
    在前面描述的订阅报纸的例子里面,对于报社来说,在一开始,它并不清楚究竟有多少个订阅者会来订阅报纸,因此,报社需要维护一个订阅者的列表,这样当报社出版报纸的时候,才能够把报纸发放到所有的订阅者手中。对于订阅者来说,订阅者也就是看报的读者,多个订阅者会订阅同一份报纸。
    这就出现了一个典型的一对多的对象关系,一个报纸对象,会有多个订阅者对象来订阅;当报纸出版的时候,也就是报纸对象改变的时候,需要通知所有的订阅者对象。那么怎么来建立并维护这样的关系呢?
    观察者模式可以处理这种问题,观察者模式把这多个订阅者称为观察者:Observer,多个观察者观察的对象被称为目标:Subject。
    一个目标可以有任意多个观察者对象,一旦目标的状态发生了改变,所有注册的观察者都会得到通知,然后各个观察者会对通知作出相应的响应,执行相应的业务功能处理,并使自己的状态和目标对象的状态保持一致。
    12.2.2  模式结构和说明
    观察者模式结构如图12.3所示:
     
    图12.3  观察者模式结构示意图
    Subject:
    目标对象,通常具有如下功能:
    一个目标可以被多个观察者观察
    目标提供对观察者注册和退订的维护
    当目标的状态发生变化时,目标负责通知所有注册的、有效的观察者
    Observer:
           定义观察者的接口,提供目标通知时对应的更新方法,这个更新方法进行相应的业务处理,可以在这个方法里面回调目标对象,以获取目标对象的数据。
    ConcreteSubject:
           具体的目标实现对象,用来维护目标状态,当目标对象的状态发生改变时,通知所有注册有效的观察者,让观察者执行相应的处理。
    ConcreteObserver:
           观察者的具体实现对象,用来接收目标的通知,并进行相应的后续处理,比如更新自身的状态以保持和目标的相应状态一致。
    12.2.3  观察者模式示例代码
    (1)先来看看目标对象的定义,示例代码如下:
    /**
     * 目标对象,它知道观察它的观察者,并提供注册和删除观察者的接口
     */
    public class Subject {
        /**
         * 用来保存注册的观察者对象
         */
        private List<Observer> observers = new ArrayList<Observer>();
        /**
         * 注册观察者对象
         * @param observer 观察者对象
         */
        public void attach(Observer observer) {
           observers.add(observer);
        }
        /**
         * 删除观察者对象
         * @param observer 观察者对象
         */
        public void detach(Observer observer) {
            observers.remove(observer);
        }
        /**
         * 通知所有注册的观察者对象
         */
        protected void notifyObservers() {
           for(Observer observer : observers){
               observer.update(this);
           }
        }
    }
    (2)接下来看看具体的目标对象,示例代码如下:
    /**
     * 具体的目标对象,负责把有关状态存入到相应的观察者对象,
     * 并在自己状态发生改变时,通知各个观察者
     */
    public class ConcreteSubject extends Subject {
        /**
         * 示意,目标对象的状态
         */
        private String subjectState;
        public String getSubjectState() {
           return subjectState;
        }
        public void setSubjectState(String subjectState) {
           this.subjectState = subjectState;
           //状态发生了改变,通知各个观察者
           this.notifyObservers();
        }
    }
    (3)再来看看观察者的接口定义,示例代码如下:
    /**
     * 观察者接口,定义一个更新的接口给那些在目标发生改变的时候被通知的对象
     */
    public interface Observer {
        /**
         * 更新的接口
         * @param subject 传入目标对象,好获取相应的目标对象的状态
         */
        public void update(Subject subject);
    }
    (4)接下来看看观察者的具体实现示意,示例代码如下:
    /**
     * 具体观察者对象,实现更新的方法,使自身的状态和目标的状态保持一致
     */
    public class ConcreteObserver implements Observer {
        /**
         * 示意,观者者的状态
         */
        private String observerState;
     
        public void update(Subject subject) {
           // 具体的更新实现
           //这里可能需要更新观察者的状态,使其与目标的状态保持一致
           observerState = ((ConcreteSubject)subject)
    .getSubjectState();
        }
    }
    12.2.4  使用观察者模式实现示例
    要使用观察者模式来实现示例,那就按照前面讲述的实现思路,把报纸对象当作目标,然后订阅者当做观察者,就可以实现出来了。
    使用观察者模式来实现示例的结构如图12.4所示:
     
    图12.4  使用观察者模式来实现示例的结构示意图
    还是来看看具体的代码实现。
    (1)被观察的目标
           在前面描述的订阅报纸的例子里面,多个订阅者都是在观察同一个报社对象,这个报社对象就是被观察的目标。这个目标的接口应该有些什么方法呢?还是从实际入手去想,看看报社都有些什么功能。报社最基本有如下的功能:
    注册订阅者,也就是说很多个人来订报纸,报社肯定要有相应的记录才行
    出版报纸,这个是报社的主要工作
    发行报纸,也就是要把出版的报纸发送到订阅者手中
    退订报纸,当订阅者不想要继续订阅了,可以取消订阅
    上面这些功能是报社最最基本的功能,当然,报社还有很多别的功能,为了简单起见,这里就不再去描述了。因此报社这个目标的接口也应该实现上述功能,把他们定义在目标接口里面,示例代码如下:
    /**
     * 目标对象,作为被观察者
     */
    public class Subject {
        /**
         * 用来保存注册的观察者对象,也就是报纸的订阅者
         */
        private List<Observer> readers = new ArrayList<Observer>();
     
        /**
         * 报纸的读者需要先向报社订阅,先要注册
         * @param reader 报纸的读者
         * @return 是否注册成功
         */
        public void attach(Observer reader) {
           readers.add(reader);
        }
        /**
         * 报纸的读者可以取消订阅
         * @param reader 报纸的读者
         * @return 是否取消成功
         */
        public void detach(Observer reader) {
           readers.remove(reader);
        }
        /**
         * 当每期报纸印刷出来后,就要迅速主动的被送到读者的手中,
         * 相当于通知读者,让他们知道
         */
        protected void notifyObservers() {
           for(Observer reader : readers){
               reader.update(this);
           }
        }
    }
           细心的朋友可能会发现,这个对象并没有定义出版报纸的功能,这是为了让这个对象更加通用,这个功能还是有的,放到具体报纸类里面去了,下面就来具体的看看具体的报纸类的实现。
    为了演示简单,在这个实现类里面增添一个属性,用它来保存报纸的内容,然后增添一个方法来修改这个属性,修改这个属性就相当于出版了新的报纸,并且同时通知所有的订阅者。示例代码如下:
    /**
     * 报纸对象,具体的目标实现
     */
    public class NewsPaper extends Subject{
        /**
         * 报纸的具体内容
         */
        private String content;
        /**
         * 获取报纸的具体内容
         * @return 报纸的具体内容
         */
        public String getContent() {
           return content;
        }
     
        /**
         * 示意,设置报纸的具体内容,相当于要出版报纸了
         * @param content 报纸的具体内容
         */
        public void setContent(String content) {
           this.content = content;
           //内容有了,说明又出报纸了,那就通知所有的读者
           notifyObservers();
        }
    }
    (2)观察者
    目标定义好过后,接下来把观察者抽象出来,看看它应该具有什么功能。分析前面的描述,发现观察者只要去邮局注册了过后,就是等着接收报纸就好了,没有什么其它的功能。那么就把这个接收报纸的功能抽象成为更新的方法,从而定义出观察者接口来,示例代码如下:
    /**
     * 观察者,比如报纸的读者
     */
    public interface Observer {
        /**
         * 被通知的方法
         * @param subject 具体的目标对象,可以获取报纸的内容
         */
        public void update(Subject subject);
    }
    定义好了观察者的接口过后,该来想想如何实现了。具体的观察者需要实现:在收到被通知的内容后,自身如何进行相应处理的功能。为了演示的简单,收到报纸内容过后,简单的输出一下,表示收到了就行了。
    定义一个简单的观察者实现,示例代码如下:
    /**
     * 真正的读者,为了简单就描述一下姓名
     */
    public class Reader implements Observer{
        /**
         * 读者的姓名
         */
        private String name;
     
        public void update(Subject subject) {
           //这是采用拉的方式
           System.out.println(name+"收到报纸了,阅读先。内容是==="
    +((NewsPaper)subject).getContent());
        }
         public String getName() {
           return name;
        }
        public void setName(String name) {
           this.name = name;
        }
    }
    (3)使用观察者模式
           前面定义好了观察者和观察的目标,那么如何使用它们呢?
           那就写个客户端,在客户端里面,先创建好一个报纸,作为被观察的目标,然后多创建几个读者作为观察者,当然需要把这些观察者都注册到目标里面去,接下来就可以出版报纸了,具体的示例代码如下:
    public class Client {
        public static void main(String[] args) {
           //创建一个报纸,作为被观察者
           NewsPaper subject = new NewsPaper();
           //创建阅读者,也就是观察者
           Reader reader1 = new Reader();
           reader1.setName("张三");
     
           Reader reader2 = new Reader();
           reader2.setName("李四");
     
           Reader reader3 = new Reader();
           reader3.setName("王五");
     
           //注册阅读者
           subject.attach(reader1);
           subject.attach(reader2);
           subject.attach(reader3);
     
           //要出报纸啦
           subject.setContent("本期内容是观察者模式");
        }
    }
    测试一下看看,输出结果如下:
    张三收到报纸了,阅读先。内容是===本期内容是观察者模式
    李四收到报纸了,阅读先。内容是===本期内容是观察者模式
    王五收到报纸了,阅读先。内容是===本期内容是观察者模式
           你还可以通过改变注册的观察者,或者是注册了又退订,来看看输出的结果。会发现没有注册或者退订的观察者是收不到报纸的。
           如同前面的示例,读者和报社是一种典型的一对多的关系,一个报社有多个读者,当报社的状态发生改变,也就是出版新报纸的时候,所有注册的读者都会得到通知,然后读者会拿到报纸,读者会去阅读报纸并进行后续的操作。
      

  2.   

    我从 http://chjavach.iteye.com转过来的