设计模式之美笔记12

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

之前学习创建型模式,主要解决“对象的创建”问题,和结构型模式,解决“类或对象的组合或组装”问题,接下来学习行为型模式,解决“类或对象的交互”问题。

观察者模式

原理及应用场景剖析

观察者模式Observer design pattern,也叫发布订阅模式publish-subscribe design pattern,设计模式一书定义:define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

一般说,被依赖的对象叫被观察者observable,依赖的对象叫观察者observer,不过,实际开发中,有各种叫法,如subject-observer、publisher-subscriber、producer-consumer、eventEmitter-eventListener、dispatcher-listener。

实际上,观察者模式是个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式。最经典的一种实现方式。

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 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) {
        //todo 获取消息通知,执行自己的逻辑
        System.out.println("ConcreteObserverOne is notified");
    }
}

public class ConcreteObserverTwo implements Observer {
    @Override
    public void update(Message message) {
        //todo 获取消息通知,执行自己的逻辑
        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函数可叫attach,remove函数可叫detach等。不过,设计思路差不多。

通过一个例子了解下什么情况下需要用到这种设计模式?或者说,这种设计模式能解决什么问题?

假设在开发一个p2p投资理财系统,用户注册成功后,会给用户发放投资体验金。代码实现大致如下:

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()方法的逻辑越来越复杂,影响到代码的可读性和可维护性。

这种情况下,观察者模式派上用场。重构后:

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.sendInboxMessage(userId,"Welcome...");
    }
}
public class UserController {
    private UserService userService;
    private List<RegObserver> regObservers = new ArrayList<>();
    //一次性设置好,之后也不可能动态的修改
    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类中即可。

可能说,当把发送体验金替换为发送优惠券,需要修改RegPromotionObserver类中的handleRegSuccess()方法的代码,违反了开闭原则。不过,相对于register()方法,handleRegSuccess()方法的逻辑简单的多,修改更不容易出错,引入bug的风险更低。

总结:设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码结构,行为型模式是将不同行为代码解耦,具体到观察者模式,是将观察者和被观察者代码解耦。

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

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

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

之前讲到的实现方式,从刚刚的分类方式看,是同步阻塞的实现方式。观察者和被观察者代码在一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成后,才执行后续代码。对照上面的用户注册的例子,register()方法依次调用执行每个观察者的handleRegSuccess()方法,等到都执行完成后,才会返回结果给客户端。

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

那如何实现一个异步非阻塞的观察者模式呢?创建一个新的线程执行代码。更优雅的实现方式是基于EventBus实现。

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

如果大数据征信系统提供了发送用户注册信息的RPC接口,仍可以沿用之前的实现思路,在handleRegSuccess()方法中调用rpc接口发送数据。但还有更优雅、更常用的实现方式,就是基于消息队列(message queue,如activeMQ)所实现。

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

和生产-消费者模型的区别是什么?发布-订阅是一对多,生产-消费也可能是一对多啊,细究的话,一条消息被生产出来,在生产者-消费者模型中只能被一个消费者消费,而在发布-订阅中可被多个订阅者消费。

异步非阻塞观察者模式的简单实现

有两种实现方式,一种是在每个handleRegSuccess()方法中创建一个新的线程执行代码逻辑;另一种是在UserController的register()中使用线程池来执行每个观察者的handleRegSuccess()方法。具体代码

public class RegPromotionObserver implements RegObserver {
    // 第一种实现方式,其他类代码不变,就没有再重复罗列
    private PromotionService promotionService;//依赖注入
    
    @Override
    public void handleRegSuccess(final long userId) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                promotionService.issueNewUserExperienceCash(userId);
            }
        });
        thread.start();
    }
}

//第二种实现方式,其他类代码不变,没有再重复罗列
public class UserController {
    private UserService userService;//依赖注入
    private List<RegObserver> regObservers = new ArrayList<>();
    private Executor executor;
    public UserController(Executor executor){
        this.executor = executor;
    }
    public void setRegObservers(List<RegObserver> observers){
        regObservers.addAll(observers);
    }
    public Long register(String telephone,String password){
        //省略输入的校验代码
        //省略userService.register()异常的try-catch代码
        final long userId = userService.register(telephone,password);
        for (final RegObserver observer: regObservers){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    observer.handleRegSuccess(userId);
                }
            });
        }
        return userId;
    }
}

对第一种实现方式,频繁的创建和销毁线程比较耗时,且并发线程数无法控制,创建过多的线程会导致堆栈溢出。第二种,尽管利用了线程池解决了第一种实现方式的问题,但线程池、异步执行逻辑都耦合在register()方法中,增加这部分业务代码的维护成本。

如果更极端点,需要再同步阻塞和异步非阻塞之间灵活切换,需要不停修改UserController的代码。此外,如果项目中,不止一个业务模块需要用到异步非阻塞观察者模式,这样的代码无法复用。

框架的作用:隐藏实现细节,降低开发难度,做到代码复用,解耦业务和非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,可抽象为框架来达到该效果。这个框架就是EventBus。

EventBus框架功能需求

EventBus,事件总线,提供实现观察者模式的骨架代码,可以基于此框架,非常容易的在自己的业务场景中实现观察者模式,其中Google guava eventbus就是比较著名的EventBus框架,不仅支持异步非阻塞模式,也支持同步阻塞模式。

举例说明:

public class UserController {
    private UserService userService;
    private EventBus eventBus;
    private static final int DEFAULT_EVENTBUS_THREAD_POOL_SIZE = 20;
    public UserController(){
        eventBus = new AsyncEventBus(Executors.newFixedThreadPool(DEFAULT_EVENTBUS_THREAD_POOL_SIZE));
    }
    public void setRegObservers(List<Object> observers){
        for (Object observer:observers){
            eventBus.register(observer);
        }
    }
    public Long register(String telephone, String password){
        //省略输入参数的校验代码
        //省略userService.register()异常的try-catch代码
        long userId = userService.register(telephone,password);
        eventBus.post(userId);
        return userId;
    }
}

public class RegPromotionObserver {
    private PromotionService promotionService;
    
    @Subscribe
    public void handleRegSuccess(long userId){
        promotionService.issueNewUserExperienceCash(userId);
    }
}

public class RegNotificationObserver {
    private NotificationService notificationService;
    
    @Subscribe
    public void handleRegSuccess(long userId){
        notificationService.sendInboxMessage(userId,"...");
    }
}

利用EventBus框架实现的观察者模式,和从零编写的观察者模式相比,实现思路大致一样,都要定义Observer,并通过register()注册Observer,也都要通过调用某个方法(如EventBus的post()方法)给Observer发送消息(在EventBus中消息被称作事件event)。

但实现细节有些区别,基于EventBus,不需要定义Observer接口,任意类型的对象都可以注册到EventBus中,通过@Subscribe 注解标明类中哪个方法可接收被观察者发送的消息。

guava EventBus的类和方法

  • EventBus AsyncEventBus

对外暴露的所有可调用接口,都封装在EventBus类中,其中,EventBus实现同步阻塞的观察者模式,而AsyncEventBus继承自EventBus,提供异步非阻塞的观察者模式,用法:

EventBus eventBus = new EventBus();//同步阻塞模式
EventBus eventBus = new AysncEventBus(Executors.newFixedThreadPool(8));//异步非阻塞
  • register()方法

EventBus提供register()方法用来注册观察者。具体定义如下,可接受任何类型(Object)的观察者;经典的观察者模式的实现中,register()方法必须接受实现了同一Observer接口的类对象。

public void register(Object object);
  • unregister()方法

该方法用于从EventBus中删除某个观察者。

public void unregister(Object object);
  • post()方法

EventBus类提供post方法,用来给观察者发送消息,具体定义

public void post(Object object);

和经典的观察者模式不同之处是,当调用post()方法发送消息时,并非把消息发送给所有的观察者,而是发送给可匹配的观察者。也就是能接收的消息类型是发送消息(post方法定义的event)类型的父类。举例说明:

如AObserver能接收的消息类型是XMsg,BObserver能接收的消息类型是YMsg,CObserver能接收的消息类型是ZMsg。其中,XMsg是YMsg的父类,当发送如下消息时,相应能接收到消息的可匹配观察者如下:

XMsg xMsg = new XMsg();
YMsg yMsg = new YMsg();
ZMsg zMsg = new ZMsg();
post(xMsg);==>AObserver接收到消息
post(yMsg);==>AObserver、BObserver接收到消息
post(zMsg);==>CObserver接收到消息

可能会问,每个Observer能接收到的消息类型是哪里定义的?看下Guava EventBus最特别的地方,就是@Subscribe 注解。

  • @Subscribe注解

通过@Subscribe 注解标明某个方法能接收哪种类型的消息,具体的使用代码如下。在DObserver类,通过@Subscribe 注解两个方法f1() f2()

public class DObserver {
    //...省略其他属性和方法...
    @Subscribe
    public void f1(PMsg event){//...}

    @Subscribe
    public void f1(QMsg event){//...}    
}

当通过register()方法将DObserver类对象注册到EventBus时,EventBus会根据@Subscribe 注解找到f1()和f2(),并将两个方法能接收的消息类型记录下来(PMsg->f1, QMsg->f2)。通过post()方法发送消息(如QMsg消息)的时候,EventBus会通过之前的记录(QMsg->f2),调用相应的方法f2.

自己实现EventBus框架

重点是两个核心方法register()和post()的实现原理。
在这里插入图片描述
在这里插入图片描述

从图中可看出,最关键的一个数据结构是Observer注册表,记录消息类型和可接收消息函数的对应关系,当调用register()方法注册观察者时,EventBus通过解析@Subscribe 注解,生成Observer注册表。当调用post()方法发送消息时,EventBus通过注册表找到相应的可接收消息的方法,然后通过java的反射语法来动态的创建对象、执行方法。对于同步阻塞模式,EventBus在一个线程内依次执行相应的方法。对于异步非阻塞模式,EventBus通过一个线程执行相应的方法。

整个小框架的代码实现包括5个类:EventBus、AsyncEventBus、Subscribe、ObserverAction、ObserverRegistry。依次看这5个类。

1. Subscribe

是个注解,用于标明观察者的哪个函数可接收消息。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Beta
public @interface Subscribe {
    
}
2. ObserverAction

用于表示@Subscribe 注解的方法,其中,target表示观察者类,method表示方法,主要用在ObserverRegistry观察者注册表中。

public class ObserverAction {
    private Object target;
    private Method method;

    public ObserverAction(Object target, Method method) {
        this.target = target;
        this.method = method;
        this.method.setAccessible(true);
    }
    public void execute(Object event){
        // event是method方法的参数
        try{
            method.invoke(target,event);
        }catch (InvocationTargetException | IllegalAccessException e){
            e.printStackTrace();
        }
    }
}
3. ObserverRegistry

就是Observer注册表,是最复杂的一个类,框架几乎所有的核心逻辑都在这个类中。这个类大量使用java的反射语法,不过代码整体不难理解,其中一个比较有技巧的是CopyOnWriteArraySet的使用。

CopyOnWriteArraySet,在写入数据的时候,会创建一个新的set,并将原始数据clone到新的set中,在新的set中写入数据完成后,再用新的set替换老的set。保证了在写入数据时,不影响数据的读取操作,,以此解决读写并发问题。此外,通过加锁的方式,避免并发写冲突。

public class ObserverRegistry {
    private ConcurrentHashMap<Class<?>, CopyOnWriteArraySet<ObserverAction>> registry;
    public void register(Object observer){
        Map<Class<?>, Collection<ObserverAction>> observerActions = findAllObserverActions(observer);
        for (Map.Entry<Class<?>,Collection<ObserverAction>> entry:observerActions.entrySet()){
            Class<?> eventType = entry.getKey();
            Collection<ObserverAction> eventActions = entry.getValue();
            CopyOnWriteArraySet<ObserverAction> registeredEventActions = registry.get(eventType);
            if (registeredEventActions == null){
                registry.putIfAbsent(eventType,new CopyOnWriteArraySet<>());
                registeredEventActions = registry.get(eventType);
            }
            registeredEventActions.addAll(eventActions);
        }
        
    }
    
    public List<ObserverAction> getMatchedObserverActions(Object event){
        List<ObserverAction> matchedObservers = new ArrayList<>();
        Class<?> postedEventType = event.getClass();
        for (Map.Entry<Class<?>,CopyOnWriteArraySet<ObserverAction>> entry:registry.entrySet()){
            Class<?> eventType = entry.getKey();
            Collection<ObserverAction> eventActions = entry.getValue();
            if (postedEventType.isAssignableFrom(eventType)){
                matchedObservers.addAll(eventActions);
            }
        }
        return matchedObservers;
    }
    
    private Map<Class<?>,Collection<ObserverAction>> findAllObserverActions(Object observer){
        Map<Class<?>,Collection<ObserverAction>> observerActions = new HashMap<>();
        Class<?> clazz = observer.getClass();
        for (Method method: getAnnotatedMethod(clazz)){
            Class<?>[] parameterTypes = method.getParameterTypes();
            Class<?> eventType = parameterTypes[0];
            if (!observerActions.containsKey(eventType)){
                observerActions.put(eventType,new ArrayList<>());
            }
            observerActions.get(eventType).add(new ObserverAction(observer,method));
        }
        return observerActions;
    }
    private List<Method> getAnnotatedMethod(Class<?> clazz){
        List<Method> annotatedMethods = new ArrayList<>();
        for (Method method: clazz.getDeclaredMethods()){
            if (method.isAnnotationPresent(Subscribe.class)){
                Class<?>[] parameterTypes = method.getParameterTypes();
                Preconditions.checkArgument(parameterTypes.length == 1,
                        "Method %s has @Subscribe annotation but has %s parameters."
                +"Subscriber methods must have exactly 1 parameter.",method,parameterTypes.length);
                annotatedMethods.add(method);
            }
        }
        return annotatedMethods;
    }
}
4. EventBus

EventBus实现的是阻塞同步的观察者模式,里面MoreExecutors.directExecutor() 是Google Guava提供的工具类,看似多线程,实则单线程。之所以这样实现,是为了和AsyncEventBus统一代码逻辑,做到代码复用。

public class EventBus {
    private Executor executor;
    private ObserverRegistry registry = new ObserverRegistry();
    public EventBus(){
        this(MoreExecutors.directExecutor());
    }
    protected EventBus(Executor executor){
        this.executor = executor;
    }
    public void register(Object object){
        registry.register(object);
    }
    public void post(Object event){
        List<ObserverAction> observerActions = registry.getMatchedObserverActions(event);
        for (ObserverAction observerAction:observerActions){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    observerAction.execute(event);
                }
            });
        }
    }
}
5. AsyncEventBus

有了EventBus,AsyncEventBus实现很简单,为实现异步非阻塞的观察者模式,不能再继续用MoreExecutors.directExecutor() 而是需要在构造函数中,由调用者注入线程池。

public class AsyncEventBus extends EventBus {
    public AsyncEventBus(Executor executor){
        super(executor);
    }
}

至此,就实现了可用的EventBus,不过在细节上,Google Guava EventBus做了很多优化,如优化在注册表查找消息可匹配方法的算法。

模板模式

模板模式主要用来解决复用和扩展两个问题。

模板模式的原理和实现

模板模式,全称模板方法设计模式,Template Method Design Pattern。定义:Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure. 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类实现。模板方法模式可让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

这里的“算法”,可理解为广义的“业务逻辑”,并不特指某个算法。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

代码更简单,如下,templateMethod()方法定义为final,是为了避免子类重写它。method1()和method2()定义为abstract,是为了强迫子类去实现。实际项目开发中,实现较为灵活。

public abstract class AbstractClass {
    public final void templateMethod(){
        //...
        method1();
        //...
        method2();
        //...
    }
    protected abstract void method1();
    protected abstract void method2();
}

public class ConcreteClass1 extends AbstractClass {
    @Override
    protected void method1() {
        //...
    }

    @Override
    protected void method2() {
        //...
    }
}

public class ConcreteClass2 extends AbstractClass {
    @Override
    protected void method1() {
        //...
    }

    @Override
    protected void method2() {
        //...
    }
}

AbstractClass demo = new ConcreteClass1();
demo.templateMethod();

模板模式作用1:复用

把一个算法中不变的流程抽象到父类的模板方法templateMethod()中,将可变的部分method1()和method2()

留给子类实现。所有的子类都可复用父类模板方法定义的流程代码。通过两个例子看下。

1. java InputStream

java IO类库中,有很多类的设计都用到模板模式,如InputStream、OutputStream、Reader、Writer。以InputStream举例。InputStream的read()方法是个模板方法,定义了读取数据的整个流程,并暴露一个可由子类定制的抽象方法。只是这个方法也被命名为read(),只是参数和模板方法不同。

public abstract class InputStream implements Closeable {
    //...省略其他代码...
    public int read(byte b[], int off, int len) throws IOException{
        if (b == null){
            throw new NullPointerException();
        }else if (off < 0 || len < 0 || len > b.length - off){
            throw new IndexOutOfBoundsException();
        }else if (len ==0){
            return 0;
        }
        
        int c = read();
        if (c == -1){
            return -1;
        }
        b[off] = (byte)c;
        
        int i = 1;
        try{
            for (; i < len; i++){
                c = read();
                if (c == -1){
                    break;
                }
                b[off + i] = (byte)c;
            }
        }catch (IOException e){
            
        }
        return i;
    }
    
    public abstract int read() throws IOException;
    @Override
    public void close() throws IOException {
        
    }
}

public class ByteArrayInputStream extends InputStream {
    //...省略其他代码...
    private int pos;
    private int count;
    private byte[] buf;
    @Override
    public synchronized int read() throws IOException {
        return (pos < count)?(buf[pos++] & 0xff):-1;
    }
}
2. java AbstractList

在AbstractList类中,addAll()方法可看做模板方法,add()是子类需要重写的方法,尽管没有声明为abstract,但函数实现直接抛出UnsupportedOperationException异常。前提是如果子类不重写是不能使用的。

public abstract class AbstractList<E> {
    public boolean addAll(int index, Collection<? extends E> c){
        rangeCheckForAdd(index);
        boolean modified = false;
        for (E e:c){
            add(index++,e);
            modified = true;
        }
        return modified;
    }
    public void add(int index, E element){
        throw new UnsupportedOperationException();
    }
  //...
}

模板模式作用2:扩展

模板模式第二个作用是扩展,这里说的扩展,不是代码的扩展性,而是框架的扩展性,有点类似控制反转。基于该作用,模板模式常用在框架的开发中,让框架用户可在不修改框架源码的情况下,定制化框架的功能。通过junit TestCase、java Servlet两个例子来解释。

1. java servlet

对java web项目开发说,常用的开发框架是springMVC。不过,如果抛开这些高级框架来开发web项目,必然会用到servlet。实际上,使用比较底层的servlet开发web项目并不难,只需定义一个继承HttpServlet的类,并重写doGet()或doPost()方法分别处理get和post请求。

public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("hello world");
    }
}

此外,还需要在配置文件web.xml中写配置,tomcat、jetty等servlet容器启动时,会自动加载这个配置文件中的url和servlet之间的映射关系。

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>com.ai.doc.template.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
</servlet-mapping>

在浏览器输入网址(http://127.0.0.1:8080/hello)后,servlet容器会接收到相应的请求,根据url和servlet的映射关系,找到相应的servlet(HelloServelt),然后执行它的service()方法,service()方法定义在父类HttpServlet中,会调用doGet()或doPost()方法,然后输出数据(“Hello world”)到网页。

再看HttpServlet的service()方法

public class HttpServlet {
    public void service(ServletRequest req, ServerHttpResponse res) throws Exception{
        HttpServletRequest request;
        HttpServletResponse response;
        if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)){
            throw new ServletException("non-HTTP request or response");
        }
        request = (HttpServletRequest)req;
        response = (HttpServletResponse)res;
        service(request,response);
    }
    protected void service(HttpServletRequest req,HttpServletResponse resp)throws Exception{
        String method = req.getMethod();
        if (method.equals(METHOD_GET)){
            long lastModified = getLastModified(req);
            if (lastModified == -1){
                //servlet doesn't support if-modified-since, no reason
                // to go through further expensive login 
                doGet(req,resp);
            }else{
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified){
                    // if the servlet mod time is later,call doGet()
                    //round down to the nearest second for a proper compare
                    // a ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp,lastModified);
                    doGet(req,resp);
                }else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
        }else if (method.equals(METHOD_HEAD)){
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp,lastModified);
            doHead(req,resp);
        }else if (method.equals(METHOD_POST)){
            doPost(req,resp);
        }else if (method.equals(METHOD_PUT)){
            doPut(req,resp);
        }else if (method.equals(METHOD_DELETE)){
            doDelete(req,resp);
        }else if (method.equals(METHOD_OPTIONS)){
            doOptions(req,resp);
        }else if (method.equals(METHOD_TRACE)){
            doTrace(req,resp);
        }else {
            String errMsg = lString.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg,errArgs);
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,errMsg;
        }
    }
  //...
  }                                            

可看出,HttpServlet的service()方法就是一个模板方法,实现了整个HTTP请求的执行流程,doGet() doPost()是模板中可由子类来定制的部分。

实际上相当于servlet框架提供了一个扩展点(doGet() doPost()方法),让框架用户在不用修改servlet框架源码的情况下,将业务代码通过扩展点嵌入框架中执行。

2. JUnit TestCase

和servlet类似,junit框架也通过模板模式提供一些功能扩展点(setUp() tearDown()等),让框架用户可在这些扩展点扩展功能。

在使用junit测试框架编写单元测试时,编写的测试类都要继承框架提供的TestCase类,在TestCase类中,runBare()方法是模板方法,定义执行测试用例的整体流程:先执行setUp做些准备工作,再执行runTest方法运行真正的测试代码,最后执行tearDown做扫尾工作。

TestCase类的具体代码如下,尽管setUp和tearDown并非抽象方法,还提供默认实现,不强制子类重新是吸纳,但也可在子类中定制,所以符合模板模式的定义。

public abstract class TestCase extends Assert implements Test {
    public void runBare() throws Throwable{
        Throwable exception = null;
        setUp();
        try{
            runTest();
        }catch (Throwable running){
            exception = running;
        }finally {
            try{
                tearDown();
            }catch (Throwable tearingDown){
                if (exception == null) exception = tearingDown;
            }
        }
        if (exception != null) throw exception;
    }
    
    protected void setUp() throws Exception{}
    
    protected void tearDown() throws Exception{}
}

回调的原理解析

相较于普通的方法调用,回调是一种双向调用关系,A类事先注册某个函数F到B类,A类在调用B类的P函数时,B类反过来调用A类注册给他的F函数,这里的F函数就是“回调函数”。A调用B,B反过来调用A,这种调用机制就叫做“回调”。

A如何将回调函数传递给B类呢?不同的编程语言,有不同的实现方法,C语言使用函数指针,java使用报过了回调函数的类对象,简称为回调对象。举例如下:

public interface ICallback {
    void methodToCallback();
}

public class BClass {
    public void process(ICallback callback){
        //...
        callback.methodToCallback();
        //...
    }
}
public class AClass {
    public static void main(String[] args) {
        BClass b = new BClass();
        b.process(new ICallback() {//回调对象
            @Override
            public void methodToCallback() {
                System.out.println("call back me.");
            }
        });
    }
}

上述代码是java回调的典型代码实现,从代码中可看出,回调跟模板模式一样,也有复用和扩展的功能。除了回调函数,BClass类的process()方法中的逻辑都可复用。如果ICallback、BClass是框架代码,AClass是使用框架的客户端代码,通过ICallback定制process()方法,也就是说,框架因此具有扩展的能力。

实际上,回调不仅可用在代码设计上,在更高层的架构设计上也较为常用。如通过三方支付系统来实现支付功能,用户在发起支付请求后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的URL)给第三方,等三方支付系统执行完成后,将结果通过回调接口返回给用户。

回调可分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指在函数返回后执行回调函数。上面的代码实际上是同步回调的实现方式,在process()函数返回之前,执行完回调函数methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付后不需等待回调接口被调用就直接返回。从应用场景看,同步回调看上去更像模板模式,异步回调更像观察者模式。

举例1:JdbcTemplate

spring提供很多template类,如JdbcTemplate RedisTemplate RestTemplate,尽管都叫xxxTemplate,但是并非基于模板模式实现,而是基于回调实现,确切的说是同步回调。而同步回调从应用场景上很像模板模式,所以,命名上用template作为后缀。

以JdbcTemplate 为例分析,java提供jdbc类库封装不同类型的数据库操作,不过直接用jdbc编写操作数据库的代码稍微复杂,如下面使用jdbc查询用户信息的代码。

public class JdbcDemo {
    public User queryUser(long id){
        Connection conn = null;
        Statement stmt = null;
        try{
            //1.加载驱动
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo","");
            //2. 创建statement类对象,用来执行SQL语句
            stmt = conn.createStatement();
            //3. resultSet类,用来存放获取的结果集
            String sql = "select * from user where id="+id;
            ResultSet resultSet = stmt.executeQuery(sql);
            
            String eid = null,ename = null, price = null;
            while(resultSet.next()){
                User user = new User();
                user.setId(resultSet.getLong("id"));
                user.setName(resultSet.getString("name"));
                user.setTelephone(resultSet.getString("telephone"));
                return user;
            }
        }catch (ClassNotFoundException e){
            //TODO log
        }catch (SQLException e){
            //todo log
        }finally {
            if (conn != null){
                try {
                    conn.close();
                }catch (SQLException e){
                    //todo log
                }
            }
            if (stmt != null){
                try {
                    stmt.close();
                }catch (SQLException e){
                    //todo log
                }
            }
        }
        return null;
    }
}

queryUser()方法包含很多流程性质的代码,和业务无关,如加载驱动、创建数据库连接、创建statement、关闭连接、关闭statement、处理异常。针对不同的SQL执行请求,这些流程性质的代码相同,可复用。spring提供JdbcTemplate进一步封装,简化数据库编程,使用JdbcTemplate查询用户信息,只需要编写跟这个业务有关的代码。包括查询用户的SQL语句、查询结果与User对象之间的映射关系。其他流程性质的代码都封装在JdbcTemplate类中,不需要每次都重新编写。重写后:

public class JdbcTemplateDemo {
    private JdbcTemplate jdbcTemplate;
    public User queryUser(long id){
        String sql = "select * from user where id="+id;
        return jdbcTemplate.query(sql,new UserRowMapper().get(0));
    }


    private class UserRowMapper implements RowMapper<User> {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getLong("id"));
            user.setName(rs.getString("name"));
            user.setTelephone(rs.getString("telephone"));
            return user;
        }
    }
}

而JdbcTemplate的底层如何实现的呢?JdbcTemplate通过回调机制,将不变的执行流程抽离出来,放到模板方法execute()中,将可变的部分设计为回调StatementCallback,由用户定制。query函数是对execute函数的二次封装,让接口用起来更方便。

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return (List)this.query((String)sql, (ResultSetExtractor)(new RowMapperResultSetExtractor(rowMapper)));
}
 public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
    Assert.notNull(sql, "SQL must not be null");
    Assert.notNull(rse, "ResultSetExtractor must not be null");
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Executing SQL query [" + sql + "]");
    }

    class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
        QueryStatementCallback() {
        }

        public T doInStatement(Statement stmt) throws SQLException {
            ResultSet rs = null;

            Object var4;
            try {
                rs = stmt.executeQuery(sql);
                ResultSet rsToUse = rs;
                if (JdbcTemplate.this.nativeJdbcExtractor != null) {
                    rsToUse = JdbcTemplate.this.nativeJdbcExtractor.getNativeResultSet(rs);
                }

                var4 = rse.extractData(rsToUse);
            } finally {
                JdbcUtils.closeResultSet(rs);
            }

            return var4;
        }

        public String getSql() {
            return sql;
        }
    }

    return this.execute((StatementCallback)(new QueryStatementCallback()));
}
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(this.getDataSource());
    Statement stmt = null;

    Object var7;
    try {
        Connection conToUse = con;
        if (this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
            conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
        }

        stmt = conToUse.createStatement();
        this.applyStatementSettings(stmt);
        Statement stmtToUse = stmt;
        if (this.nativeJdbcExtractor != null) {
            stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
        }

        T result = action.doInStatement(stmtToUse);
        this.handleWarnings(stmt);
        var7 = result;
    } catch (SQLException var11) {
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, this.getDataSource());
        con = null;
        throw this.getExceptionTranslator().translate("StatementCallback", getSql(action), var11);
    } finally {
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, this.getDataSource());
    }

    return var7;
}
应用举例2:setClickListener()

在客户端开发中,经常给控件注册事件监听器,如下,在Android应用开发中,给button控件的点击事件注册监听器。

Button button = (Button)findViewById(R.id.button);
button.setOnclickListener(new OnclickListener(){
	@Override
	public void onclick(View v){
		System.out.println("I am clicked.");
	}
});

从代码结构上看,事件监听器很像回调,即传递一个包含回调函数onclick()的对象给另一个函数。从应用场景看,又像观察者模式,即先注册观察者onClickListener,当用户点击按钮,发送点击事件给观察者,并执行相应的onClick()函数。

回调分为同步回调和异步回调,这里的回调算是异步回调,往setOnClickListener()函数中注册好回调函数后,并不需要等待回调函数执行,也印证了异步回调比较像观察者模式。

应用举例3:addShutdownHook()

hook,钩子,有人觉得hook是callback的一种应用,callback更侧重语法机制的描述,hook更侧重应用场景的描述。hook比较经典的应用场景是tomcat和jvm的shutdown hook。以jvm举例,提供Runtime.addShutdownHook(Thread hook)方法,可注册一个jvm关闭的hook。当应用程序关闭时,jvm自动调用hook代码,示例:

public class ShutdownHookDemo {
    private static class ShutdownHook extends Thread{
        @Override
        public void run() {
            System.out.println("I am called during shutting down.");
        }
    }

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new ShutdownHook());
    }
}

再看addShutdownHook()的代码实现,如下:

public class Runtime {
	public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }
}

class ApplicationShutdownHooks {
	static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }
    /* Iterates over all application hooks creating a new thread for each
     * to run in. Hooks are run concurrently and this method waits for
     * them to finish.
     */
    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            try {
                hook.join();
            } catch (InterruptedException x) { }
        }
    }
}

有关hook的逻辑被封装到ApplicationShutDownHooks类,当应用程序关闭时,jvm会调用该类的runHooks()方法,创建多个线程,并发执行多个hook。注册完hook后,并不需要等待hook的执行完成,也算是异步回调。

模板模式vs回调

从应用场景和代码实现的角度,对比模板模式和回调

应用场景看,同步回调和模板模式几乎一致,都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调和模板模式有很大区别,更像是观察者模式。

从代码实现上看,回调和模板模式完全不同,回调基于组合关系实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系实现,子类重写父类的抽象方法,是一种类之间的关系。

组合优于继承,在代码实现上,回调相对于模板模式更灵活。

  • 像java这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可使用匿名类创建回调对象,不用事先定义类;模板模式针对不同的实现都要定义不同的子类
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,即便只用到其中一个模板方法,子类必须实现所有的抽象方法;而回调更灵活,只需往用到的模板方法中注入回调对象即可。

callback更加灵活,适合算法逻辑较少的场景,如guava的Futures.addCallback回调onSuccess onFailure方法,而模板模式更适合复杂的场景,且子类可复用父类提供的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值