当前位置 : 主页 > 编程语言 > java >

JAVA设计模式之模板模式

来源:互联网 收集:自由互联 发布时间:2023-09-03
模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。 这里的“算法”,可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架

模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。

这里的“算法”,可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

代码实现示例,如下所示。templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活。

创建一个抽象类,定义算法的骨架:

public abstract class AbstractTemplate {
    // 模板方法,定义算法的骨架
    public final void templateMethod() {
        step1();
        step2();
        step3();
    }

    // 基本方法,定义算法中不会变化的步骤
    private void step1() {
        System.out.println("Step 1: Prepare the ingredients.");
    }

    // 抽象方法,定义算法中需要子类实现的步骤
    protected abstract void step2();

    // 基本方法,定义算法中不会变化的步骤
    private void step3() {
        System.out.println("Step 3: Serve the dish.");
    }
}

创建具体的子类,实现抽象类中定义的抽象方法:

public class ConcreteTemplateA extends AbstractTemplate {
    @Override
    protected void step2() {
        System.out.println("Step 2 (A): Cook the dish using method A.");
    }
}

public class ConcreteTemplateB extends AbstractTemplate {
    @Override
    protected void step2() {
        System.out.println("Step 2 (B): Cook the dish using method B.");
    }
}

在客户端代码中使用模板方法:

public class Main {
    public static void main(String[] args) {
        AbstractTemplate templateA = new ConcreteTemplateA();
        AbstractTemplate templateB = new ConcreteTemplateB();

        System.out.println("Using Template A:");
        templateA.templateMethod();

        System.out.println("\nUsing Template B:");
        templateB.templateMethod();
    }
}

运行上面的程序,输出如下:

vbnetCopy code
Using Template A:
Step 1: Prepare the ingredients.
Step 2 (A): Cook the dish using method A.
Step 3: Serve the dish.

Using Template B:
Step 1: Prepare the ingredients.
Step 2 (B): Cook the dish using method B.
Step 3: Serve the dish.

AbstractTemplate是一个抽象类,它定义了一个名为templateMethod的模板方法。该方法包含三个步骤:step1step2step3。其中,step1step3是基本方法,它们的实现在抽象类中定义且不会改变。step2是一个抽象方法,需要子类(如ConcreteTemplateAConcreteTemplateB)根据具体需求实现。客户端代码通过创建子类的实例并调用templateMethod方法来执行算法。

源码中的作用

复用

模板模式有两大作用:复用和扩展。

模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 step2()留给子类来实现。所有的子类都可以复用父类中模板方法定义的流程代码。。

Java InputStream

Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。

把 InputStream 部分相关代码贴在了下面。在代码中,read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了 read(),只是参数跟模板方法不同。

public abstract class InputStream implements Closeable {
    //...省略其他代码...

     public int read(byte b[], int off, int len) throws IOException {
        Objects.checkFromIndexSize(off, len, b.length);
        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 ee) {
        }
        return i;
    }

    public abstract int read() throws IOException;
}

// 这里有一个具体的实现类。用于从一个字节缓冲区中读取一个字节。方法的签名和功能如下:
public class ByteArrayInputStream extends InputStream {
    //...省略其他代码...

    @Override
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
    }
}
Java AbstractList

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

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();
}

其在ArrayList中的实现如下:

public void add(int index, E element) {
    rangeCheckForAdd(index);
    checkForComodification();
    root.add(offset + index, element);
    updateSizeAndModCount(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.xzg.cd.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(HelloServlet),然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,然后输出数据(“Hello world”)到网页。

HttpServlet 的 service() 函数长什么样子。

public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException
{
    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 ServletException, IOException
{
    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 logic
            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 = lStrings.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 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。

spring中的核心refresh

spring中存在大量的模板方法,核心的refresh方法:

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
            contextRefresh.end();
        }
    }
}
MyBatis

MyBatis框架中也有运用模板方法设计模式的例子,尽管它们的实现方式可能没有那么明显。以下是一个常见的例子:

  1. BaseExecutor:在MyBatis中,BaseExecutor是一个抽象类,它提供了查询、更新等数据库操作的通用实现。具体的数据库操作是通过它的子类(如SimpleExecutorReuseExecutorBatchExecutor)来实现的。在BaseExecutor中,有一个名为query的模板方法,它包含了查询操作的通用逻辑。这个方法的部分实现如下:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}


 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }


protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

在这个query方法中,BaseExecutor定义了查询操作的通用逻辑,例如异常处理、资源清理等。而具体的查询操作是通过queryFromDatabase方法来实现的,这是一个抽象方法,需要由BaseExecutor的子类实现。通过这种方式,MyBatis实现了对数据库操作的通用逻辑和具体逻辑的分离,提高了代码的可维护性和可扩展性。

虽然MyBatis中使用模板方法设计模式的例子没有Spring那么明显,但在实际开发过程中,可以借鉴这种设计思想,将通用逻辑抽象到模板中,以降低代码复杂性。

应用场景

电商系统

将通用逻辑和特定逻辑分离,提高代码复用性和可维护性。以下是一些典型的应用场景:

  1. 支付流程:电商系统通常需要支持多种支付方式(如信用卡支付、支付宝支付、微信支付等)。虽然不同的支付方式在实现细节上有所不同,但它们的整体流程是相似的。可以使用模板方法设计模式创建一个支付流程抽象类,定义通用的支付流程骨架,然后通过子类实现各种具体支付方式的逻辑。
  2. 订单处理:电商系统的订单处理流程通常包括一系列步骤,如验证库存、计算价格、生成运单等。这些步骤中,有些是通用的,而有些可能因订单类型、商品类型等因素而异。可以使用模板方法设计模式创建一个订单处理抽象类,定义通用的订单处理流程骨架,然后通过子类实现特定订单类型或商品类型的逻辑。
  3. 促销策略:电商系统中的促销活动通常具有多种策略(如满减、打折、赠品等)。尽管不同策略的具体实现不同,但它们都需要进行一些通用操作(如获取用户信息、验证促销条件等)。可以使用模板方法设计模式创建一个促销策略抽象类,定义通用的促销操作骨架,然后通过子类实现各种具体促销策略的逻辑。
  4. 报表生成:电商系统需要生成各种报表(如销售报表、库存报表、财务报表等)。这些报表在数据查询和报表样式上可能有所不同,但它们的生成流程是类似的(如查询数据、生成报表、导出文件等)。可以使用模板方法设计模式创建一个报表生成抽象类,定义通用的报表生成流程骨架,然后通过子类实现具体报表类型的逻辑。

这些应用场景展示了如何在电商系统中利用模板方法设计模式来简化代码结构和提高可维护性。在实际开发过程中,可以根据业务需求和系统架构选择合适的设计模式。

以支付流程为例:

在电商系统中,支付流程是一个典型的应用场景。可以使用模板方法设计模式创建一个抽象类PaymentProcessor来定义通用的支付流程骨架,然后通过子类实现各种具体支付方式的逻辑。以下是一个简化的代码示例及其中文注释:

创建一个抽象类PaymentProcessor来定义支付流程:

public abstract class PaymentProcessor {

    // 模板方法,定义支付流程骨架
    public final void processPayment(Order order) {
        // 获取支付方式(如信用卡、支付宝、微信等)
        String paymentMethod = getPaymentMethod();

        // 验证订单信息(如订单金额、收货地址等)
        validateOrder(order);

        // 验证支付信息(如支付账号、支付密码等)
        validatePaymentInfo(paymentMethod);

        // 执行支付
        executePayment(paymentMethod, order);

        // 发送支付通知
        sendPaymentNotification(order);
    }

    // 获取支付方式,具体实现由子类提供
    protected abstract String getPaymentMethod();

    // 验证订单信息,通用逻辑
    private void validateOrder(Order order) {
        // 验证订单信息的实现
    }

    // 验证支付信息,具体实现由子类提供
    protected abstract void validatePaymentInfo(String paymentMethod);

    // 执行支付,具体实现由子类提供
    protected abstract void executePayment(String paymentMethod, Order order);

    // 发送支付通知,通用逻辑
    private void sendPaymentNotification(Order order) {
        // 发送支付通知的实现
    }
}

创建一个具体的支付处理器类AlipayProcessor来实现支付宝支付方式:

public class AlipayProcessor extends PaymentProcessor {

    @Override
    protected String getPaymentMethod() {
        return "Alipay";
    }

    @Override
    protected void validatePaymentInfo(String paymentMethod) {
        // 验证支付宝支付信息的实现
    }

    @Override
    protected void executePayment(String paymentMethod, Order order) {
        // 执行支付宝支付的实现
    }
}

创建一个具体的支付处理器类WechatPayProcessor来实现微信支付方式:

public class WechatPayProcessor extends PaymentProcessor {

    @Override
    protected String getPaymentMethod() {
        return "WechatPay";
    }

    @Override
    protected void validatePaymentInfo(String paymentMethod) {
        // 验证微信支付信息的实现
    }

    @Override
    protected void executePayment(String paymentMethod, Order order) {
        // 执行微信支付的实现
    }
}

PaymentProcessor定义了支付流程的通用骨架,如验证订单信息、发送支付通知等。具体的支付方式(如支付宝、微信支付等)由子类AlipayProcessorWechatPayProcessor实现。这样可以轻松地添加新的支付方式,而不需要修改现有的支付流程代码,从而提高代码的可维护性和可扩展性。

使用AlipayProcessorWechatPayProcessor来处理不同的支付方式。例如,当用户选择支付宝支付时,可以创建一个AlipayProcessor实例来处理支付流程:

public class PaymentService {

    public void processPayment(Order order, String paymentType) {
        PaymentProcessor paymentProcessor;

        if ("Alipay".equalsIgnoreCase(paymentType)) {
            paymentProcessor = new AlipayProcessor();
        } else if ("WechatPay".equalsIgnoreCase(paymentType)) {
            paymentProcessor = new WechatPayProcessor();
        } else {
            throw new IllegalArgumentException("Unsupported payment type: " + paymentType);
        }

        paymentProcessor.processPayment(order);
    }
}

PaymentService类中,根据用户选择的支付方式创建相应的支付处理器实例,然后调用processPayment方法来处理支付流程。这种方式使得支付流程的处理逻辑更加清晰,易于维护和扩展。

需要注意的是,为了更好地支持新的支付方式,可以考虑使用工厂模式或策略模式来创建支付处理器实例,进一步提高代码的可维护性和可扩展性。

这个例子展示了如何在电商系统的支付流程中应用模板方法设计模式。通过将通用逻辑和特定逻辑分离,可以更轻松地添加新的支付方式,同时保持代码的清晰和易于维护。在实际开发过程中,可以根据业务需求和系统架构灵活地运用模板方法设计模式。

在线考试系统

在一个在线考试系统中,可以使用模板方法设计模式处理不同类型的题目。在这个场景中,可以将通用逻辑(例如展示题目、计算分数等)与特定题目类型的逻辑(例如判断题的判断逻辑、选择题的选择逻辑等)分离。

创建一个抽象类Question来定义题目处理的通用骨架:

public abstract class Question {

    // 模板方法,定义题目处理流程
    public final void processQuestion(String userAnswer) {
        // 展示题目
        displayQuestion();

        // 检查用户答案
        boolean isCorrect = checkAnswer(userAnswer);

        // 计算分数
        int score = calculateScore(isCorrect);

        // 显示结果
        displayResult(isCorrect, score);
    }

    // 展示题目,通用逻辑
    protected void displayQuestion() {
        // 展示题目的实现
    }

    // 检查用户答案,具体实现由子类提供
    protected abstract boolean checkAnswer(String userAnswer);

    // 计算分数,通用逻辑
    protected int calculateScore(boolean isCorrect) {
        // 计算分数的实现
    }

    // 显示结果,通用逻辑
    protected void displayResult(boolean isCorrect, int score) {
        // 显示结果的实现
    }
}

创建一个具体的题目类MultipleChoiceQuestion来实现选择题的逻辑:

public class MultipleChoiceQuestion extends Question {

    private String correctAnswer;

    public MultipleChoiceQuestion(String correctAnswer) {
        this.correctAnswer = correctAnswer;
    }

    @Override
    protected boolean checkAnswer(String userAnswer) {
        // 检查选择题答案的实现
        return correctAnswer.equalsIgnoreCase(userAnswer);
    }
}

创建一个具体的题目类TrueOrFalseQuestion来实现判断题的逻辑:

public class TrueOrFalseQuestion extends Question {

    private boolean correctAnswer;

    public TrueOrFalseQuestion(boolean correctAnswer) {
        this.correctAnswer = correctAnswer;
    }

    @Override
    protected boolean checkAnswer(String userAnswer) {
        // 检查判断题答案的实现
        return correctAnswer == Boolean.parseBoolean(userAnswer);
    }
}

Question定义了题目处理的通用骨架,如展示题目、计算分数等。具体的题目类型(如选择题、判断题等)由子类MultipleChoiceQuestionTrueOrFalseQuestion实现。这样,可以轻松地添加新的题目类型,而不需要修改现有的题目处理代码,从而提高代码的可维护性和可扩展性。

使用MultipleChoiceQuestionTrueOrFalseQuestion来处理不同类型的题目。例如,当用户回答一个选择题时,可以创建一个MultipleChoiceQuestion实例来处理题目:

public class ExamService {

    public void processQuestion(Question question,String userAnswer) {
        // 调用processQuestion方法来处理题目
        question.processQuestion(userAnswer);
    }
}

ExamService类中,调用processQuestion方法来处理不同类型的题目。这种方式使得题目处理的逻辑更加清晰,易于维护和扩展。 需要注意的是,为了更好地支持新的题目类型,可以考虑使用工厂模式或策略模式来创建题目实例,进一步提高代码的可维护性和可扩展性。 这个例子展示了如何在一个在线考试系统中应用模板方法设计模式。通过将通用逻辑和特定逻辑分离,可以更轻松地添加新的题目类型,同时保持代码的清晰和易于维护。在实际开发过程中,可以根据业务需求和系统架构灵活地运用模板方法设计模式。

Callback回调

回调的原理、实现和应用,以及它跟模板模式的区别和联系。

区别:

  1. 设计范式:模板方法设计模式通常用于面向对象编程(OOP),它依赖于继承和多态来实现代码复用。回调函数则通常用于函数式编程,它通过将函数作为参数传递给其他函数来实现代码复用。
  2. 实现方式:模板方法设计模式依赖于抽象类和子类之间的继承关系。在抽象类中定义一个算法的骨架,并将某些步骤延迟到子类中实现。而回调函数通过将一个函数作为参数传递给另一个函数,让调用者可以自定义特定的行为。

联系:

  1. 目的:模板方法设计模式和回调函数都旨在将变化的部分与不变的部分分离,提高代码的复用性和可维护性。
  2. 实现相互关系:在某些情况下,模板方法设计模式可以通过回调函数来实现。例如,在Java中,可以使用匿名内部类或者Lambda表达式作为回调函数,实现模板方法设计模式的目标。类似地,在面向对象的语言中,回调函数也可以通过模板方法设计模式来实现。

回调的原理解析

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”,如果学习过JavaScript这个概念应该十分清楚。

A 类如何将回调函数传递给 B 类呢?不同的编程语言,有不同的实现方法。C 语言可以使用函数指针,Java 则需要使用包裹了回调函数的类对象,简称为回调对象。这里我用 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()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

JdbcTemplate的回调应用

Spring 提供了很多 Template 类,比如,JdbcTemplate、RedisTemplate、RestTemplate。尽管都叫作 xxxTemplate,但它们并非基于模板模式来实现的,而是基于回调来实现的,确切地说应该是同步回调。而同步回调从应用场景上很像模板模式,所以,在命名上,这些类使用 Template(模板)这个单词作为后缀。

这些 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/ydlclass", "root", "123");
            //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,对 JDBC 进一步封装,来简化数据库编程。使用 JdbcTemplate 查询用户信息,只需要编写跟这个业务有关的代码,其中包括,查询用户的 SQL 语句、查询结果与 User 对象之间的映射关系。其他流程性质的代码都封装在了 JdbcTemplate 类中,不需要每次都重新编写。我用 JdbcTemplate 重写了上面的例子,代码简单了很多,如下所示:

public class JdbcTemplateDemo {
    @Resource
    private JdbcTemplate jdbcTemplate;
    public User queryUser(long id) {
        String sql = "select * from user where id="+id;
        return jdbcTemplate.query(sql, new UserRowMapper()).get(0);
    }
    
    class UserRowMapper implements RowMapper<User> {
        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() 函数的二次封装,让接口用起来更加方便。

@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
}

@Override
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 (logger.isDebugEnabled()) {
        logger.debug("Executing SQL query [" + sql + "]");
    }
    class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
        @Override
        public T doInStatement(Statement stmt) throws SQLException {
            ResultSet rs = null;
            try {
                rs = stmt.executeQuery(sql);
                ResultSet rsToUse = rs;
                if (nativeJdbcExtractor != null) {
                    rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
                }
                return rse.extractData(rsToUse);
            }
            finally {
                JdbcUtils.closeResultSet(rs);
            }
        }
        @Override
        public String getSql() {
            return sql;
        }
    }
    return execute(new QueryStatementCallback());
}

@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(getDataSource());
    Statement stmt = null;
    try {
        Connection conToUse = con;
        if (this.nativeJdbcExtractor != null &&
            this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
            conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
        }
        stmt = conToUse.createStatement();
        applyStatementSettings(stmt);
        Statement stmtToUse = stmt;
        if (this.nativeJdbcExtractor != null) {
            stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
        }
        T result = action.doInStatement(stmtToUse);
        handleWarnings(stmt);
        return result;
    }
    catch (SQLException ex) {
        // Release Connection early, to avoid potential connection pool deadlock
        // in the case when the exception translator hasn't been initialized yet.
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, getDataSource());
        con = null;
        throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
    }
    finally {
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, getDataSource());
    }
}

模板模式 VS 回调

回调的原理、实现和应用到此就都讲完了。从应用场景和代码实现两个角度,来对比一下模板模式和回调。

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

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

回调相对于模板模式会更加灵活,主要体现在下面几点:

像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。

回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。

如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,只需要往用到的模板方法中注入回调对象即可。

上一篇:《Java编程思想第四版》学习笔记21
下一篇:没有了
网友评论