聊一聊模板方法模式
一、概述
模板方法模式,又叫模板模式,屬于23種設(shè)計(jì)模式中的行為型模式。在抽象類中公開(kāi)定義了執(zhí)行的方法,子類可以按需重寫(xiě)其方法,但是要以抽象類中定義的方式調(diào)用方法??偨Y(jié)起來(lái)就是:定義一個(gè)操作的算法結(jié)構(gòu),而將一些步驟延遲到子類中。在不改變算法結(jié)構(gòu)的情況下,子類能重定義該算法的特定步驟。
下面是模板模式的UML圖,抽象類(AbstractClass)定義公共的步驟和方法,依次調(diào)用實(shí)際的模板方法,當(dāng)然每個(gè)方法可以是抽象方法(需交給子類實(shí)現(xiàn)),也可以是提供默認(rèn)的方法。具體的類(ConcreteClass)可以重寫(xiě)所有的方法,但是不能改變抽象類中定義的整體結(jié)構(gòu)。
二、入門案例
相信大家都吃過(guò)蛋糕,現(xiàn)在市面上的蛋糕可謂是五花八門,你能想到的造型商家能給你整出來(lái),你想不到的,他們也能整出來(lái)。不過(guò)無(wú)論造型如何變化,不變的有兩種東西:“奶油”和“面包”。其余的材料隨意搭配,就湊成了各式各樣的蛋糕。
基于這個(gè)場(chǎng)景,我們來(lái)寫(xiě)一個(gè)案例,進(jìn)一步了解下模板模式;創(chuàng)建三個(gè)類:Cake(蛋糕)、StrawberryCake(草莓蛋糕)、CherryCake(櫻桃蛋糕)。最后創(chuàng)建一個(gè)Client類,實(shí)現(xiàn)這個(gè)制作蛋糕的調(diào)用過(guò)程。
package com.wsrf.template;
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 16:12
* @description:抽象類:蛋糕
*/
public abstract class Cake {
/**
* 制作
*/
public void make() {
System.out.println("開(kāi)始準(zhǔn)備材料。");
bread();
cream();
fruit();
System.out.println("經(jīng)過(guò)一系列的操作。");
System.out.println("制作完成。");
}
/**
* 準(zhǔn)備面包
*/
public void bread() {
System.out.println("準(zhǔn)備材料:面包");
}
/**
* 準(zhǔn)備奶油
*/
public void cream() {
System.out.println("準(zhǔn)備材料:奶油");
}
/**
* 準(zhǔn)備水果
*/
protected abstract void fruit();
}
package com.wsrf.template;
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 16:13
* @description:具體類:草莓蛋糕
*/
public class StrawberryCake extends Cake{
@Override
protected void fruit() {
System.out.println("準(zhǔn)備材料:草莓");
}
}
package com.wsrf.template;
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 16:14
* @description:具體類:櫻桃蛋糕
*/
public class CherryCake extends Cake{
@Override
protected void fruit() {
System.out.println("準(zhǔn)備材料:櫻桃");
}
}
package com.wsrf.template;
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 16:21
* @description
*/
public class Client {
public static void main(String[] args) {
Cake c1 = new CherryCake();
c1.make();
System.out.println("-------------------------------------");
Cake c2 = new StrawberryCake();
c2.make();
}
}
/**
輸出結(jié)果:
開(kāi)始準(zhǔn)備材料。
準(zhǔn)備材料:面包
準(zhǔn)備材料:奶油
準(zhǔn)備材料:櫻桃
經(jīng)過(guò)一系列的操作。
制作完成。
-------------------------------------
開(kāi)始準(zhǔn)備材料。
準(zhǔn)備材料:面包
準(zhǔn)備材料:奶油
準(zhǔn)備材料:草莓
經(jīng)過(guò)一系列的操作。
制作完成。
*/
在Cake類中定義了制作蛋糕的整個(gè)步驟,也就是make方法;然后抽取了公用的方法,bread方法和cream方法;最后定義一個(gè)抽象方法fruit,這個(gè)方法需要交給具體的子類StrawberryCake和CherryCake去實(shí)現(xiàn),從而定制差異化的“蛋糕”。
三、運(yùn)用場(chǎng)景
通過(guò)上面的“蛋糕”案例,在平時(shí)開(kāi)發(fā)中我們可以具體分析一下業(yè)務(wù)需求,首先在父類中定義需求需要實(shí)現(xiàn)的步驟,然后將可以公用的方法抽取到父類中,將個(gè)性化的方法放到具體的子類中去實(shí)現(xiàn);這樣可以很好的培養(yǎng)“抽象化”的思維模式,這是拉開(kāi)差距的第一步。
最近在開(kāi)發(fā)中,遇到這樣的一個(gè)業(yè)務(wù)場(chǎng)景:需要給不同的管理人員計(jì)算各種不同的津貼,如區(qū)域總監(jiān)有區(qū)域管理津貼、傭金、培養(yǎng)育成津貼等等。通過(guò)分析,每種不同類型的津貼,都是需要金額x比例x系數(shù),比如每種津貼都有不同的計(jì)算方式,系數(shù)也是。所以,大致的想法就是:金額x比例x系數(shù)這個(gè)計(jì)算方式設(shè)置為統(tǒng)一的方法,系數(shù)和比例讓具體的津貼子類去實(shí)現(xiàn)。所以大致的偽代碼如下;
首先,我定義了一個(gè)抽象類AbstractManageAllowanceCalService,用于定義統(tǒng)一的計(jì)算方法,并預(yù)留了獲取比例和獲取系數(shù)的抽象方法。
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 17:12
* @description:津貼計(jì)算父類
*/
@Slf4j
public abstract class AbstractManageAllowanceCalService {
/**
* 計(jì)算津貼
* @param amount
* @return
*/
public BigDecimal calAmount(BigDecimal amount) {
if (Objects.isNull(amount)) {
return BigDecimal.ZERO;
}
BigDecimal ratio = getRatio();
BigDecimal coefficient = getCoefficient();
log.info("金額:{},系數(shù):{},比例:{}", amount, coefficient, ratio);
return amount.multiply(ratio).multiply(coefficient);
}
/**
* 獲取比例
* @return
*/
protected abstract BigDecimal getRatio();
/**
* 獲取系數(shù)
* @return
*/
protected abstract BigDecimal getCoefficient();
}
然后,定義兩個(gè)具體的子類,用于計(jì)算區(qū)域管理津貼和傭金。
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 17:17
* @description:區(qū)域管理津貼計(jì)算
*/
@Service
public class AreaBusinessAllowanceCalService extends AbstractManageAllowanceCalService{
/**
* 區(qū)域管理津貼比例
* @return
*/
@Override
protected BigDecimal getRatio() {
return new BigDecimal(0.5).setScale(1, BigDecimal.ROUND_HALF_UP);
}
/**
* 區(qū)域管理津貼系數(shù)
* @return
*/
@Override
protected BigDecimal getCoefficient() {
return new BigDecimal(0.92).setScale(2, BigDecimal.ROUND_HALF_UP);
}
}
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 17:19
* @description:傭金計(jì)算
*/
@Service
public class SalaryCalService extends AbstractManageAllowanceCalService{
/**
* 傭金比例
* @return
*/
@Override
protected BigDecimal getRatio() {
return new BigDecimal(0.45).setScale(2, BigDecimal.ROUND_HALF_UP);
}
/**
* 傭金系數(shù)
* @return
*/
@Override
protected BigDecimal getCoefficient() {
return new BigDecimal(0.88).setScale(2, BigDecimal.ROUND_HALF_UP);
}
}
最后,定義一個(gè)controller類,用于接口調(diào)用,提供計(jì)算能力;接收兩個(gè)參數(shù),金額和計(jì)算津貼類型。
/**
* @author 往事如風(fēng)
* @version 1.0
* @date 2023/5/4 17:21
* @description
*/
@RestController
@RequestMapping("/cal")
public class CalController implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@PostMapping("/amount")
public Result<BigDecimal> calAmount(BigDecimal amount, String calType) {
AbstractManageAllowanceCalService service = null;
if ("AREA".equals(calType)) {
// 區(qū)域管理津貼
service = (AbstractManageAllowanceCalService) applicationContext.getBean("areaBusinessAllowanceCalService");
} else if ("SALARY".equals(calType)) {
// 傭金
service = (AbstractManageAllowanceCalService) applicationContext.getBean("salaryCalService");
}
if (Objects.nonNull(service)) {
return Result.success(service.calAmount(amount));
}
return Result.fail();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
CalController.applicationContext = applicationContext;
}
}
在這個(gè)controller類中,我通過(guò)分析“類型”這個(gè)參數(shù),來(lái)判斷需要調(diào)用哪個(gè)service去實(shí)現(xiàn)具體的計(jì)算邏輯。這里用了if-else的方式去實(shí)現(xiàn);其實(shí)也可以用到另一個(gè)設(shè)計(jì)模式——策略模式,這樣寫(xiě)出來(lái)的代碼就會(huì)比較優(yōu)雅,這里就不對(duì)策略模式展開(kāi)贅述了。
四、源碼中運(yùn)用
4.1、JDK源碼中的模板模式
在JDK中其實(shí)也有很多地方運(yùn)用到了模板模式,這里咱挑一個(gè)講。并發(fā)包下的AbstractQueuedSynchronizer類,就是一個(gè)抽象類,也就是我們先前的文章中提到過(guò)的AQS。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
}
其中,tryAcquire和tryRelease這兩個(gè)方式直接拋了異常,用protected關(guān)鍵詞修飾,需要由子類去實(shí)現(xiàn)。然后在acquire和release方法中分別去調(diào)用這兩方法。也就是acquire方法定義了一個(gè)統(tǒng)一的結(jié)構(gòu),差異化的tryAcquire方法需要具體的子類去實(shí)現(xiàn)功能,實(shí)現(xiàn)了模板模式。
4.2、Spring源碼中的模板模式
說(shuō)到源碼,Spring是一個(gè)繞不開(kāi)的話題,那就來(lái)學(xué)習(xí)下Spring中的模板模式。其中,有一個(gè)類DefaultBeanDefinitionDocumentReader,它是BeanDefinitionDocumentReader的實(shí)現(xiàn)類,是提取spring配置文件中的bean信息,并轉(zhuǎn)化為BeanDefinition。
public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = this.createDelegate(this.getReaderContext(), root, parent);
//...
this.preProcessXml(root);
this.parseBeanDefinitions(root, this.delegate);
this.postProcessXml(root);
this.delegate = parent;
}
protected void preProcessXml(Element root) {
}
protected void postProcessXml(Element root) {
}
}
這里我截圖了其中的一段代碼,主要是doRegisterBeanDefinitions這個(gè)方法,從跟節(jié)點(diǎn)root出發(fā),root下的每個(gè)bean注冊(cè)定義。
該方法中還調(diào)用了preProcessXml和postProcessXml這兩個(gè)方法,但是在DefaultBeanDefinitionDocumentReader類中,這兩個(gè)方法是未實(shí)現(xiàn)的,需要其子類去實(shí)現(xiàn)具體的邏輯。所以,這里也是一個(gè)很典型的模板模式的運(yùn)用。
五、總結(jié)
模板方法模式其實(shí)是一個(gè)比較簡(jiǎn)單的設(shè)計(jì)模式,它有如下優(yōu)點(diǎn):1、封裝不變的邏輯,擴(kuò)展差異化的邏輯;2、抽取公共代碼,提高代碼的復(fù)用性;3、父類控制行為,子類實(shí)現(xiàn)細(xì)節(jié)。
其缺點(diǎn)就是不同的實(shí)現(xiàn)都需要一個(gè)子類去維護(hù),會(huì)導(dǎo)致子類的個(gè)數(shù)不斷增加,造成系統(tǒng)更加龐大。
用一句話總結(jié):將公用的方法抽取到父類,在父類中預(yù)留可變的方法,最后子類去實(shí)現(xiàn)可變的方法。
模板模式更多的是考察我們對(duì)于公用方法的提??;對(duì)于編程也是這樣,更多的是一種思維能力,不能只局限于代碼,要把格局打開(kāi)。
六、參考源碼
編程文檔:
https://gitee.com/cicadasmile/butte-java-note
應(yīng)用倉(cāng)庫(kù):
https://gitee.com/cicadasmile/butte-flyer-parent