統一抽取,制定規範;
一、概述
模板方法模式,又叫模板模式,屬於23種設計模式中的行為型模式。在抽象類中公開定義了執行的方法,子類可以按需重寫其方法,但是要以抽象類中定義的方式呼叫方法。總結起來就是:定義一個操作的演算法結構,而將一些步驟延遲到子類中。在不改變演算法結構的情況下,子類能重定義該演算法的特定步驟。
下面是模板模式的UML圖,抽象類(AbstractClass)定義公共的步驟和方法,依次呼叫實際的模板方法,當然每個方法可以是抽象方法(需交給子類實現),也可以是提供預設的方法。具體的類(ConcreteClass)可以重寫所有的方法,但是不能改變抽象類中定義的整體結構。
二、入門案例
相信大家都吃過蛋糕,現在市面上的蛋糕可謂是五花八門,你能想到的造型商家能給你整出來,你想不到的,他們也能整出來。不過無論造型如何變化,不變的有兩種東西:“奶油”和“麵包”。其餘的材料隨意搭配,就湊成了各式各樣的蛋糕。
基於這個場景,我們來寫一個案例,進一步瞭解下模板模式;建立三個類:Cake
(蛋糕)、StrawberryCake
(草莓蛋糕)、CherryCake
(櫻桃蛋糕)。最後建立一個Client
類,實現這個製作蛋糕的呼叫過程。
package com.wsrf.template;
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 16:12
* @description:抽象類:蛋糕
*/
public abstract class Cake {
/**
* 製作
*/
public void make() {
System.out.println("開始準備材料。");
bread();
cream();
fruit();
System.out.println("經過一系列的操作。");
System.out.println("製作完成。");
}
/**
* 準備麵包
*/
public void bread() {
System.out.println("準備材料:麵包");
}
/**
* 準備奶油
*/
public void cream() {
System.out.println("準備材料:奶油");
}
/**
* 準備水果
*/
protected abstract void fruit();
}
package com.wsrf.template;
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 16:13
* @description:具體類:草莓蛋糕
*/
public class StrawberryCake extends Cake{
@Override
protected void fruit() {
System.out.println("準備材料:草莓");
}
}
package com.wsrf.template;
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 16:14
* @description:具體類:櫻桃蛋糕
*/
public class CherryCake extends Cake{
@Override
protected void fruit() {
System.out.println("準備材料:櫻桃");
}
}
package com.wsrf.template;
/**
* @author 往事如風
* @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();
}
}
/**
輸出結果:
開始準備材料。
準備材料:麵包
準備材料:奶油
準備材料:櫻桃
經過一系列的操作。
製作完成。
-------------------------------------
開始準備材料。
準備材料:麵包
準備材料:奶油
準備材料:草莓
經過一系列的操作。
製作完成。
*/
在Cake
類中定義了製作蛋糕的整個步驟,也就是make方法;然後抽取了公用的方法,bread方法和cream方法;最後定義一個抽象方法fruit,這個方法需要交給具體的子類StrawberryCake
和CherryCake
去實現,從而定製差異化的“蛋糕”。
三、運用場景
透過上面的“蛋糕”案例,在平時開發中我們可以具體分析一下業務需求,首先在父類中定義需求需要實現的步驟,然後將可以公用的方法抽取到父類中,將個性化的方法放到具體的子類中去實現;這樣可以很好的培養“抽象化”的思維模式,這是拉開差距的第一步。
最近在開發中,遇到這樣的一個業務場景:需要給不同的管理人員計算各種不同的津貼,如區域總監有區域管理津貼、佣金、培養育成津貼等等。透過分析,每種不用型別的津貼,都是需要金額x比例x係數,比例每種津貼都有不同的計算方式,係數也是。所以,大致的想法就是:金額x比例x係數這個計算方式設定為統一的方法,係數和比例讓具體的津貼子類去實現。所以大致的虛擬碼如下;
首先,我定義了一個抽象類AbstractManageAllowanceCalService
,用於定義統一的計算方法,並預留了獲取比例和獲取係數的抽象方法。
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 17:12
* @description:津貼計算父類
*/
@Slf4j
public abstract class AbstractManageAllowanceCalService {
/**
* 計算津貼
* @param amount
* @return
*/
public BigDecimal calAmount(BigDecimal amount) {
if (Objects.isNull(amount)) {
return BigDecimal.ZERO;
}
BigDecimal ratio = getRatio();
BigDecimal coefficient = getCoefficient();
log.info("金額:{},係數:{},比例:{}", amount, coefficient, ratio);
return amount.multiply(ratio).multiply(coefficient);
}
/**
* 獲取比例
* @return
*/
protected abstract BigDecimal getRatio();
/**
* 獲取係數
* @return
*/
protected abstract BigDecimal getCoefficient();
}
然後,定義兩個具體的子類,用於計算區域管理津貼和佣金。
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 17:17
* @description:區域管理津貼計算
*/
@Service
public class AreaBusinessAllowanceCalService extends AbstractManageAllowanceCalService{
/**
* 區域管理津貼比例
* @return
*/
@Override
protected BigDecimal getRatio() {
return new BigDecimal(0.5).setScale(1, BigDecimal.ROUND_HALF_UP);
}
/**
* 區域管理津貼係數
* @return
*/
@Override
protected BigDecimal getCoefficient() {
return new BigDecimal(0.92).setScale(2, BigDecimal.ROUND_HALF_UP);
}
}
/**
* @author 往事如風
* @version 1.0
* @date 2023/5/4 17:19
* @description:佣金計算
*/
@Service
public class SalaryCalService extends AbstractManageAllowanceCalService{
/**
* 佣金比例
* @return
*/
@Override
protected BigDecimal getRatio() {
return new BigDecimal(0.45).setScale(2, BigDecimal.ROUND_HALF_UP);
}
/**
* 佣金係數
* @return
*/
@Override
protected BigDecimal getCoefficient() {
return new BigDecimal(0.88).setScale(2, BigDecimal.ROUND_HALF_UP);
}
}
最後,定義一個controller類,用於介面呼叫,提供計算能力;接收兩個引數,金額和計算津貼型別。
/**
* @author 往事如風
* @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)) {
// 區域管理津貼
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;
}
}
在這個controller類中,我透過分析“型別”這個引數,來判斷需要呼叫哪個service去實現具體的計算邏輯。這裡用了if-else的方式去實現;其實也可以用到另一個設計模式——策略模式,這樣寫出來的程式碼就會比較優雅,這裡就不對策略模式展開贅述了。
四、原始碼中運用
4.1、JDK原始碼中的模板模式
在JDK中其實也有很多地方運用到了模版模式,這裡我們挑一個講。併發包下的AbstractQueuedSynchronizer
類,就是一個抽象類,也就是我們先前的文章中提到過的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這兩個方式直接拋了異常,用protected關鍵詞修飾,需要由子類去實現。然後再acquire和release方法中分別去呼叫這兩方法。也就是acquire方法定義了一個統一的結構,差異化的tryAcquire方法需要具體的子類去實現功能,實現了模版模式。
4.2、Spring原始碼中的模板模式
說到原始碼,Spring是一個繞不開的話題,那就來學習下Spring中的模版模式。其中,有一個類DefaultBeanDefinitionDocumentReader
,它是BeanDefinitionDocumentReader
的實現類,是提取spring配置檔案中的bean資訊,並轉化為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這個方法,從跟節點root出發,root下的每個bean註冊定義。
該方法中還呼叫了preProcessXml和postProcessXml這兩個方法,但是在DefaultBeanDefinitionDocumentReader
類中,這兩個方法是未實現的,需要其子類去實現具體的邏輯。所以,這裡也是一個很典型的模板模式的運用。
五、總結
模板方法模式其實是一個比較簡單的設計模式,它有如下優點:1、封裝不變的邏輯,擴充套件差異化的邏輯;2、抽取公共程式碼,提高程式碼的複用性;3、父類控制行為,子類實現細節。
其缺點就是不同的實現都需要一個子類去維護,會導致子類的個數不斷增加,造成系統更加龐大。
用一句話總結:將公用的方法抽取到父類,在父類中預留可變的方法,最後子類去實現可變的方法。
模板模式更多的是考察我們對於公用方法的提取;對於程式設計也是這樣,更多的是一種思維能力,不能只侷限於程式碼,要把格局開啟。
六、參考原始碼
程式設計文件:
https://gitee.com/cicadasmile/butte-java-note
應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent