一、概念
1、概念
模板方法模式是一種基於繼承的程式碼複用技術,它是一種類行為型模式
。
它定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。
2、舉例理解
網上舉了一個請客吃飯的例子,我覺得解釋的挺好的。我們每個人去請客吃飯。一般都含三個步驟:點單、吃東西、買單,而且順序就是從做左到右的。
在這三個步驟中,點單和買單大同小異,最大的區別在於第二步——吃什麼?吃麵條和吃滿漢全席可大不相同,如圖1所示
所以從開發角度來分析,有時也會遇到類似的情況,某個方法的實現需要多個步驟(類似“ 請客 ”),其中有些步驟是固定的(類似“點單”和“買單”),而有些步驟並不固定,存在
可變性(類似“吃東西”)。
為了提高程式碼的複用性和系統的靈活性,我們把"點單"和"買單"的實現放在父類中實現,而對於"吃東西",因為差異性就很大所以在父類中只做一個宣告,將其具體實現放在不同的
子類中,在一個子類中提供“吃麵條”的實現,而另一個子類提供“吃滿漢全席”的實現。通過使用模板方法模式,一方面提高了程式碼的複用性,另一方面還可以利用物件導向的多型性,
在執行時選擇一種具體子類,實現完整的“請客”方法,提高系統的靈活性和可擴充套件性。
3、結構和說明
AbstractClass:抽象類。用來定義演算法骨架和原語操作,在這個類裡面,還可以提供演算法中通用的實現
ConcreteClass:具體實現類。用來實現演算法骨架中的某些步驟,完成跟特定子類相關的功能。
public abstract class AbstractClass {
/**
* 1、點餐 直接用final修飾,代表子類不能在重寫
*/
private final void order(){
//點餐
}
/**
* 2、吃東西 吃什麼由子類實現
*/
public abstract void eatSomething();
/**
* 3、結算
*/
private final void settlement(){
//結算
}
/**
* 記錄一次請客
*/
protected final void workOneDay()
{
//點單
order();
//吃東西
eatSomething();
//結單
settlement();
}
/**
* 是否需要上廁所,這個其實可以理解成一個鉤子,意思就是子類可以選擇是否重寫,不重寫就用父類的方法。
* 在有些時候 可以通過重寫修改boolean的返回值,可以調協一些流程。
*/
protected boolean isNeedWc()
{
return false;
}
}
子類這裡就不寫了。
4、優缺點
優點
- 封裝不變,擴充套件可變:父類封裝了具體流程以及實現部分不變行為,其它可變行為交由子類進行具體實現;
- 流程由父類控制,子類進行實現:框架流程由父類限定,子類無法更改;子類可以針對流程某些步驟進行具體實現;
缺點
抽象規定了行為,具體負責實現,與通常事物的行為相反,會帶來理解上的困難(通俗地說,“父類呼叫了子類方法”);
5、應用場景
- 多個子類有公有的方法,並且邏輯基本相同時;
- 重要,複雜的演算法,可以把核心演算法設計為模板方法,周邊的相關細節功能則由各個子類實現;
- 重構時,模板方法模式是一個經常使用的模式,把相同的程式碼抽取到父類,然後通過鉤子函式約束其行為;
二、模版方法模式實戰示例
之前在參與一個app後段開發,裡面有個資訊模組。這個模版的內容不是我們工作人員進行編輯,而是通過爬蟲去獲取各大網站的資訊,然後處理好後存入我們的資料庫。
之前大概爬了十幾個網站的資訊。這裡的業務流程是這樣的 1、爬取資訊內容->2、校驗是否已經抓取過->3、儲存內容。
這裡只有第一步是需要子類去實現的,因為每個網站的資料格式都是不一樣的,所以我們需要子類爬取後,統一處理成我們的格式。那麼第二步第三步是可以通過父類完成的。
這裡程式碼如下。
1、抽象類
抽象類
/**
* 定義一個爬取網站資訊的模版
*/
@Slf4j
public abstract class AbstractCrawlNewsService {
/**
* 1、爬取各網站訊息 由子類去實現
*/
protected abstract List<Object> crawlPage(int pageNum) throws IOException;
/**
* 2、校驗該資訊是否已經爬取過 因為這個邏輯是一樣的 由父類實現就可以了
*/
protected final Map<String, Boolean> isCrawled(List<Object> checkParams){
//資料庫校驗
return new HashMap<>();
}
/**
* 3、儲存資訊 同樣由父類實現即可
*/
protected final void saveArticle(Object object){
//儲存資料庫
}
/**
* 模版方法 定義了上面的一整套流程
*/
protected void doTask(String url) {
int pageNum = 1;
while (true) {
//1、爬取網站資料,因為網站不可能一次性獲取所以資訊資料的 所以進行分頁查詢 預設第一頁開始
List<Object> newsList = crawlPage(pageNum++);
// 抓取不到新的內容本次抓取結束
if (CollectionUtils.isEmpty(newsList)) {
break;
}
//2、校驗是否已經抓取過濾(查詢我們自己資料庫)
Map<String, Boolean> crawledMap = isCrawled(newsList);
//3、將資料儲存資料庫
for (int i = newsList.size() - 1; i >= 0; i--) {
Object object = newsList.get(i);
// 沒有爬取過,才進行爬取
if (!crawledMap.getOrDefault(object.getTitle(), false)) {
saveArticle(object);
}
}
//可以考慮請求後休眠以下 因為太頻繁IP容易被封。
ThreadUtils.sleep(2);
}
}
}
2、具體實現類
介面
/**
* @Description: 爬取獲悉網介面
*/
public interface CrawlerHuoXingService {
void start();
}
實現類
/**
* 抓取火星網新聞
*/
@Slf4j
@Service
public class CrawlerHuoXingServiceImpl extends AbstractCrawlNewsService
implements CrawlerHuoXingService {
/**
* 爬取介面
*/
@Override
protected List<Object> crawlPage(int pageNum) throws IOException {
//通過url獲取指定網站介面 進行爬取
return new ArrayList<>();
}
@Override
public void start() {
try {
doTask("http://www.huoxing24.com/news");
} catch (IOException e) {
log.error("抓取火星網新聞異常", e);
}
}
}
至於什麼時候去爬取資訊,我們可以通過定時器,定時去爬取。
定時任務類
/**
* @Description: 定時爬取火星網資訊
*/
@Slf4j
@Component
public class ScheduleHuoXingTrigger {
@Autowired
private CrawlerHuoXingService crawlerHuoXingService;
/**
* 定時抓取火星資訊
*/
@Scheduled(initialDelay = 1000, fixedDelay = 15 * 60 * 1000)
public void doCrawlHuoXing() {
try {
crawlerHuoXingService.start();
} catch (Exception e) {
log.error("本次抓取火星資訊異常", e);
}
}
}
整個大致流程就是這樣,以後要是新增一條資訊那就只要寫多個具體實現類就行。