模板方法模式
案例
奶茶在生活中是很常見的,很多商場(chǎng)周邊都有看到各種奶茶店。下面通過程序來模擬這一過程:
1.建立奶茶店類:
// 奶茶店
public class MilkTeaShop {
// 制作椰果奶茶
public void makeCoconutMilkTea() {
System.out.println("準(zhǔn)備調(diào)制杯");
System.out.println("加入牛奶");
System.out.println("加入椰果");
System.out.println("調(diào)制奶茶");
System.out.println("包裝入奶茶杯");
}
// 制作紅茶
public void makeBlackTea() {
System.out.println("準(zhǔn)備調(diào)制杯");
System.out.println("加入紅茶");
System.out.println("加入水");
System.out.println("調(diào)制奶茶");
System.out.println("包裝入奶茶杯");
}
}
2.客戶端使用:
public class Main {
public static void main(String[] args) {
MilkTeaShop milkTeaShop = new MilkTeaShop();
milkTeaShop.makeCoconutMilkTea();
System.out.println("----------------");
milkTeaShop.makeBlackTea();
}
}
3.使用結(jié)果:
準(zhǔn)備調(diào)制杯
加入牛奶
加入椰果
調(diào)制奶茶
包裝入奶茶杯
----------------
準(zhǔn)備調(diào)制杯
加入紅茶
加入水
調(diào)制奶茶
包裝入奶茶杯
我們分別定義了制作椰果奶茶和紅茶的方法,但是方法內(nèi)部有著很多重復(fù)的代碼。其實(shí)制作奶茶的步驟可以歸結(jié)為這幾點(diǎn):準(zhǔn)備調(diào)制杯-->放入材料-->調(diào)制奶茶-->包裝入奶茶杯。在這之中最大的不同其實(shí)就是放入材料的不同。由此可以通過模板方法模式來對(duì)代碼進(jìn)行改經(jīng)。
模式介紹
模板方法模式定義了一個(gè)算法的步驟,并允許子類別為一個(gè)或多個(gè)步驟提供其實(shí)踐方式。讓子類別在不改變算法架構(gòu)的情況下,重新定義算法中的某些步驟。
角色構(gòu)成
AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個(gè)基本操作對(duì)應(yīng)算法的一個(gè)步驟,在其子類中可以重定義或?qū)崿F(xiàn)這些步驟。同時(shí),在抽象類中實(shí)現(xiàn)了一個(gè)模板方法(Template Method),用于定義一個(gè)算法的框架,模板方法不僅可以調(diào)用在抽象類中實(shí)現(xiàn)的基本方法,也可以調(diào)用在抽象類的子類中實(shí)現(xiàn)的基本方法,還可以調(diào)用其他對(duì)象中的方法。
ConcreteClass(具體子類):它是抽象類的子類,用于實(shí)現(xiàn)在父類中聲明的抽象基本操作以完成子類特定算法的步驟,也可以覆蓋在父類中已經(jīng)實(shí)現(xiàn)的具體基本操作。
UML 類圖

代碼改造
可以看到模板方法模式結(jié)構(gòu)非常的簡(jiǎn)單,下面就對(duì)代碼進(jìn)行改進(jìn):
1.首先是抽象模板類:
// 抽象奶茶類,抽象類角色
public abstract class MilkTea {
// 模板方法
public void makeMilkTea() {
System.out.println("準(zhǔn)備調(diào)制杯");
putMaterials();
System.out.println("調(diào)制奶茶");
System.out.println("包裝入奶茶杯");
}
// 抽象方法,需子類實(shí)現(xiàn)具體細(xì)節(jié)
public abstract void putMaterials();
}
2.兩個(gè)具體奶茶子類:
椰果奶茶類:
// 椰果奶茶類,具體子類角色
public class CoconutMilkTea extends MilkTea {
@Override
public void putMaterials() {
System.out.println("加入牛奶");
System.out.println("加入椰果");
}
}
紅茶類
// 紅茶類,具體子類角色
public class BlackTea extends MilkTea {
@Override
public void putMaterials() {
System.out.println("加入紅茶");
System.out.println("加入水");
}
}
3.奶茶點(diǎn)定義制作奶茶方法
public class MilkTeaShop {
private BlackTea blackTea = new BlackTea();
private CoconutMilkTea coconutMilkTea = new CoconutMilkTea();
public void makeCoconutMilkTea() {
coconutMilkTea.makeMilkTea();
}
public void makeBlackTea() {
blackTea.makeMilkTea();
}
}
4.客戶端使用:
public class Main {
public static void main(String[] args) {
MilkTeaShop milkTeaShop = new MilkTeaShop();
milkTeaShop.makeCoconutMilkTea();
System.out.println("----------------");
milkTeaShop.makeBlackTea();
}
}
5.使用結(jié)果:
準(zhǔn)備調(diào)制杯
加入牛奶
加入椰果
調(diào)制奶茶
包裝入奶茶杯
----------------
準(zhǔn)備調(diào)制杯
加入紅茶
加入水
調(diào)制奶茶
包裝入奶茶杯
可以看到模板方法模式中最主要的就是模板方法和需要子類重寫的抽象方法,也可以在父類中定義好方法,再讓子類覆蓋以達(dá)到不同的功能。
模式應(yīng)用
模板方法模式在許多框架中都有極其廣泛的應(yīng)用,還記得最開始寫 Java Web 項(xiàng)目是使用的原生Servlet API ,下面就簡(jiǎn)單介紹一下在Servlet中的使用。
1.pom 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>design-pattern</artifactId>
<groupId>com.phoegel</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>template</artifactId>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
</project>
2.編寫簡(jiǎn)單的請(qǐng)求類:
public class HelloWorldServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("Hello World");
}
}
3.配置 web.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>helloWorldServlet</servlet-name>
<servlet-class>com.phoegel.templatemethod.analysis.HelloWorldServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloWorldServlet</servlet-name>
<url-pattern>/helloWorld</url-pattern>
</servlet-mapping>
</web-app>
4.這里簡(jiǎn)單配置 tomcat 進(jìn)行項(xiàng)目啟動(dòng):

5.啟動(dòng)成功之后,訪問地址localhost:8081/helloWorld,在頁面中會(huì)打印出
Hello World
通過追蹤源碼我們發(fā)現(xiàn)請(qǐng)求會(huì)經(jīng)過javax.servlet.http.HttpServlet.service(HttpServletRequest req, HttpServletResponse resp)方法,下面是方法具體內(nèi)容:
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) {
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
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);
}
}
這里的service()方法就是模板方法,方法內(nèi)部定義了請(qǐng)求到來時(shí)的走向骨架,從中很明顯的發(fā)現(xiàn)就是根據(jù)請(qǐng)求方式的不同調(diào)用不同的請(qǐng)求方法,而子類通過重寫請(qǐng)求方法來改變整個(gè)service()方法的調(diào)用結(jié)果。比如說doGet()方法,在HttpServlet類中只是簡(jiǎn)單的輸出了錯(cuò)誤信息:
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
但是我們?cè)?code>HelloWorldServlet類中重寫了doGet()方法,在頁面中就打輸出了我們自定義的信息。
總結(jié)
模式優(yōu)點(diǎn)
- 在父類中形式化地定義一個(gè)算法,而由它的子類來實(shí)現(xiàn)細(xì)節(jié)的處理,在子類實(shí)現(xiàn)詳細(xì)的處理算法時(shí)并不會(huì)改變算法中步驟的執(zhí)行次序。
- 模板方法模式是一種代碼復(fù)用技術(shù),它在類庫設(shè)計(jì)中尤為重要,它提取了類庫中的公共行為,將公共行為放在父類中,而通過其子類來實(shí)現(xiàn)不同的行為,它鼓勵(lì)我們恰當(dāng)使用繼承來實(shí)現(xiàn)代碼復(fù)用。
- 可實(shí)現(xiàn)一種反向控制結(jié)構(gòu),通過子類覆蓋父類的鉤子方法來決定某一特定步驟是否需要執(zhí)行。
- 在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可以提供基本方法的不同實(shí)現(xiàn),更換和增加新的子類很方便,符合單一職責(zé)原則和開閉原則。
模式缺點(diǎn)
- 需要為每一個(gè)基本方法的不同實(shí)現(xiàn)提供一個(gè)子類,如果父類中可變的基本方法太多,將會(huì)導(dǎo)致類的個(gè)數(shù)增加,系統(tǒng)更加龐大,設(shè)計(jì)也更加抽象,此時(shí),可結(jié)合橋接模式來進(jìn)行設(shè)計(jì)。
模式適用場(chǎng)景
- 對(duì)一些復(fù)雜的算法進(jìn)行分割,將其算法中固定不變的部分設(shè)計(jì)為模板方法和父類具體方法,而一些可以改變的細(xì)節(jié)由其子類來實(shí)現(xiàn)。即:一次性實(shí)現(xiàn)一個(gè)算法的不變部分,并將可變的行為留給子類來實(shí)現(xiàn)。
- 各子類中公共的行為應(yīng)被提取出來并集中到一個(gè)公共父類中以避免代碼重復(fù)。
- 需要通過子類來決定父類算法中某個(gè)步驟是否執(zhí)行,實(shí)現(xiàn)子類對(duì)父類的反向控制。
參考資料
- 大話設(shè)計(jì)模式
- 設(shè)計(jì)模式Java版本-劉偉
本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/template-method
轉(zhuǎn)載請(qǐng)說明出處,本篇博客地址:http://www.itdecent.cn/p/ad20f274c7d0