設計模式之模板方法模式

2023-05-30 06:00:29

一、簡介

模板方法模式是一種行為型設計模式,它定義一個操作(模板方法)的基本組合與控制流程,將一些步驟(抽象方法)推遲到子類中,在使用時呼叫不同的子類,就可以達到不改變一個操作的基本流程情況下,即可修改其中的某些特定步驟。這種設計方式將特定步驟的具體實現與操作流程分離開來,實現了程式碼的複用和擴充套件,從而提高程式碼質量和可維護性。

模板方法模式包含以下:

  • 抽象類:負責定義模板方法、基本方法、抽象方法。
  • 模板方法:在抽象類中定義的流程操作集合,裡面有一系列流程操作和條件控制,包含基本方法和抽象方法。
  • 基本方法:在抽象類中已經實現了的方法。
  • 抽象方法:在抽象類中還沒有實現的方法。
  • 具體子類:實現抽象類中所定義的抽象方法,也就是實現特定步驟。

模板方法模式的優點:

  1. 封裝不變部分,擴充套件可變部分。模板方法模式將可變的部分封裝在抽象方法中,不變的部分封裝在基本方法中。這使得子類可以根據需求對可變部分進行擴充套件,而不變部分仍然保持不變。
  2. 避免重複程式碼,抽象類中包含的基本方法可以避免子類重複實現相同的程式碼邏輯。
  3. 更好的擴充套件性,由於具體實現由子類來完成,因此可以方便地擴充套件新的功能或變更實現方式,同時不影響模板方法本身。

模板方法模式的缺點:

  1. 類多,由於每個演演算法都需要一個抽象類和具體子類來實現,因此在操作流程比較多時可能導致類的數量急劇增加,從而導致程式碼的複雜性提高。
  2. 關聯性高,模板方法與子類實現的抽象方法緊密相關,如果該模板方法需要修改,可能會涉及到多個子類的修改。

簡單列一些模板方法模式的應用場景:

  1. 開發框架,通常框架會定義一些通用的模板,子類可以根據自身的特定需求來細化模板的實現細節,比如 Spring 中的 JdbcTemplate、RestTemplate、RabbitTemplate、KafkaTemplate 等。
  2. 業務邏輯,我們可以針對業務流程做一些拆解,將特定步驟改為子類實現。比如傳送驗證碼的流程,在傳送驗證碼時需要選擇不同廠商來傳送驗證碼,但是我們傳送的驗證碼前的檢查、驗證碼生成、儲存驗證碼邏輯都是一樣的。

二、Java中實現模板方法模式

如上,我們用一個簡單的傳送簡訊程式碼來做模板方法模式的範例:

定義一個傳送簡訊模板

/**
 * 傳送簡訊模板
 */
public abstract class SmsTemplate {

    /**
     * 傳送方法
     *
     * @param mobile 手機號
     */
    public void send(String mobile) throws Exception {
        System.out.println("檢查使用者一分鐘內是否傳送過簡訊,
                    mobile:" + mobile);
        if (checkUserReceiveInOneMinute(mobile)) {
            throw new Exception("請等待1分鐘後重試");
        }
        String code = genCode();
        if (manufacturer(mobile, code)) {
            System.out.println("簡訊廠商傳送簡訊成功,
                    mobile:" + mobile + ",code=" + code);
            save2redis(mobile, code);
        }
    }

    /**
     * 模板方法,由不同的廠商來實現傳送簡訊到手機上
     * @return
     */
    abstract boolean manufacturer(String mobile, String code);

    /**
     * 檢查1分鐘內該手機號是否接收過驗證碼,1分鐘內接收過就不能在傳送驗證碼
     * @param mobile
     * @return
     */
    public boolean checkUserReceiveInOneMinute(String mobile) {
        return ...;
    }


    /**
     * 生成6位驗證碼
     * @return
     */
    public String genCode() {
        return "123456";
    }

    /**
     * 將手機號+驗證碼存進redis中,給登入介面做校驗用
     * @param mobile
     * @param code
     */
    public void save2redis(String mobile, String code) {
        ...
    }
}

新增兩個不同廠商實現的子類

/**
 * 阿里雲簡訊傳送
 */
public class AliyunSmsSend extends SmsTemplate{
    @Override
    boolean manufacturer(String mobile, String code) {
        System.out.println("讀取阿里雲簡訊設定");
        System.out.println("建立阿里雲傳送簡訊使用者端");
        System.out.println("阿里雲傳送簡訊成功");
        return true;
    }
}

/**
 * 騰訊雲簡訊傳送
 */
public class TencentSmsSend extends SmsTemplate {
    @Override
    boolean manufacturer(String mobile, String code) {
        System.out.println("讀取騰訊雲簡訊設定");
        System.out.println("建立騰訊雲傳送簡訊使用者端");
        System.out.println("騰訊雲傳送簡訊成功");
        return true;
    }
}

在 Java 程式中進行呼叫

public class Main {
    public static void main(String[] args) throws Exception {
        SmsTemplate smsTemplate1 = new AliyunSmsSend();
        smsTemplate1.send("13333333333");
        System.out.println("---------------------------");
        SmsTemplate smsTemplate2 = new TencentSmsSend();
        smsTemplate2.send("13333333333");
    }
}

輸出如下:

檢查使用者一分鐘內是否傳送過簡訊,mobile:13333333333
讀取阿里雲簡訊設定
建立阿里雲傳送簡訊使用者端
阿里雲傳送簡訊成功
簡訊廠商傳送簡訊成功,mobile:13333333333,code=123456
---------------------------
檢查使用者一分鐘內是否傳送過簡訊,mobile:13333333333
讀取騰訊雲簡訊設定
建立騰訊雲傳送簡訊使用者端
騰訊雲傳送簡訊成功
簡訊廠商傳送簡訊成功,mobile:13333333333,code=123456

我們來看看模板方法模式的組成:

  • 抽象類 SmsTemplate 中定義了傳送簡訊的基本流程操作
    1. 傳送前檢查使用者1分鐘內是否接收過簡訊,不變部分。
    2. 生成驗證碼,不變部分。
    3. 發遠驗證碼到使用者手機,這個抽象方法由不同子類實現,可變部分
    4. 傳送成功則儲存到 redis 中,不變部分。
  • 具體子類 AliyunSmsSend、TencentSmsSend 繼承抽象類,實現抽象方法 manufacturer(String mobile, String code),定義流程中的可變部分。
  • 呼叫模板方法 send(mobile) ,在模板方法中完成了基本流程組合與條件控制。

三、Spring 實現模板方法模式

Spring 中實現模板方法模式,是非常簡單的,我們只需要對上述的 Java 程式碼範例的 AliyunSmsSend 類稍作改造,加上 @Component 註解就行,

/**
 * 阿里雲簡訊傳送
 */
@Component
public class AliyunSmsSend extends SmsTemplate{
    @Override
    boolean manufacturer(String mobile, String code) {
        IUserService userService = SpringUtil.getBean(IUserService.class);
        System.out.println("讀取阿里雲簡訊設定");
        System.out.println("建立阿里雲傳送簡訊使用者端");
        System.out.println("阿里雲傳送簡訊成功");
        return true;
    }
}

如果在 AliyunSmsSend 類中需要注入其他 bean,通過 cn.hutool.extra.spring.SpringUtil.getBean(...) 方法獲取對應 bean 就行。

四、使用Java8中Lambda表示式

在Java8 中,還可以使用函數表示式來替換抽象方法,程式碼如下,

/**
 * 傳送簡訊模板
 */
public class SmsTemplateLambda {
    /**
     * 傳送簡訊
     * @param mobile 手機號
     * @param biFunction
     * @throws Exception
     */
    public void send(String mobile,
        BiFunction<String, String, Boolean> biFunction) throws Exception {
        System.out.println("檢查使用者一分鐘內是否傳送過簡訊,mobile:" + mobile);
        if (checkUserReceiveInOneMinute(mobile)) {
            throw new Exception("請等待1分鐘後重試");
        }
        String code = genCode();
        if (biFunction.apply(mobile, code)) {
            System.out.println("簡訊廠商傳送簡訊成功,mobile:" 
                + mobile + ",code=" + code);
            save2redis(mobile, code);
        }
    }
    ...
}

通過 BiFunction 函數,將不同廠商傳送簡訊到使用者手機的程式碼在 send(mobile) 方法中分離處理。


呼叫方法如下:

    public static void main(String[] args) throws Exception {
        SmsTemplateLambda smsTemplateLambda = new SmsTemplateLambda();
        smsTemplateLambda.send("1333333333", (s, s2) -> {
            System.out.println("讀取阿里雲簡訊設定");
            System.out.println("建立阿里雲傳送簡訊使用者端");
            System.out.println("阿里雲傳送簡訊成功");
            return true;
        });

        smsTemplateLambda.send("1333333333", (s, s2) -> {
            System.out.println("讀取騰訊雲簡訊設定");
            System.out.println("建立騰訊雲傳送簡訊使用者端");
            System.out.println("騰訊雲傳送簡訊成功");
            return true;
        });
    }

可以看到,我們可以只在呼叫 SmsTemplateLambda 類的 send(mobile) 方法時,才實現不同廠商傳送簡訊到手機的具體邏輯。好處就是每增加一個模板方法時,不用增加具體的子類實現,減少類的建立與降低子類的實現成本。

總結

模板方法模式通過定義一個流程基本操作也就是模板方法,將具體的實現步驟推遲到子類中,使得子類可以靈活地實現可變的行為,這是模板方法模式的核心思想與價值所在。

關注公眾號【waynblog】每週分享技術乾貨、開源專案、實戰經驗、高效開發工具等,您的關注將是我的更新動力!