開發軟體時一個常見的情況是有一個通用的算法,隻是步驟上略有不同。我們希望不同的 實作能夠遵守通用模式,保證它們使用了同一個算法,也是為了讓代碼更加易讀。一旦你 從整體上了解了算法,就能更容易了解其各種實作。
模闆方法模式是為這些情況設計的:整體算法的設計是一個
抽象類,它有一系列抽象方法,代表算法中可被定制的步驟,同時這個類中包含了一些通用代碼。算法的每一個變種 由具體的類實作,它們重寫了抽象方法,提供了相應的實作。
讓我們假想一個情境來搞明白這是怎麼回事。假設我們是一家銀行,需要對公衆、公司和 職員放貸。放貸程式大體一緻即驗明身份、信用記錄和收入記錄。這些資訊來源不一, 衡量标準也不一樣。你可以檢視一個家庭的賬單來核對個人身份;公司都在官方機構注冊 過,比如美國的SEC、英國的Companies House。
我們先使用一個抽象類LoanApplication 來控制算法結構,該類包含一些貸款調查結果 報告的通用代碼。根據不同的申請人,有不同的類:CompanyLoanApplication、Personal LoanApplication 和EmployeeLoanApplication。圖1 展示了LoanApplication 類的結構。

圖1 使用模闆方法模式描述申請貸款過程
CompanyLoanApplication 的checkIdentity 方法在Companies House 等注冊公司資料庫中查找相關資訊。checkIncomeHistory 方法評估公司的現有利潤、損益表和資産負債表。checkCreditHistory 方法則檢視現有的壞賬和未償債務。
PersonalLoanApplication 的checkIdentity 方法通過分析客戶提供的紙本結算單,确認客戶位址是否真實有效。checkIncomeHistory 方法通過檢查工資條判斷客戶是否仍被雇傭。checkCreditHistory 方法則會将工作交給外部的信用卡支付提供商。
EmployeeLoanApplication 就是沒有查閱員工曆史功能的PersonalLoanApplication。為了友善起見,我們的銀行在雇傭員工時會查閱所有員工的收入記錄圖 2。
圖2 員工申請貸款是個人申請的一種特殊情況
使用Lambda 表達式和方法引用,我們能換個角度思考模闆方法模式,實作方式也跟以前不一樣。模闆方法模式真正要做的是将一組方法調用按一定順序組織起來。如果用函數接口表示函數,用Lambda 表達式或者方法引用實作這些接口,相比使用繼承建構算法,就會得到極大的靈活性。讓我們看看如何使用這種方式實作LoanApplication 算法,請看圖3
圖3 員工申請貸款的例子
正如讀者所見, 這裡沒有使用一系列的抽象方法, 而是多出一些屬性:identity、creditHistory 和incomeHistory。每一個屬性都實作了函數接口Criteria,該接口檢查一項标準,如果不達标就抛出一個問題域裡的異常。我們也可以選擇從check 方法傳回一個類來表示成功或失敗,但是沿用異常更加符合先前的實作圖4。
圖4 如果申請失敗,函數接口Criteria 抛出異常
采用這種方式,而不是基于繼承的模式的好處是不需要在LoanApplication 及其子類中實作算法,配置設定功能時有了更大的靈活性。比如,我們想讓Company 類負責所有的檢查,那麼Company 類就會多出一系列方法,如圖5。
圖5 Company 類中的檢查方法
現在隻需為CompanyLoanApplication 類傳入對應的方法引用,如圖6。
圖6 CompanyLoanApplication 類聲明了對應的檢查方法
将行為配置設定給Company 類的原因是各個國家之間确認公司資訊的方式不同。在英國,Companies House 規範了注冊公司資訊的位址,但在美國,各個州的政策是不一樣的。
使用函數接口實作檢查方法并沒有排除繼承的方式。我們可以顯式地在這些類中使用Lambda 表達式或者方法引用。
我們也不需要強制EmployeeLoanApplication 繼承PersonalLoanApplication 來達到複用,可以對同一個方法傳遞引用。它們之間是否天然存在繼承關系取決于員工的借貸是否是普通人借貸這種特殊情況,或者是另外一種不同類型的借貸。是以,使用這種方式能讓我們更加緊密地為問題模組化。