簡介
模板方法模式是最為常見的幾個模式之一(也比較簡單),是基于繼承實現代碼復用的基本技術。
模板方法模式(TemplateMethod Pattern)的定義是:首先定義了一個由若干執(zhí)行步驟組成的執(zhí)行過程(形成模板),而將一些步驟延遲在子類中實現,使得子類能夠對其中一個或者多個具體步驟進行重新定義,從而改變最終的執(zhí)行結果。
模板方法模式的UML類圖如下:

這里涉及到了兩個角色:
抽象模板 AbstractTemplate
- 定義并實現了一個模板方法。這個模板方法一般是一個具體的方法,它定義了一個頂級邏輯的框架,而邏輯的組成步驟由抽象方法和其他具體方法組成,抽象方法在子類中實現。
- 定義了一個或者多個抽象方法,這個/些抽象方法是模板方法的一部分,組成模板方法內一個或者多個步驟。
具體模板 ConcreteTemplate
- 實現抽象父類定義的一個或者多個抽象方法,它們是模板方法的組成步驟。
- 每一個抽象模板都可以有任意多個具體模板的實現,每一個具體模板都可以給出這些抽象方法的不同實現,從而改變模板方法最后的執(zhí)行結果。
模板方法模式中的方法
模板方法模式中的方法可以分為兩大類:模板方法和基本方法
模板方法
模板方法是定義在抽象類中的,把基本操作組合在一起形成一個總的執(zhí)行過程。
一個抽象類可以有任意多個模板方法,而不限于一個,每一個模板方法都可以調用任意多個具體方法。
基本方法
基本方法又可以進一步劃分成:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)
- 抽象方法:抽象方法由抽象類聲明并最終由具體子類實現,從而改變模板方法的最終執(zhí)行結果
- 具體方法:具體方法由抽象類聲明并實現,子類不一定需要實現(除非是需要重寫改方法)
- 鉤子方法:鉤子方法由抽象類聲明并實現,而子類會對其進行拓展,通常抽象類給出的實現是一個空實現,作為方法的默認實現。
使用場景
我之前做過的一個業(yè)務場景是:對短信的文本內容進行敏感詞過濾,需要將文本內容與敏感詞詞庫做匹配,通過加載敏感詞文件即可獲取全部的違禁詞,敏感詞文件可以放在服務器內的某一個具體路徑,也可以是放在項目內的Resource目錄下,在這兩個位置下加載方式是有區(qū)別的,但是除此之外違禁詞的讀取、存放都是相同的。
定義了一個接口SensitiveWordLoader、一個抽象類SensitiveWordAbstractFileLoader以及兩個具體實現SensitiveWordExternalFileLoader和SensitiveWordJarFileLoader,UML類圖如下:

由于敏感詞詞庫也可以存放在各類數據庫中,從文件中加載只是其中的一種方法,因此提供了一個接口
SensitiveWordLoader供抽象類SensitiveWordAbstractFileLoader實現,SensitiveWordAbstractFileLoader中的load方法即是模板方法,其中定義了敏感詞文件流的獲取以及從文件流中讀取敏感詞并存放,其中的extractSensitiveWordFromSingleLine是具體方法,用于定義從敏感詞文件中逐行提取敏感詞的邏輯,initInputStream是抽象類中定義的輸入流初始化方法,在不同加載方式下獲取輸入流的方式均不相同因此需要交由子類進行實現;模板方法load內的主要執(zhí)行邏輯由具體方法extractSensitiveWordFromSingleLine和抽象方法initInputStream完成。各類代碼如下:
SensitiveWordLoader
package com.cube.dp.tmp;
import org.springframework.lang.NonNull;
import java.util.List;
/**
* @author cube.li
* @date 2021/9/29 17:45
* <p>
* 違禁詞加載器
*/
public interface SensitiveWordLoader {
/**
* 加載違禁詞
*
* @return 違禁詞列表
*/
@NonNull
List<String> load();
}
SensitiveWordAbstractFileLoader
package com.cube.dp.tmp;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.lang.NonNull;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author cube.li
* @date 2021/9/29 17:46
* <p>
* 抽象違禁詞文件加載器
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class SensitiveWordAbstractFileLoader implements SensitiveWordLoader {
/**
* 違禁詞文件路徑
*/
protected String filePath;
/**
* 文件字符集
*/
protected Charset charset;
/**
* 關鍵詞分隔符
*/
protected String separator;
protected InputStream inputStream;
/**
* 加載標識
*/
private static final AtomicBoolean LOAD_FLAG = new AtomicBoolean(false);
/**
* 違禁詞列表
*/
private static List<String> sensitiveWordList;
public SensitiveWordAbstractFileLoader(String filePath, Charset charset, String separator) {
this.filePath = filePath;
this.charset = charset;
this.separator = separator;
sensitiveWordList = new ArrayList<>();
initInputStream();
}
/**
* 初始化inputStream
*/
public abstract void initInputStream();
@Override
@NonNull
public List<String> load() {
if (LOAD_FLAG.getAndSet(true)) {
return sensitiveWordList;
}
InputStreamReader reader = null;
BufferedReader bufferedReader = null;
try {
reader = new InputStreamReader(inputStream, charset.name());
bufferedReader = new BufferedReader(reader);
String singleLine;
while ((singleLine = bufferedReader.readLine()) != null) {
sensitiveWordList.addAll(extractSensitiveWordFromSingleLine(singleLine.trim()));
}
return sensitiveWordList;
} catch (Exception e) {
e.printStackTrace();
LOAD_FLAG.set(false);
throw new IllegalStateException("load sensitive word file failed,msg =" + e.getMessage());
} finally {
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(reader);
IOUtils.closeQuietly(bufferedReader);
}
}
/**
* 從文件單行內容中獲取違禁詞
*
* @param singleLine 單行內容
* @return 提取到的違禁詞列表
*/
@NonNull
private List<String> extractSensitiveWordFromSingleLine(String singleLine) {
String[] array = singleLine.split(separator);
List<String> list = new ArrayList<>();
for (String str : array) {
if (StringUtils.isNotBlank(str)) {
list.add(str);
}
}
return list;
}
public String getFilePath() {
return filePath;
}
public Charset getCharset() {
return charset;
}
public String getSeparator() {
return separator;
}
public InputStream getInputStream() {
return inputStream;
}
public static List<String> getSensitiveWordList() {
return sensitiveWordList;
}
}
SensitiveWordJarFileLoader
package com.cube.dp.tmp;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author cube.li
* @date 2021/9/30 10:10
* <p>
* 加載jar包內的違禁詞文件加載器
*/
@SuppressWarnings("unused")
public class SensitiveWordJarFileLoader extends SensitiveWordAbstractFileLoader {
public SensitiveWordJarFileLoader(String relativePath, String separator) {
super(relativePath, StandardCharsets.UTF_8, separator);
}
public SensitiveWordJarFileLoader(String relativePath) {
super(relativePath, StandardCharsets.UTF_8, ",");
}
/**
* 構造器
*
* @param relativePath 相對路徑,包含文件名(位于resources目錄下)
* @param charset 字符集
* @param separator 分隔符
*/
public SensitiveWordJarFileLoader(String relativePath, Charset charset, String separator) {
super(relativePath, charset, separator);
}
@Override
public void initInputStream() {
try {
ClassPathResource classPathResource = new ClassPathResource(filePath);
inputStream = classPathResource.getInputStream();
} catch (IOException e) {
throw new IllegalStateException("load sensitive word file in jar failed,message = " + e.getMessage());
}
}
}
SensitiveWordExternalFileLoader
package com.cube.dp.tmp;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author cube.li
* @date 2021/9/30 10:01
* <p>
* 違禁詞外部文件加載器(加載服務器內部,jar包外部)
*/
@SuppressWarnings("unused")
public class SensitiveWordExternalFileLoader extends SensitiveWordAbstractFileLoader {
public SensitiveWordExternalFileLoader(String externalFileAbsolutePath, String separator) {
this(externalFileAbsolutePath, StandardCharsets.UTF_8, separator);
}
public SensitiveWordExternalFileLoader(String externalFileAbsolutePath) {
this(externalFileAbsolutePath, ",");
}
/**
* 構造器
*
* @param externalFileAbsolutePath 違禁詞文件的絕對路徑
* @param charset 字符集
* @param separator 違禁詞分隔符
*/
public SensitiveWordExternalFileLoader(String externalFileAbsolutePath, Charset charset, String separator) {
super(externalFileAbsolutePath, charset, separator);
}
@Override
public void initInputStream() {
File file = new File(filePath);
try {
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new IllegalStateException("external sensitive word file not found,filePath = " + filePath);
}
}
}
通過上面的例子可以看出,使用模板方法模式實現后的代碼復用率極高,子類只需要專注其中的一個或者多個步驟即可,無須將模板方法內的頂層執(zhí)行邏輯全部重寫,代碼的閱讀性和可拓展性極高。有的同學可能會問到,為什么這個例子里沒有鉤子方法(Hook Method)呢?我這里的業(yè)務場景比較簡單,因此不需要借助鉤子方法實現邏輯。
總結
其實各種設計模板無非是前人在無數的業(yè)務開發(fā)中總結出來的一套模板,說到底就是合理的封裝、繼承和解耦,設計模式中的各種概念是高度抽象的結果,我們只需要重點關注每個設計模式的實現思路
,不必完全對設計模式進行生搬硬套,所謂盡信書則不如無書,結合實際應用場景靈活運用不拘于形才是最佳。
代碼詳情請點擊:示例代碼