设计模式----模板方法模式

2021-11-15 15:27:38 浏览数 (1)

模板方法模式

  • 模板方法模式
  • 介绍
  • 角色
  • 代码演示
  • 模板方法模式总结
    • 优点
    • 缺点
  • 适用场景
  • 源码分析模板方法模式的典型应用
    • Servlet 中的模板方法模式
    • Spring中的IOC容器启动---refresh()方法
  • 参考文章

模板方法模式

在程序开发中,经常会遇到这种情况:某个方法要实现的算法需要多个步骤,但其中有一些步骤是固定不变的,而另一些步骤则是不固定的。为了提高代码的可扩展性和可维护性,模板方法模式在这种场景下就派上了用场。

譬如制作一节网课的步骤可以简化为4个步骤:

  1. 制作PPT
  2. 录制视频
  3. 编写笔记
  4. 提供课程资料

其中1、2、3的动作在所有课程中的固定不变的,步骤3可有可无,步骤4在每个课程都不同(有些课程需要提供源代码,有些需要提供图片文件等)

我们可以在父类中确定整个流程的循序,并实现固定不变的步骤,而把不固定的步骤留给子类实现。甚至可以通过一个钩子方法,让子类来决定流程中某个方法的执行与否


介绍

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。


角色

AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模板方法定义在抽象类中,并由子类不加以修改地完全继承下来。模板方法是一个具体方法,它给出了一个顶层逻辑框架,而逻辑的组成步骤在抽象类中可以是具体方法,也可以是抽象方法。

基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。

  • 抽象方法:一个抽象方法由抽象类声明、由其具体子类实现。
  • 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
  • 钩子方法:可以与一些具体步骤 “挂钩” ,以实现在不同条件下执行模板方法中的不同步骤

代码演示

所以我们通过模板方法模式,在抽象类中把整个流程固定下来,其中1、2、3的实现在抽象类中完成,3的执行与否则由子类通过钩子方法来控制,4则由子类来实现

抽象类定义如下:

代码语言:javascript复制
//抽象课程类
public abstract class Course
{
    protected final void makeCourse() {
        this.makePPT();
        this.makeVideo();
        if (needWriteArticle()) {
            this.writeArticle();
        }
        this.packageCourse();
    }

    //final表示子类继承后,无法修改父类该方法
    final void makePPT() {
        System.out.println("1. 制作PPT");
    }

    final void makeVideo() {
        System.out.println("2. 制作视频");
    }

    final void writeArticle() {
        System.out.println("3. 编写课程笔记");
    }

    //钩子方法
    protected boolean needWriteArticle() {
        return false;
    }

   //抽象方法,留给子类去实现
    abstract void packageCourse();
}

其中的 makeCourse 方法是模板方法,它定义了制作网课的基本流程,makePPTmakeVideowriteArticle 这三个步骤在所有课程中都是固定的,所以用 final 关键字修饰;packageCourse 方法在所有课程中都可能不一样,所以声明为抽象方法,由子类自行实现;钩子方法 needWriteArticle 返回一个 boolean 类型的值,控制是否编写课程笔记

子类 JavaCourse,实现了抽象方法 packageCourse,重写了钩子方法 needWriteArticle

代码语言:javascript复制
public class JavaCourse extends Course {
    @Override
    void packageCourse() {
        System.out.println("4. 提供Java课程源代码");
    }

    @Override
    protected boolean needWriteArticle() {
        return true;
    }
}

子类 FECourse,实现了抽象方法 packageCourse,重写了钩子方法 needWriteArticle,其中把钩子方法的结果交给客户端确定

代码语言:javascript复制
public class FECourse extends Course {
    private boolean needWriteArticleFlag = false;
    @Override
    void packageCourse() {
        System.out.println("4.1 提供课程的前端代码");
        System.out.println("4.2 提供课程的图片等多媒体素材");
    }

    public FECourse(boolean needWriteArticleFlag) {
        this.needWriteArticleFlag = needWriteArticleFlag;
    }

    @Override
    protected boolean needWriteArticle() {
        return this.needWriteArticleFlag;
    }
}

客户端测试

代码语言:javascript复制
public class TEST
{
    public static void main(String[] args) {
                System.out.println("Java课程start---");
                Course javaCourse = new JavaCourse();
                javaCourse.makeCourse();
                System.out.println("Java课程end---n");


                System.out.println("前端课程start---");
                Course feCourse = new FECourse(false);
                feCourse.makeCourse();
                System.out.println("前端课程end---");
    }
}

输出结果

代码语言:javascript复制
Java课程start---
1. 制作PPT
2. 制作视频
3. 编写笔记
4. 提供Java课程源代码
Java课程end---

前端课程start---
1. 制作PPT
2. 制作视频
4.1 提供课程的前端代码
4.2 提供课程的图片等多媒体素材
前端课程end---

模板方法模式总结

优点

  • 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。
  • 模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用
  • 可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。
  • 在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则

缺点

需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。


适用场景

  • 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
  • 一般模板方法都会加上final关键字,防止子类重写该方法

源码分析模板方法模式的典型应用

Servlet 中的模板方法模式

Servlet(Server Applet)是Java Servlet的简称,用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。在每一个 Servlet 都必须要实现 Servlet 接口,GenericServlet 是个通用的、不特定于任何协议的Servlet,它实现了 Servlet 接口,而 HttpServlet 继承于 GenericServlet,实现了 Servlet 接口,为 Servlet 接口提供了处理HTTP协议的通用实现,所以我们定义的 Servlet 只需要继承 HttpServlet 即可。

HttpServlet 的简要代码如下所示

代码语言:javascript复制
public abstract class HttpServlet extends GenericServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doDelete(HttpServletRequest req,  HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // ...
    }
    
    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 {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            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);
        }
    }
    
    // ...省略...
}

HttpServletservice 方法中,首先获得到请求的方法名,然后根据方法名调用对应的 doXXX 方法,比如说请求方法为GET,那么就去调用 doGet 方法;请求方法为POST,那么就去调用 doPost 方法

HttpServlet 相当于定义了一套处理 HTTP 请求的模板;service 方法为模板方法,定义了处理HTTP请求的基本流程;doXXX 等方法为基本方法,根据请求方法做相应的处理,子类可重写这些方法;HttpServletRequest 中的Method则起到钩子方法的作用.

在开发javaWeb应用时,自定义的Servlet类一般都扩展 HttpServlet 类,譬如我们实现一个输出 Hello World! 的 Servlet 如下

代码语言:javascript复制
// 扩展 HttpServlet 类
public class HelloWorld extends HttpServlet {

  public void init() throws ServletException {
    // ...
  }

  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.setContentType("text/html");
      PrintWriter out = response.getWriter();
      out.println("<h1>Hello World!</h1>");
  }
  
  public void destroy() {
      // ...
  }
}

该自定义的 Servlet 重写了 doGet 方法,当客户端发起 GET 请求时将得到

Hello World!


Spring中的IOC容器启动—refresh()方法

Spring IOC容器初始化时运用到的模板方法模式

1、首先定义一个接口ConfigurableApplicationContext,声明模板方法refresh

代码语言:javascript复制
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
  /**声明了一个模板方法*/
  void refresh() throws BeansException, IllegalStateException;
}

2、抽象类AbstractApplicationContext实现了接口,主要实现了模板方法refresh(这个方法很重要,是各种IOC容器初始化的入口)的逻辑

代码语言:javascript复制
public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext, DisposableBean {

   /**模板方法的具体实现*/
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

        //注意这个方法是,里面调用了两个抽象方法refreshBeanFactory、getBeanFactory
            // 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);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);
                // 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) {
                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();
                // Reset 'active' flag.
                cancelRefresh(ex);
                // Propagate exception to caller.
                throw ex;
            }
        }
    }

这里最主要有一个抽象方法obtainFreshBeanFactory、两个钩子方法postProcessBeanFactory和onRefresh,看看他们在类中的定义

两个钩子方法:

代码语言:javascript复制
 protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    }
    
    protected void onRefresh() throws BeansException {
        // For subclasses: do nothing by default.
    }

再看看获取Spring容器的抽象方法:

代码语言:javascript复制
/**其实他内部只调用了两个抽象方法**/    
  protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
        refreshBeanFactory();
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();
        if (logger.isDebugEnabled()) {
            logger.debug("Bean factory for "   getDisplayName()   ": "   beanFactory);
        }
        return beanFactory;
    }
  protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
  public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

具体要取那种BeanFactory容器的决定权交给了子类!

3、具体实现的子类,实现了抽象方法getBeanFactory的子类有: AbstractRefreshableApplicationContext

代码语言:javascript复制
public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
        synchronized (this.beanFactoryMonitor) {
            if (this.beanFactory == null) {
                throw new IllegalStateException("BeanFactory not initialized or already closed - "  
                        "call 'refresh' before accessing beans via the ApplicationContext");
            }
            //这里的this.beanFactory在另一个抽象方法refreshBeanFactory的设置的
            return this.beanFactory;
        }
    }
}    
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
    //同样这里的this.beanFactory在另一个抽象方法中设置        
    return this.beanFactory;
    }
}

其实这里的差别还不是很大,我们可以看看另一个抽象方法refreshBeanFactory的实现,两个抽象方法的配合使用。


参考文章

设计模式 | 模板方法模式及典型应用

设计模式——Spring IoC中用到的模板方法模式

0 人点赞