设计模式学习笔记 - 设计模式与范式 -行为型:1.观察者模式(上)

概述

前面已经学习了创建型和结构性设计模式,从本章开始开始学习行为型设计模式。创建型设计模式主要解决 "对象的创建" 问题,结构性设计模式主要解决 "类或对象的组合或组装" 问题,行为型设计模式主要解决 "类或对象之间的交互" 问题。

行为型设计模式比较多,有 11 个,基于占了 23 种设计模式的一半。它们分别是:观察者模式、模版模式、策略模式、职责链模式、状态模式、迭代器模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

本章学习第一个行为型设计模式,也是应用的最广泛的一种设计模式:观察者模式。观察者模式有不同的代码实现方式:同步阻塞的实现方式、异步非阻塞的实现方式;进程内的实现方式,也有跨进程的实现方式。本章重点讲解原理、实现、应用场景。下章会实现一个基于观察者模式的异步非阻塞的 EventBus,加深你对这个模式的理解。


原理及应用场景剖析

观察者模式(Observer Design Pattern)也成为发布订阅模式(Publish-Subscribe Design Pattern)。GoF 的《设计模式》是这样定义的:

Define a one-to-many dependency between objects so that when one object changes state,all it's dependents are notified and updated automatically.

翻译成中文:定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会收到通知。

一般情况下,被依赖的对象叫做被观察者 (Observable),依赖的对象叫做观察者(Observer)。不过在实际的开发中,这两种对象的称呼比较灵活,比如:Subject-Observer、Publisher-Subscribe、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚的定义,都可以看做观察者模式。

实际上,观察者模式是一个比较抽象的模式,根据不同的应用场景,有完全不同的实现方式。现在,先来看其中最经典的一种实现方式。这也是在讲到观察者模式时,很多书籍给出的最常见的实现方式。其代码如下所示:

java 复制代码
public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(Message message);
}

public interface Observer {
    void update(Message message);
}

public class ConcreteSubject implements Subject {
    private static List<Observer> observers = new ArrayList<>();

    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(Message message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

public class ConcreteObserverOne implements Observer {
    @Override
    public void update(Message message) {
        // 获取消息通知,执行自己的逻辑
        System.out.println("ConcreteObserverOne is notified.");
    }
}

public class ConcreteObserverTwo implements Observer {
    @Override
    public void update(Message message) {
        // 获取消息通知,执行自己的逻辑
        System.out.println("ConcreteObserverTwo is notified.");
    }
}

public class Demo {
    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        subject.registerObserver(new ConcreteObserverOne());
        subject.registerObserver(new ConcreteObserverTwo());
        subject.notifyObservers(new Message());
    }
}

实际上,上面的代码算式观察者模式的 "模版代码",只能反映大体的设计思路。在真实的软件开发中,并不需要照搬上面的代码。观察者模式的实现方法各式各样,函数、类的命名等会根据业务场景的不同有很大的差别,比如 register 函数还可以叫做 attachremove 函数还可以叫做 detach 等等。不过,万变不离其宗,设计思路都是差不多的。

原理和代码实现都非常简单,不需要过多解释。我们还是通过一个例子来重点讲下,什么情况下需要用到这种设计模式?

假设,要开发一个 P2P 投资理财系统,用户注册成功之后,我们会给用户发放投资体验金。代码实现大致是下面这样子的:

java 复制代码
public class UserController {
    private UserService userService; // 依赖注入
    private PromotionService promotionService; //依赖注入

    public long register(String telephone, String password) {
        // 省略输入参数的校验代码
        // 省略userService.register()异常的try-catch代码
        long userId = userService.register(telephone, password);
        promotionService.issueNewUserExperienceCash(userId);
        return userId;
    }
}

虽然注册接口做了两件事情,注册和发放体验金,违反单一职责原则,但是如果没有扩展和修改的需求,现在的代码实现是可以接受的。如果非得用观察者模式,就需要引入更多的类和更加复杂的代码结构,反倒是一种过度设计。

如果需求频繁改动,比如用户注册成功之后,不在发放体验券,而是改为发放优惠券,并且还要给用户发送一封 "欢迎注册成功" 的站内信。这种情况下,就需要频繁地修改 register() 函数中的代码,违反开闭原则。而且,如果注册成功之后需要执行的后续操作越来越多,那 register() 函数的逻辑会变得越来越复杂,也就影响到代码的可读性和可维护性。

此时,观察者模式就派上用场了。利用观察者模式,对上面的代码进行重构。

java 复制代码
public interface RegObserver {
    void handleRegSuccess(long userId);
}

public class RegPromotionObserver implements RegObserver {
    private PromotionService promotionService; // 依赖注入
    @Override
    public void handleRegSuccess(long userId) {
        promotionService.issueNewUserExperienceCash(userId);
    }
}

public class RegNotificationObserver implements RegObserver {
    private NotificationService notificationService;
    @Override
    public void handleRegSuccess(long userId) {
        notificationService.senInboxMessage(userId, "Welcome ...");
    }
}

public class UserController {
    private List<RegObserver> regObservers = new ArrayList<>();
    private UserService userService; // 依赖注入

    // 一次性设置好,之后也不可能动态地修改
    public void setRegObservers(List<RegObserver> observers) {
        regObservers.addAll(observers);
    }

    public long register(String telephone, String password) {
        // 省略输入参数的校验代码
        // 省略userService.register()异常的try-catch代码
        long userId = userService.register(telephone, password);

        for (RegObserver observer : regObservers) {
            observer.handleRegSuccess(userId);
        }

        return userId;
    }
}

当我们需要添加观察者时,比如用户注册成功之后,推送用户注册信息给大数据征信系统,基于观察者模式的代码实现, UserController 类的 register() 函数完全不需要修改,只需要再添加一个实现 RegObserver 接口的类,并通过 setRegObservers() 函数将它注册到 UserController 类中即可。

前面已经学习了很多设计模式,不知道你发现没有,实际上,设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦,再具体到观察者模式,它是将观察者和被观察者解耦。 借助设计模式,利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚松耦合等特性,依此来控制代码的复杂性,提高代码的可扩展性。

基于不同应用场景的不同实现方式

观察者模式的应用场景非常广泛,小到代码层面解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、Rss Feeds,本质上都是观察者模式。

不同的应用场景和需求下,这个模式也有截然不同的实现方式,上一小节我们提到,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

第一小节例子中的实现方式,是一种同步阻塞的实现方式。观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。对照上面讲到的用户注册的例子, register() 函数依次调用执行每个观察者的 handleRegSuccess() 函数,最后才返回结果给客户端。

如果注册接口是一个调用比较频繁地接口,对性能非常敏感,希望接口响应时间尽可能短,那我们可以将同步阻塞的实现方式改为异步非阻塞的实现方式,依此来减少响应时间。具体来讲,当 userService.register() 函数执行完成之后,我们启动一个新的线程开执行观察者的 handleRegSuccess() 函数,这样 userController.register() 函数不要等到所有的 handleRegSuccess() 函数都执行完成之后才返回结果给客户端。userController.register() 函数从执行 3 个 SQL 才返回,减少到只需要执行 1 个 SQL 语句就返回,响应时间粗略地讲减少为原来的 1/3

如何实现一个异步非阻塞的观察者模式呢?简单的做法是在每个 handleRegSuccess() 函数中,创建一个新线程执行代码。不过,我们还有更加优雅的实现方式,那就是基于 EventBus 来实现。

刚刚提到的两个场景,不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。如果用户注册成功之后,我们需要发送用户信息给大数据征信系统,而大数据征信系统是一个独立的系统,跟它之间的交互是跨不同进程的,那如何实现一个跨进程的观察者模式呢?

如果大数据征信系统提供了发送用户注册信息的 RPC 接口,仍然可以沿用之前的实现思路,在 handleRegSuccess() 函数中调用 RPC 接口来发送数据。但是,我们还有更加优雅、更加常用的一种实现方式,那就是基于消息队列(比如 ActiveMQ)来实现。

当然,这种实现方式也有弊端,那就是需要引入一个新的系统(消息队列),增加了维护成本。不过,它的好处也非常明显。在原来的实现方式中,观察者需要注册到被观察者需要依次遍历观察者来发送消息。而基于消息队列的实现方式,被观察者和观察者解耦更加彻底,两部分的耦合更小。被观察者完全不感知观察者,同理,观察者也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。

总结

设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构性模式是将不同功能代码解耦,行为性模式是将不同行为解耦,具体到观察者模式,它将观察者和被观察者代码解耦。借助设计模式,我们利用更好的代码结构,将一大坨代码拆分成职责更单一的小类,让其满足开闭原则、高内聚低耦合等特性,依此来控制和应对代码的复杂性,提高代码的可扩展性。

观察者模式的应用场景非常广泛,小到代码层面的解耦,大到框架层面的解耦,再或者一些产品的设计思路,都有观察者模式的影子,这个模式也有截然不同的实现方式,有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

相关推荐
晨米酱10 小时前
JavaScript 中"对象即函数"设计模式
前端·设计模式
数据智能老司机15 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机16 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机16 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机16 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
使一颗心免于哀伤17 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
数据智能老司机1 天前
精通 Python 设计模式——创建型设计模式
python·设计模式·架构
数据智能老司机2 天前
精通 Python 设计模式——SOLID 原则
python·设计模式·架构
烛阴2 天前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
李广坤2 天前
工厂模式
设计模式