乐趣区

关于设计模式:轻松搞懂设计模式观察者模式

【设计模式】观察者模式

前言

观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态产生扭转时,所有依赖于它的对象都失去告诉并被自动更新。这种模式有时又称作公布 - 订阅模式、模型 - 视图模式,它是对象行为型模式。

观察者模式是一种对象行为型模式,其次要长处如下:

  • 升高了指标与观察者之间的耦合关系,两者之间是形象耦合关系。合乎依赖倒置准则。
  • 指标与观察者之间建设了一套触发机制。

它的次要毛病如下:

  • 指标与观察者之间的依赖关系并没有齐全解除,而且有可能呈现循环援用。
  • 当观察者对象很多时,告诉的发布会破费很多工夫,影响程序的效率。

生存小案列

举例说明:领导创立了一个名为摸鱼先锋队的群,用于团建事项的告诉,领导当晚公布了一条音讯 – 这次团建新入职的员工须要筹备表演节目。这个时候群里所有人都能看到这条音讯,只有新员工收到这个告诉后会有一系列表演的筹备工作要做,然而这条音讯对老员工没有任何的影响。

这就是一个观察者模式的生存案例。当领导有事的时候公布告诉到群里,群里的所有人收到告诉后做相应的事件。
以上案例中能够分为上面几个角色:

  • 监听者(也能够说是观察者): 群外面每一个人都是一个监听者。
  • 管理者(对应其余教程中的主题 -subject): 也就是群,次要有增加(群成员)监听者,移除(群成员)监听者,还有告诉所有监听者的性能。
  • 事件(或者说告诉): 也就是领导发到群外面的音讯是一个事件(或者说告诉)。

通过下面的例子来整顿一下实现一个观察者模式的思路。
看看一个流程:领导创立群将相干人员增加到群,而后向群外面公布一个告诉。群外面每个人看到这条音讯后,做相应的事件,当这个音讯与本人无关时,啥也不做。
领导在这外面能够看做是应用程序的一个线程,只是程序执行的一个单元而已。
接下来就是个别设计模式都有的套路,为了程序的扩展性。下面的几个角色都须要定义成形象的概念,那么在 Java 外面定义形象有两种一个是接口一个是抽象类。具体定义成接口还是抽象类依据理论状况自行抉择。

抽象概念

为什么要定义成形象的呢?咱们先理解一下形象的概念,我了解形象就是对一类事物公共局部的定义。比如说水果,就是对一类事物的形象定义,说到水果,大家必定能联想到,多汁且次要味觉为甜味和酸味,可食用的动物果实,有丰盛的营养成分。这个就是水果的公共成分,然而水果又分为多种,火龙果,百香果···。
形象的益处:比方明天你家里只有一种水果 - 火龙果。你爹叫你拿一点水果来吃,那你必定就能间接把家里惟一的水果火龙果拿过去孝顺你老爹。在这个过程中你爹说的水果而不是火龙果,可能少说一个字从而节约能量多活一纳秒。那么咱们能够得出一个论断 - 应用抽象概念能够祛病延年→_→。
开个玩笑,上面言归正传,我说一下我认为形象的益处:

  • 当接口只定义一个实现类时,不便性能的替换(换一个实现类,在新实现类新增性能。从而防止了对调用方和原实现类原代码的改变)。
  • 办法形参定义为形象,这时就能实现传入不同的实现类该办法能够实现不同的性能。
  • 对立治理,让程序更规范化,当形象中定义新的非形象办法,子类能够间接继承应用。

有了下面的铺垫,很容易了解上面的代码示例。

观察者模式代码示例

代码地址:https://gitee.com/kangarookin…
瞎编业务:用户购买商品后,应用观察者模式给相应用户增加积分。用户会员到期,应用观察者模式给相应用户发送短信。
注:这里的业务是瞎编乱造的,结尾会给大家提供几个观察者模式在真正企业外面应用的场景。

观察者模式其实也是公布订阅模式。
针对不同的观察者须要有不同的实现形式,所以先创立一个管理者的接口,将其定义为一个抽象概念,不便后续扩大。
这个接口相当于 - 群(管理者)


/**
 * 观察者的顶层接口
 * @param <T>
 */
public interface ObserverInterface<T> {
    // 注册监听者
    public void registerListener(T t);
    // 移除监听者
    public void removeListener(T t);
    // 告诉监听者
    public void notifyListener(DataEvent t);
}

定义形象的监听者接口
这个接口相当于 - 群成员(监听者)

/**
 * Listener 的顶级接口,为了形象 Listener 而存在
 */
public interface MyListener {void onEvent(DataEvent event);
}

定义形象的事件接口
这个接口相当于群外面公布的告诉

@Data
public abstract class DataEvent {private String msg;}

创立管理者的实现类,相当于具体的群(如微信群,钉钉群)

/**
 * 循环调用形式的观察者(同步)*/
@Component
public class LoopObserverImpl implements ObserverInterface<MyListener> {
    // 监听者的注册列表
    private List<MyListener> listenerList = new ArrayList<>();
    @Override
    public void registerListener(MyListener listener) {listenerList.add(listener);
    }

    @Override
    public void removeListener(MyListener listener) {listenerList.remove(listener);
    }

    @Override
    public void notifyListener(DataEvent event) {for (MyListener myListener : listenerList) {myListener.onEvent(event);
        }
    }
}

创立两个 event 的实现类,一个是积分事件,一个是短信事件

/**
 * 积分事件类
 */
public class ScoreDataEvent extends DataEvent {private Integer score;}

/**
 * 短信事件类
 */
public class SmsDataEvent extends DataEvent {private String phoneNum;}

创立两个 listener 的实现类,一个是解决积分的,一个是解决短信的

/**
 * MyListener 的实现类,分数监听者
 */
@Component
public class MyScoreListener implements MyListener {
    @Override
    public void onEvent(DataEvent dataEvent) {if (dataEvent instanceof ScoreDataEvent) {
            //... 省略业务逻辑
            System.out.println("积分解决:" + dataEvent.getMsg());
        }
    }
}

/**
 * MyListener 的实现类,短信监听者
 */
@Component
public class MySmsListener implements MyListener {
    @Override
    public void onEvent(DataEvent dataEvent) {if (dataEvent instanceof SmsDataEvent) {
            //... 省略短信解决逻辑
            System.out.println("短信解决");
        }
    }
}

观察者模式的因素就到齐了,咱们在 main 办法外面跑一下

public class Operator {public static void main(String[] args) {
        // 通过 spring 的 AnnotationConfigApplicationContext 将 com.example.demo.user.admin.design 门路下的所有加了 spring 注解的类都扫描放入 spring 容器
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.example.demo.user.admin.design");
        // 从 spring 容器中获取对应 bean 的实例
        LoopObserverImpl loopObserver = context.getBean(LoopObserverImpl.class);
        MyScoreListener scoreL = context.getBean(MyScoreListener.class);
        MySmsListener smsL = context.getBean(MySmsListener.class);

        // 向观察者中注册 listener
        loopObserver.registerListener(scoreL);
        loopObserver.registerListener(smsL);
        ScoreDataEvent scoreData = new ScoreDataEvent();
        scoreData.setMsg("循环同步观察者");
        // 公布积分事件,告诉监听者
        loopObserver.notifyListener(scoreData);

        /*******************************************/
        // 从 spring 容器获取 QueueObserverImpl 观察者
        
        QueueObserverImpl queueObserver = context.getBean(QueueObserverImpl.class);
        // 向观察者中注册 listener
        queueObserver.registerListener(scoreL);
        queueObserver.registerListener(smsL);
        ScoreDataEvent scoreData1 = new ScoreDataEvent();
        scoreData1.setMsg("队列异步观察者");
        // 公布积分事件,告诉监听者
        queueObserver.notifyListener(scoreData1);
    }
}

接下来看看上面这个新的观察者实现类和下面示例中的的观察者实现类 LoopObserverImpl 有什么不同吗

/**
 * 启动一个线程循环阻塞队列的观察者,能够实现解耦异步。*/
@Component
public class QueueObserverImpl implements ObserverInterface<MyListener> {
    // 监听者的注册列表
    private List<MyListener> listenerList = new ArrayList<>();
    // 创立一个大小为 10 的阻塞队列
    private BlockingQueue<DataEvent> queue = new LinkedBlockingQueue<>(10);
    // 创立一个线程池
    private ExecutorService executorService = new ScheduledThreadPoolExecutor(1, r -> {Thread t = new Thread(r);
        t.setName("com.kangarooking.observer.worker");
        t.setDaemon(false);
        return t;
    });
//    private ExecutorService executorService = Executors.newFixedThreadPool(1);

    @Override
    public void registerListener(MyListener listener) {listenerList.add(listener);
    }

    @Override
    public void removeListener(MyListener listener) {listenerList.remove(listener);
    }

    @Override
    public void notifyListener(DataEvent event) {System.out.println("向队列放入 DataMsg:" + event.getMsg());
        queue.offer(event);
    }

    @PostConstruct
    public void initObserver() {System.out.println("初始化时启动一个线程");
        executorService.submit(() -> {while (true) {
                try {System.out.println("循环从阻塞队列外面获取数据,take 是阻塞队列没有数据就会阻塞住");
                    DataEvent dataMsg = queue.take();
                    System.out.println("从阻塞队列获取到数据:" + dataMsg.getMsg());
                    eventNotify(dataMsg);
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
        });
    }

    private void eventNotify(DataEvent event) {System.out.println("循环所有的监听者");
        for (MyListener myListener : listenerList) {myListener.onEvent(event);
        }
    }
}

不同之处就是引入了阻塞队列,让告诉这个操作变成异步操作,既只须要将 event 工夫放入阻塞队列之后就能够间接返回了。不必像 LoopObserverImpl 要等到 listener 注册表循环结束能力返回。这样就实现了告诉操作和循环 listener 注册表的解耦和异步。

举例说明异步实现和同步实现的区别:
同步:还是团建群的例子,如果领导是保姆型领导,告诉下来工作之后可能不太释怀,要挨个问,小张你筹备什么表演阿,大略多久能筹备好鸭。小红你呢→_→。。。
异步:如果是甩手掌柜型领导,公布完音讯之后他就不论了。
下面就是同步和异步的区别,同步就是领导是个保姆,挨个问挨个理解状况之后这个事件才算完。异步就是领导公布完音讯就完事儿。

开源框架的实现

同步形式

spring 的公布订阅就是基于同步的观察者模式: 简略来说就是将所有的监听者注册到一个列表外面,而后当公布事件时,通过循环监听者列表,在循环外面调用每个监听者的 onEvent 办法,每个监听者实现的在 onEvent 办法外面判断传入的 event 是否属于以后须要的 event,属于就解决该事件,反之不解决。

spring 的 ApplicationEventMulticaster 就是示例讲的观察者顶层接口

ApplicationListener就是示例代码的监听者顶层接口

refresh 办法外面调用的 registerListeners(); 办法就是将所有的监听者实现类注册到观察者的注册表中

ApplicationEventMulticastermulticastEvent 办法就是下面讲的告诉办法,这里就是循环监听者注册表,调用每个监听者的 onApplicationEvent 办法(这里的 invokeListener 办法外面最终会调用到listener.onApplicationEvent(event);

轻易看一个 onApplicationEvent 办法的实现,跟下面的例子是不是很类似

异步形式

nacos 中有很多中央都应用到了观察者模式,如 client 端和 server 端建设连贯,公布连贯事件,相干监听者做相应的解决,断开连接也是一样。

在 server 端接管到 client 端的注册申请后,会公布一个注册事件的告诉

在 nacos-server 启动的时候也是会开启一个线程做死循环,循环的去 queue 外面 take 数据,如果没有的话就会阻塞。所以死循环只有在 queue 外面始终有数据的时候才会始终循环,当 queue 外面没有数据的时候就会阻塞在 queue.take(); 办法处。

咱们看看 receiveEvent(event); 办法外面做了什么,这里就体现了框架外面设计的精妙:在下面咱们本人的设计中,这里应该是须要循环调用所有的 listener 的 onApplicationEvent 办法,然而当注册表中 listener 太多的时候就会呈现(有些 event 可能会有多个 listener 须要解决)循环调用太慢的问题,这里应用多线程的解决形式,让这些调用并行处理,大大的进步了框架的事件处理效率。

对于业务应用场景

能够说观察者模式能解决的,音讯队列也能够解决,并且能够做的更好。次要依据理论状况取舍。

当公司的服务器资源短缺,并且用户量大,相干业务逻辑调用频繁,音讯要求高可靠性,以及音讯要求公布订阅更灵便,就能够思考应用音讯队列。

当服务器资源不短缺,或者调用比拟少,或者心愿应用轻量的告诉机制,对于音讯可靠性要求不高,能够思考在我的项目代码外面应用观察者模式。
当然应用观察者模式比拟麻烦的一点就是要本人写一定量的代码,而且性能还不如音讯队列的弱小,并且不能保障音讯的可靠性,当观察者获取音讯在本人的解决逻辑外面产生异样时,可能还须要本人先写好产生异样后的降级代码(当然如果对可靠性要求不高的业务场景就不须要)。

为什么框架应用观察者模式而不应用音讯队列(集体了解):

  1. 音讯队列太重;
  2. 自身就是开源框架(本省代表原创),不适宜再引入另一个很重的音讯队列。减少用户的应用和部署老本以及难度,对于本身的推广也不利。

总结

看到这里其实我就想通知大家,设计模式其实只是一种思维形式,咱们学习设计模式只是理解一个根本的编程思维形式,在理论的应用过程中是须要依据理论状况变动的。观察者模式也是如此,只有思维不滑坡,你能够发明出很多种不同实现形式的观察者模式。

我是 kangarooking,以开源的形式保护文章。心愿大家不吝赐教,积极参与 _(:з」∠)_。

退出移动版