【java設計模式】(10)—模版方法模式(案例解析)
- 2021 年 11 月 1 日
- 筆記
- 【Java】-- 設計模式
一、概念
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("//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);
}
}
}
整個大致流程就是這樣,以後要是添加一條資訊那就只要寫多個具體實現類就行。