JAVA设计模式之观察者模式

前言

本系列文章参考《设计模式之禅》、菜鸟教程网以及网上的一些文章进行归纳总结,并结合自身开发应用。设计模式的命名以《设计模式之禅》为准。

设计模式仅是一些开发者在日常开发中的编码技巧的汇总并非固定不变,可根据项目业务实际情况进行扩展和应用,切不可被这个束缚。更不要为了使用而使用,设计模式是一把双刃剑,过度的设计会导致代码的可读性下降,代码的体积增加。

系列文章不会详细介绍设计模式的《七大原则》,也不会对设计模式进行分类。这样只会增加学习和记忆的成本,也会导致使用时的思想固化,总在想这么设计是否符合xx原则,是否是xx设计模式,xx模式是什么类型等等,不是本系列文章的所希望看到的,目标只有一个,结合日常开发分享自身的应用,以提供一种代码优化的思路。

学习然后忘记,也许是一种最好的方式。

就像俗话说的那样:天下本没有路,走的人多了,就变成了路。在我看来,设计模式也一样,它并非是一种定律,而是前辈们总结下来的经验,我们学习并结合实际加以利用,而不是生搬硬套。

定义

官腔:对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

人话:可能概念不是很明白,但说道Spring或MQ中的发布订阅模型,大家都很熟悉了。在不考虑分布式的情况下,一般一个发布者会对应多个订阅者,发布者的信息会被多个订阅者接受并处理。

扩展:

关于发布/订阅模型观察者模式的区别。

先说一下个人观点:两者本是同源,有小区别,但并非完全不同,发布订阅模型相当与观察者模式的升级版,也没必要刻意区别并去记忆,纯粹浪费时间。

以下纯属个人爆论,请理性看待。

看了一下网上的几篇文章,一句话总结一下,即发布/订阅模型相对于观察者模式,多了一个任务调度中心,且发布者和订阅者直接没有直接关系。

关于发布/定于模型基本都是基于MQ的设计模型来说的,而对比观察者的时候却又是简单的样板代码!一个是设计概念,一个是完全基于书本概念的简单demo代码。后者是看过观察者模式概念,并且完全不去思考变通,认为必须只存在,观察者被观察者两个才算符合其模式。设想一下,在一个商城中,用户支付完成后,你需要推送消息给订单系统更新状态,给库存系统扣减库存,给优惠券系统销毁使用的优惠券,给积分系统扣减积分,给消息系统通知用户购买成功等等。假设,消息系统压力过大,超时处理了,导致整个购买流程失败回滚,是不是得不偿失?所以为了保证稳定性,我们增加了一个消息队列,那么在消息系统和支付系统就不算是观察者和被观察者了吗?

我所做的比对并非是要你接受两种模型之间没有区别,而是想告诉在看这篇文章的各位,不要陷入定式思维。不要去纠结一个没有任何意义的问题。无论是发布订阅模型还是观察者模式,它们本就不存在,而仅仅是一种工具一种编码技巧。有去百度的时间,不如多想想怎么提升自己眼前代码的扩展性和可读性。

作者个人还是那个观点,设计模式并非是刻板的定理,并非1+1=2这样的固定的模式。而是一个你可以拿来任意操控的工具,用于提升代码的兼容性和稳定性。

设计模式要在可读性和扩展性之间进行权衡。不要张嘴就说,你看我这地方用了策略模式,那个地方是个单例,这里嵌套了一个迭代器模式。而是在介绍自己代码的时候说,我这里这么做考虑到了以后的扩展,尽可能的抽离的公共的部分,并且加了适当的注释,这样写的好处是xxxx!

编程是一门艺术,好比写作,作家在创作的过程不会关心这里是否应该用比喻手法,排比手法等等乱七八糟的所谓写作手法,更多的是灵感,只有给学生做阅读理解才会回答这些作者自己都不知道的写作手法。我相信各位也有体会,当有灵感的时候写代码就像停不下来一样,脑子里有千万种方式去实现眼前这个渺小的功能。我希望设计模式对各位来说如同写作手法一样,用时不自知,留给后人去分析。

死记硬背设计模式不如不学,否者如同邯郸学步。

以上纯属个人爆论!不喜轻喷!

应用场景

样例1

老样子,还是来一个简单的demo。

相信大家多少都看过或了解过警匪片,里面一般都会有一个打入黑帮内部的卧底警察,偷偷的给警方传递消息。

img

假设,在灯塔国有一个斧头帮,帮主人狠话不多,我们称其为"大黑"。而灯塔国的警察已经注意这个帮很久了,一直想找机会一窝端,但无奈大黑太狡猾,总能躲过围剿。因此,这天新调来了一个警察局局长“大白”,关于黑帮,大白也很苦恼,经过高层的商谈,大家一致决定安排一个卧底到斧头帮,里应外合之下,一定可以一网打尽。于是大白开始物色人选,为了避开熟面孔,大白决定让一个刚从警校毕业,还未到警局报道的“小白”担此重任,并为“小白”伪造了一个身份。小白需要从底层小混混开始做起,尽可能的靠近管理层。那么如何取得大黑的信任呢?于是大白进行了又一次围剿活动,在活动中,小白拼死为大黑挡了一发子弹。逃过一劫的大黑对小白赞赏有加,并视为左膀右臂,从此小白开始了3年之后又3年的亡命生涯。。。

但故事仍没完,大白不想把赌注都压在一个人身上,因此又派了一个卧底“小新”。但为了在暴露的时候不会供出另一个人,两人之间并不知道彼此的存在。

故事说完了开始整代码:

1.先定义观察者
public interface Observer {

    /**
     * 通知给大白
     */
    void doAction();
}

2.构建2个卧底
//小白
public class XiaoBai implements Observer{
    @Override
    public void doAction() {
        //通知大白
    }
}
//小新
public class XiaoXin implements Observer{
    @Override
    public void doAction() {
        //通知大白
    }
}

如果结构完整的话,我需要再定一个“大白”类,但在观察者模式中,描述的观察者和被观察者,虽然主观上,“大白”才是观察者,但他并没有直接观察,而是委托给了两个卧底。因此,“大黑”和两个卧底之间属于被观察者和观察者的概念。这是基于所谓的定义。

3.构建被观察者
public abstract class Subject {
    List<Observer> observers = new ArrayList<>();

    /**
     * 外出
     */
    public void goOut() {
        this.notifyOb();
    }

    /**
     * 新增观察者
     */
    public void addObserver(Observer observer) {
      observers.add(observer);
    }

    /**
     * 通风报信
     */
    private void notifyOb() {
        //所有的卧底得知动作后偷偷的报信
        observers.forEach(Observer::doAction);
    }
}


public class DaHei extends Subject{

}

这个情景比较简单,因此定义了一个公共的被观察对象,“大黑”没有单独的其他内容。

4.调用
    public static void main(String[] args) {
        DaHei daHei = new DaHei();

        XiaoBai xiaoBai = new XiaoBai();
        daHei.addObserver(xiaoBai);
        XiaoXin xiaoXin = new XiaoXin();
        daHei.addObserver(xiaoXin);
        daHei.goOut();
    }

每次“大黑”外出,小白和小新都会通风报信,可谓最惨老大。。。哈哈哈。

这个demo于网上的可能不太一样,网上的,比较完整一点,观察者和被观察者的顶层都是接口,中间层使用抽象类,用于提取公共的部分,实现层则是业务的定制操作。

样例2

spring自带的异步事件监听器AOP都可以视为观察者模式,应用大家都很熟悉了,这里就不在画蛇添足。

但我们可以仿照异步事件去实现一个简单的可实际应用的事件处理器。

通过事件的参数类型进行区别,可控制一类事件监听一个服务。这里我们就以支付成功后的业务处理为例。

假设,支付成功后,需要更新订单状态,扣减库存,销毁优惠券,发送短信等。

1.定义观察者
public interface LogicObserver {

    <T extends ObserverBaseBean> void  doAction(T t);
}

2.定义观察者参数对象

ObserverBaseBean为观察者参数统一的顶层类,不同的业务定制各自的参数对象,并以此对观察者进行分组。

@Data
public class ObserverBaseBean implements Serializable {
}

这里我们定义了2个参数对象。分别为:

支付成功后:用于统一处理支付成功后的其他业务操作

@EqualsAndHashCode(callSuper = true)
@Data
public class PayObserverBean extends ObserverBaseBean {
}

其他业务:用于测试,区别与支付成功后的操作。

@EqualsAndHashCode(callSuper = true)
@Data
public class OtherObserverBean extends ObserverBaseBean{
}
3.过渡抽象类

在业务观察者和观察者接口之间使用一个抽象类AbstractLogicObserver进行过渡,内部定义一个getBean方法,用于返回当前业务的观察者参数对象。

public abstract class AbstractLogicObserver implements LogicObserver {

    @PostConstruct
    public void init() {
        ObserverManager.addObservers(getBean().getClass(), this.getClass());
    }


    protected abstract ObserverBaseBean getBean();
}

此外,在init方法,将当前的观察者注入到ObserverManager中,我愿称其为调度中心。其中用到了@PostConstruct注解,不熟悉的小伙伴可自行了解一下。简单的来说就是在bean中的属性自动注入后,会执行这个注解标注的方法,这个方法是无参的void方法。

4.调度中心

ObserverManager:即所谓的调度中心就比较简单了,主要对观察者按业务(这里通过不同业务的参数对象)分组保存,使用时遍历即可。

@Slf4j
@SuppressWarnings("unused")
public class ObserverManager {
    private final static Map<Class<? extends ObserverBaseBean>, List<Class<? extends LogicObserver>>> observerMap = new HashMap<>(16);

    /**
     * 新增观察者,并按参数类型进行分组
     */
    public static void addObservers(Class<? extends ObserverBaseBean> bean, Class<? extends LogicObserver> service) {
        List<Class<? extends LogicObserver>> observers = observerMap.get(bean);
        if (observers == null || observers.isEmpty()) {
            observers = new ArrayList<>();
            observers.add(service);
            observerMap.put(bean, observers);
        } else {
            observers.add(service);
        }
    }

    /**
     * 调用观察者
     */
    public static <T extends ObserverBaseBean> void notifyOb(T t) {
        List<Class<? extends LogicObserver>> observers = observerMap.get(t.getClass());
        if (observers == null || observers.isEmpty()) {
            log.warn("未设置观察者");
            return;
        }
        observers.forEach(ob -> {
            //依然静态的获取bean
            LogicObserver observer = SpringContextUtil.getBean(ob);
            observer.doAction(t);
        });
    }
}

5.业务的实际观察对象

至于3个观察者就不多说了,直接上代码。

支付后消息:

@Component
public class PayAfterMsgObserver extends AbstractLogicObserver {


    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        System.out.println("发送消息提醒");
    }

    @Override
    protected ObserverBaseBean getBean() {
        return new PayObserverBean();
    }
}

支付后扣减优惠:

@Component
public class PayAfterCouponObserver extends AbstractLogicObserver{

    @Override
    protected ObserverBaseBean getBean() {
        return new PayObserverBean();
    }

    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        System.out.println("扣减优惠券");
    }
}

其他业务:

@Component
public class OtherObserver extends AbstractLogicObserver{

    @Override
    protected ObserverBaseBean getBean() {
        return new OtherObserverBean();
    }

    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        //其他业务操作
        System.out.println("其他业务操作");
    }
}
6.业务对象

支付类:

@Service
public class PayService {

    public void payOrder() {
        //支付流程

        //支付完成后的处理
        ObserverManager.notifyOb(new PayObserverBean());
    }

    public void other() {
        //其他业务处理
        
        //处理之后
        ObserverManager.notifyOb(new OtherObserverBean());
    }
}
7.测试

需要使用springboot单元测试,或通过springmvc的方式调用,必须在spring的内部使用。不可通过main函数直接调用,否则ObserverManagerobserverMapSpringContextUtil中的ApplicationConext参数都将为null,原因为类加载器Classloader不同,感兴趣的可以自行了解一下,后面有空也会单独介绍classloader。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ObserverDemo {


    @Resource
    private PayService payService;

    @Test
    public void testPay() {
        payService.payOrder();

    }

    @Test
    public void testOther(){
        payService.other();
    }
}

一个简单的事件处理器完成,不过是同步的,若将调用的时候改为多线程,就可以实现异步并发执行。样例仅供参考,还有其他方式实现和优化。

UMl图

老样子,从菜鸟教程搬运。

观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。我们创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。

ObserverPatternDemo,我们的演示类使用 Subject 和实体类对象来演示观察者模式。

Uml

小结

这篇文章拖了很久,其中有一个原因就是在main方法中测试时,结果和我预期的相差甚远,颠覆了我的认知,将一个自以为很熟悉的类加载器概念重新拉回视野。心疼的骂了自己一句。

img

同时在查看网上资料的时候,看到很多人在类比发布订阅模型观察者模式的区别,扣着字眼在那比较,真心觉得没有意义。

希望通过个人的讲解,能让各位有所收货。哪怕有一点灵感,也算是值得了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容