【设计模式】观察者模式:一个注册功能也能使用到设计模式?

设计模式 同时被 2 个专栏收录
10 篇文章 0 订阅
48 篇文章 0 订阅

人生在世如身处荆棘之中,心不动,人不妄动,不动则不伤,如心动则人妄动,伤其身,痛其骨,于是体会到世间诸般痛苦


由于受到疫情的影响,现在周末街上都没有以前那么热闹,虽然相比前几个月已经好了很多,但我还是喜欢宅在家里,第一是因为安全,第二是因为穷,一不小心又把心里话说出来了,既然是周末,像我这种屌丝肯定是看着动漫、玩着游戏、惬意的不行,人生得意需尽欢,那为什么我又会来写博客呢?那是因为我的同事小明今天加班,问了我一个既简单又不简单的问题,我觉得工作的你也可能会遇到,所以在这里就记录一下,showtime。

我的斗罗梦

星期六上午起床,一看时间,握草,10点了,斗罗大陆更新了,昊天要出天锤教皇了,赶紧打开电脑,哇,真好看,虽然前面的剧情有点拖,但是最后快结束的时候,昊天锤加上BGM(破茧),燃爆了,可就在这时候,正在加班的小明在微信给我发了夺命十三连,大哥,求救,我遇到问题了,不知道怎么解决,请大哥帮忙,正看得精彩的时候被打断,虽然很不爽,但是视频也结束了,我问了问是什么问题,大致上是这样的:他负责的注册功能现在又需要加新功能了,什么叫又?没错,之前已经加过很多次了,所以小明才会找我,他说注册的方法现在已经几百行了,看着非常累,如果后面还要加功能的话,这个方法就没法看下去了,谁叫我是一个乐于助人的好同事呢?今天就给小明上一课,主题就是:观察者模式。

悲催的小明

为什么是悲催的小明呢?一个注册功能,被产品改了不下5次,不是新增功能,就是删掉之前加的功能,我感觉产品这是在作死的边缘疯狂试探啊,哈哈哈,好了,话不多说,开始今天的主题吧,观察者模式,观察者模式和注册有什么关系呢?别急,我们先来看看小明遇到的问题,产品让小明做的第一版注册:这一版没有什么可说的,就是普通的注册功能,让用户可以登录,小明三下五除二,不一会就搞定了,可是过了一段时间之后产品希望在用户注册成功之后给用户推送一条系统使用教程,小明觉得这个soeasy,然后再注册的方法中加了推送消息的功能,可是过了一段时间,产品又找到了小明,说到:现在是我们app的关键时期,只要是新注册的用户,直接送一个月的限时VIP,小明有点不耐烦了,但是没办法,产品说加,你还能不加吗?小明照着加了。

加完这个功能之后产品有很长一段时间没有来找小明的麻烦了,小明以为注册功能已经完结了,可是让小明没有想到的是,噩梦才刚刚开始,今天产品找到小明。

产品::小明,忙吗?不忙的话和你讨论一个需求。

小明听到这话,脸瞬间就变了,不过还是说到:不忙。

产品:我希望再注册功能上再新增两个功能。

小明:什么功能?

产品:我希望用户注册的时候通过手机号查询他的所在地。

小明崩溃了,还有完没完啊,一个注册有必要加这么多的功能吗?产品很认真的回答:很有必要,后续有可能还会增加或者有可能会取消一个月的限时VIP,听到这句话,小明流下了无助的泪水,我太难了。
在这里插入图片描述
可是没办法,抱怨归抱怨,功能还是得做啊,因为产品加的这些功能,小明已经将之前一个很简单的注册方法改成了一个很复杂的方法,现在他自己看着都不爽,所以他找到了我,问我有没有什么比较优雅一点方法,既可以动态的增加需求,又可以让注册的方法简单明了,今天我就和他讲了如何利用观察者模式解决动态的需求问题。

什么是观察者模式?

观察者模式(有时又被称为模型(Model)-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统(百度百科)。

这种解释一看就很抽象,你们看一遍能理解吗?反正我是理解不了,那什么是观察者模式呢?听说过发布订阅模式吗?它就是观察者模式简单点来说:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。这样好理解多了,你可以结合上面说的注册功能,你想到怎么实现了吗?

我们先一起看看观察者模式的简单使用,后面再结合上面讲的注册功能进行一遍优化。

上代码

/**
 * 观察者接口
 */
public interface MyObserver {

    /**
     * 做某些事
     * @param msg
     */
    void dosomething(Msg msg);
}



public interface MySubject {

    /**
     * 注册观察者
     */
    void reg(MyObserver myObserver);

    /**
     * 删除观察者
     */
    void detele(MyObserver myObserver);

    /**
     * 通知所有的观察者
     */
    void notify(Msg msg);
}




public class MyConcreteSubject implements MySubject {

    private List<MyObserver> myObservers = new ArrayList<MyObserver>();
    @Override
    public void reg(MyObserver myObserver) {
        myObservers.add(myObserver);
    }

    @Override
    public void detele(MyObserver myObserver) {
        myObservers.remove(myObserver);
    }

    @Override
    public void notify(Msg msg) {
        if(myObservers.size() > 0){
            for (MyObserver observer: myObservers) {
                observer.dosomething(msg);
            }
        }
    }
}





public class MyObserverA implements MyObserver {
    @Override
    public void dosomething(Msg msg) {
        System.out.println("我是:MyObserverA,我被执行了;执行参数:"+msg);
    }
}




public class MyObserverB implements MyObserver {
    @Override
    public void dosomething(Msg msg) {
        System.out.println("我是:MyObserverB,我被执行了;执行参数:"+msg);
    }
}




public class MyObserverC implements MyObserver {
    @Override
    public void dosomething(Msg msg) {
        System.out.println("我是:MyObserverC,我被执行了;执行参数:"+msg);
    }
}





public class ObserverTest {

    public static void main(String[] args) {
        MySubject mySubject = new MyConcreteSubject();
        mySubject.reg(new MyObserverA());
        mySubject.reg(new MyObserverB());
        mySubject.reg(new MyObserverC());
        Msg msg = new Msg();
        msg.setId(1);
        msg.setName("小明");
        mySubject.notify(msg);
    }
}



public class Msg {

    private Integer id;

    private String name;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Msg{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

上面就是一个简单的观察者模式模板,是不是很简单,没错,就是这么简单,我们一起来看看打印的结果是什么

我是:MyObserverA,我被执行了;执行参数:Msg{id=1, name='小明'}
我是:MyObserverB,我被执行了;执行参数:Msg{id=1, name='小明'}
我是:MyObserverC,我被执行了;执行参数:Msg{id=1, name='小明'}

Process finished with exit code 0

现在对观察者模式是不是有点理解了呢?那和上面的注册有什么关系呢?大家可以想一想,注册时候添加的消息推送、限时VIP赠送、保存手机号的归属地是不是刚好对应上面的MyObserverA、MyObserverB、MyObserverC,没错,我们可以将这三个功能与注册功能剥离,这样注册功能就会变得非常的简单,通过发布订阅的这种方式来告诉订阅的观察者们,你们需要执行了。

注册功能改造

看到上面的这个案例之后想必你应该知道怎么去改造注册的代码了吧,那我们就一起来看看注册这个功能。

原始写法

public class UserReg {

    /**
     * 注册,保存用户信息
     * @param user
     */
    private void saveUser(User user){
        System.out.println("将用户信息保存到数据库中,注册成功!");
    }


    /**
     * 赠送限时VIp
     * @param user
     */
    private void giveVip(User user){
        System.out.println("赠送限时VIp成功!");
    }

    /**
     * 发送使用指南的消息
     * @param user
     */
    private void sendMsg(User user){
        System.out.println("给用户推送一条系统消息,消息的内容为用户指南!");
    }

    /**
     * 查询用户手机号码的归属地,保存到数据库
     * @param user
     */
    private void savePhoneRegion(User user){
        System.out.println("查询用户手机号码的归属地,保存到数据库,方便发数据部门的同事做数据分析!");
    }

    public void reg(User user){
        //注册
        saveUser(user);
        //赠送限时VIp
        giveVip(user);
        //发送使用指南的消息
        sendMsg(user);
        //查询用户手机号码的归属地,保存到数据库
        savePhoneRegion(user);
    }
}





public class User {

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String pwd;

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        return "User{" +
                "phone='" + phone + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
}



public class UserTest {

    public static void main(String[] args) {
        UserReg userReg = new UserReg();
        User user = new User();
        user.setPhone("18888888888");
        user.setPwd("123456");
        userReg.reg(user);
    }
}

运行main函数

将用户信息保存到数据库中,注册成功!
给用户推送一条系统消息,消息的内容为用户指南!
查询用户手机号码的归属地,保存到数据库,方便发数据部门的同事做数据分析!

Process finished with exit code 0

是不是实现了小明做的注册功能?是的,一点毛病都没有,但我不推荐这么写,你们知道为什么吗?现在只有三个,那后面还有新的功能呢?我们还继续在reg()方法中添加吗?这种很明显违反了设计原则中的单一原则,一个注册方法做了太多与注册无关的事情,所以我们需要将它们剥离,设计原则往往比设计模式更加的重要,所以我们一起使用观察模式改造一下这个注册功能。

public interface RegSubject {

    /**
     * 注册观察者
     */
    void reg(MyRegObserver myRegObserver);

    /**
     * 删除观察者
     */
    void detele(MyRegObserver myRegObserver);

    /**
     * 通知所有的观察者
     */
    void notify(User user);
}



public class MyRegSubject implements RegSubject {

    private List<MyRegObserver> myObservers = new ArrayList<MyRegObserver>();
    @Override
    public void reg(MyRegObserver myRegObserver) {
        myObservers.add(myRegObserver);
    }

    @Override
    public void detele(MyRegObserver myRegObserver) {
        myObservers.remove(myRegObserver);
    }

    @Override
    public void notify(User user) {
        if(myObservers.size() > 0){
            for (MyRegObserver observer: myObservers) {
                observer.regHandler(user);
            }
        }
    }
}


/**
 * 观察者接口
 */
public interface MyRegObserver {

    /**
     * 做某些事
     * @param user
     */
    void regHandler(User user);
}


public class MsgNotificationObserver implements MyRegObserver {
    @Override
    public void regHandler(User user) {
        System.out.println("给用户推送一条系统消息,消息的内容为用户指南!;执行参数:"+user);
    }
}


/**
 * 限时vip
 */
public class LimitVipObserver implements MyRegObserver {
    @Override
    public void regHandler(User user) {
        System.out.println("赠送限时VIp成功!执行参数:"+user);
    }
}


/**
 * 保存用户手机号归属地
 */
public class PhoneRegionObserver implements MyRegObserver {
    @Override
    public void regHandler(User user) {
        System.out.println("查询用户手机号码的归属地,保存到数据库,方便发数据部门的同事做数据分析!执行参数:"+user);
    }
}


public class User {

    /**
     * 手机号
     */
    private String phone;

    /**
     * 密码
     */
    private String pwd;

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        return "User{" +
                "phone='" + phone + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
}

public class UserReg {

    /**
     * 注册,保存用户信息
     * @param user
     */
    private void saveUser(User user){
        System.out.println("将用户信息保存到数据库中,注册成功!");
    }

    public void reg(User user){
        //注册
        saveUser(user);
        RegSubject regSubject = new MyRegSubject();
        regSubject.reg(new MsgNotificationObserver());
        regSubject.reg(new LimitVipObserver());
        regSubject.reg(new PhoneRegionObserver());
        regSubject.notify(user);
    }
}


public class UserTest {

    public static void main(String[] args) {
        UserReg userReg = new UserReg();
        User user = new User();
        user.setPhone("18888888888");
        user.setPwd("123456");
        userReg.reg(user);
    }
}



执行main()函数

将用户信息保存到数据库中,注册成功!
给用户推送一条系统消息,消息的内容为用户指南!;执行参数:User{phone='18888888888', pwd='123456'}
赠送限时VIp成功!执行参数:User{phone='18888888888', pwd='123456'}
查询用户手机号码的归属地,保存到数据库,方便发数据部门的同事做数据分析!执行参数:User{phone='18888888888', pwd='123456'}

Process finished with exit code 0

虽然已经将与注册无关的功能抽离了出来,但是还有一个问题没有解决,那就是需要在注册方法中new 观察者类,这点肯定会让很多开发者非常不爽,如果使用观察者模式需要这样new的话,还不如之前的写法呢,没错,在reg()方法中new观察者确实不是一个好办法,这个肯定是可以解决的,那你知道如何解决这个问题吗?

我们只需要将MyRegSubject中的方法移到UserReg类中,改造一下如下:

public class UserReg {

    private List<MyRegObserver> myObservers = new ArrayList<MyRegObserver>();

    /**
     * 一次性将所有的观察者都添加进来
     * @param myObservers
     */
    public void regAll(List<MyRegObserver> myObservers) {
        myObservers.addAll(myObservers);
    }

    /**
     * 注册,保存用户信息
     * @param user
     */
    private void saveUser(User user){
        System.out.println("将用户信息保存到数据库中,注册成功!");
    }

    public void reg(User user){
        //注册
        saveUser(user);
        for(MyRegObserver myRegObserver : myObservers){
            myRegObserver.regHandler(user);
        }
    }
}

这样就是将观察者的管理交给了UserReg,那你可能就会有疑惑了,这样还不是需要在外部new?没错,虽然需要new,但是我可以在程序启动的时候通过注入的方式将这些观察者都注入到UserReg中,我们直接去ioc容器拿这个bean就可以了,后续的改动也仅仅只是需要修改对应的观察者,并不需要对注册的方法做修改,虽然还是违背开闭原则,但是这种写法可以大大的降低程序的bug,还是比较推荐的。
在这里插入图片描述

我和小明讲到这里的时候,他好像明白了,点了点头,随后又说了一句:大哥,能将这些观察者设置成异步的吗?我发现用户的注册并不需要这三个观察者的结果,如果能做成异步,那就会大大的提高注册的效率。握草,过分了啊,不过看着小明对知识的渴望,我就成全他吧,在给他上一课,什么叫:异步非阻塞观察者模式.。

异步非阻塞观察者模式

是的,你没有听错,就是异步非阻塞观察者模式,什么意思呢?顾名思义,就是让观察者的执行异步,主线程的执行不受它们的执行影响。

如果只是一个很简单的观察者模式,直接在观察者方法中创建一个新的线程,这样它们的执行就是异步了,实现如下:

/**
 * 限时vip
 */
public class LimitVipObserver implements MyRegObserver {
    @Override
    public void regHandler(User user) {
        new Thread(() -> 
                System.out.println("赠送限时VIp成功!执行参数:"+user)
        ).start();

    }
}

其他两个观察者我就不写了,一样的写法,这样就实现了异步非阻塞观察者模式,你可能会说,这也太简单了吧,但是这里会有一个问题,大家知道是什么吗?没错,那就是线程被频繁的创建和销毁,线程的创建和销毁是需要时间的,所以这种方式使用的时候得慎重,那我们能对它进行优化吗?当然可以,只需要将线程交给线程池管理即可,这里我就不给出使用代码了,比较简单,我要介绍的一部非阻塞观察者模式的实现方式并不是这两种,所以这两个你们了解一下就行。

EventBus

EventBus是Google Guava非常著名的一个“事件总线”框架,它分别支持同步阻塞和异步非阻塞模式。

它可以让我们省略掉MyRegObserver接口,直接交给EventBus管理,话不多说,我们一起使用EventBus将同步的注册改造成异步的注册。

public class UserReg {

    private EventBus eventBus;

    public UserReg() {
         eventBus = new AsyncEventBus(Executors.newFixedThreadPool(10)); // 异步非阻塞模式
    }

    /**
     * 一次性将所有的观察者都添加进来
     * @param myObservers
     */
    public void regAll(List<Object> myObservers) {
        for(Object myObserver : myObservers){
            eventBus.register(myObserver);
        }
    }

    /**
     * 注册,保存用户信息
     * @param user
     */
    private void saveUser(User user){
        System.out.println("将用户信息保存到数据库中,注册成功!");
    }

    public void reg(User user){
        //注册
        saveUser(user);
        eventBus.post(user);
    }
}


public class MsgNotificationObserver  {

    @Subscribe
    public void regHandler(User user) {
        System.out.println("给用户推送一条系统消息,消息的内容为用户指南!;执行参数:"+user);
    }
}

public class LimitVipObserver  {

    @Subscribe
    public void regHandler(User user) {
        System.out.println("赠送限时VIp成功!执行参数:"+user);
    }
}


public class PhoneRegionObserver {

    @Subscribe
    public void regHandler(User user) {
        System.out.println("查询用户手机号码的归属地,保存到数据库,方便发数据部门的同事做数据分析!执行参数:"+user);
    }
}

我们创建了一个异步非阻塞的EventBus(new AsyncEventBus(Executors.newFixedThreadPool(10)))
eventBus.register(myObserver):将我们的观察者注册到EventBus中。
eventBus.post(user):通过eventBus将消息发送出去。
@Subscribe:被他修饰的方法可以接收到eventBus.post()发送出去的消息。

我们发现只要是被@Subscribe修饰的方,都是接收到eventBus发送出去的事件,那会不会出现消息被不属于注册事件的观察者消费呢?那就要看你是怎么定义的参数了,eventBus通过参数来决定它的消费者,这里我来重点讲一下参数的注意事项。
我们有三个bean:UserA、UserB、UserC;其中UserA是UserB的父类,这个时候用分别用它们做参数,会发生什么事情呢?


UserA userA = new UserA ();
post(userA);
 
UserB userB = new UserB();
post(userB); 

UserC userC = new UserC();
post(userC );

post(userA): 只有参数为UserA的观察类会接收到。
post(userB): 只要是定义的观察类的接收参数为:UserA、UserB;都会接收到
post(userC): 只有参数为UserC的观察类会接收到。

所以在使用的时候一定需要注意这一点哦,否者就会出现一个post(),多个不同的观察类消费的情况。

之前不是EventBus 还支持同步吗?他是怎么实现的呢?很简单,请看代码:

public UserReg() {
         //eventBus = new AsyncEventBus(Executors.newFixedThreadPool(10)); // 异步非阻塞模式
        eventBus = new EventBus();//同步阻塞模式
    }

应用场景

好了,观察者模式的实现方式讲的差不多了,其实在我们的开发中也会经常遇到观察者模式,比如邮件的订阅,但是需要注意的是不同的需求场景,有着不同的实现方式,比如同步阻塞模式/异步非阻塞模式等等,它还可以跨进程实现哦。

所以,观察者模式运用的非常广泛,也是23种设计模式中非常常用的一种设计模式。

总结

虽然观察者模式可以让代码解耦,但是也不要过度的使用,比如上面的注册功能,如果它注册成功之后只有用户指南消息的推送,后续也没有新增功能的可能,这个时候如果强行使用观察者模式就有点杀猪用牛刀了,比起设计模式来说,设计原则更为重要,就算是在使用设计模式的时候也要尽可能的满足代码的设计原则,比如类的单一原则、开闭原则、里氏替换原则、接口隔离原则等等,如果设计模式脱离了设计原则,那这个模式毫无意义。

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

流星007

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值