<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          模板方法模式——看看 JDK 和 Spring 是如何優(yōu)雅復(fù)用代碼的

          共 4254字,需瀏覽 9分鐘

           ·

          2020-09-11 08:35

          點(diǎn)擊藍(lán)色“JavaKeeper”關(guān)注我喲
          加個(gè)“星標(biāo)”,一起成長,做牛逼閃閃的技術(shù)人

          Keeper導(dǎo)讀:不管是我們學(xué)習(xí)并發(fā)編程中的 AQS,還是看 Spring 的源碼,肯定都會(huì)遇到模板方法模式,它簡直太常見了。

          前言

          模板,顧名思義,它是一個(gè)固定化、標(biāo)準(zhǔn)化的東西。

          模板方法模式是一種行為設(shè)計(jì)模式, 它在超類中定義了一個(gè)算法的框架, 允許子類在不修改結(jié)構(gòu)的情況下重寫算法的特定步驟。

          場景問題

          程序員不愿多扯,上來先干兩行代碼

          網(wǎng)上模板方法的場景示例特別多,個(gè)人感覺還是《Head First 設(shè)計(jì)模式》中的例子比較好。

          假設(shè)我們是一家飲品店的師傅,起碼需要以下兩個(gè)手藝

          真簡單哈,這么看,步驟大同小異,我的第一反應(yīng)就是寫個(gè)業(yè)務(wù)接口,不同的飲品實(shí)現(xiàn)其中的方法就行,像這樣

          畫完類圖,猛地發(fā)現(xiàn),第一步和第三步?jīng)]什么差別,而且做飲品是個(gè)流程式的工作,我希望使用時(shí),直接調(diào)用一個(gè)方法,就去執(zhí)行對應(yīng)的制作步驟。

          靈機(jī)一動(dòng),不用接口了,用一個(gè)抽象父類,把步驟方法放在一個(gè)大的流程方法 makingDrinks() 中,且第一步和第三步,完全一樣,沒必要在子類實(shí)現(xiàn),改進(jìn)如下

          再看下我們的設(shè)計(jì),感覺還不錯(cuò),現(xiàn)在用同一個(gè) makingDrinks() 方法來處理咖啡和茶的制作,而且我們不希望子類覆蓋這個(gè)方法,所以可以申明為 final,不同的制作步驟,我們希望子類來提供,必須在父類申明為抽象方法,而第一步和第三步我們不希望子類重寫,所以我們聲明為非抽象方法

          public?abstract?class?Drinks?{

          ????void?boilWater()?{
          ????????System.out.println("將水煮沸");
          ????}

          ????abstract?void?brew();

          ????void?pourInCup()?{
          ????????System.out.println("倒入杯子");
          ????}

          ????abstract?void?addCondiments();
          ????
          ????public?final?void?makingDrinks()?{
          ????????//熱水
          ????????boilWater();
          ????????//沖泡
          ????????brew();
          ????????//倒進(jìn)杯子
          ????????pourInCup();
          ????????//加料
          ????????addCondiments();
          ????}
          }

          接著,我們分別處理咖啡和茶,這兩個(gè)類只需要繼承父類,重寫其中的抽象方法即可(實(shí)現(xiàn)各自的沖泡和添加調(diào)料)

          public?class?Tea?extends?Drinks?{
          ????@Override
          ????void?brew()?{
          ????????System.out.println("沖茶葉");
          ????}
          ????@Override
          ????void?addCondiments()?{
          ????????System.out.println("加檸檬片");
          ????}
          }
          public?class?Coffee?extends?Drinks?{
          ????@Override
          ????void?brew()?{
          ????????System.out.println("沖咖啡粉");
          ????}

          ????@Override
          ????void?addCondiments()?{
          ????????System.out.println("加奶加糖");
          ????}
          }

          現(xiàn)在可以上崗了,試著制作下咖啡和茶吧

          public?static?void?main(String[]?args)?{
          ????Drinks?coffee?=?new?Coffee();
          ????coffee.makingDrinks();
          ????System.out.println();
          ????Drinks?tea?=?new?Tea();
          ????tea.makingDrinks();
          }

          好嘞,又學(xué)會(huì)一個(gè)設(shè)計(jì)模式,這就是模板方法模式,我們的 makingDrinks() 就是模板方法。我們可以看到相同的步驟 boilWater()pourInCup() 只在父類中進(jìn)行即可,不同的步驟放在子類實(shí)現(xiàn)。

          認(rèn)識(shí)模板方法

          在閻宏博士的《JAVA與模式》一書中開頭是這樣描述模板方法(Template Method)模式的:

          模板方法模式是類的行為模式。準(zhǔn)備一個(gè)抽象類,將部分邏輯以具體方法以及具體構(gòu)造函數(shù)的形式實(shí)現(xiàn),然后聲明一些抽象方法來迫使子類實(shí)現(xiàn)剩余的邏輯。不同的子類可以以不同的方式實(shí)現(xiàn)這些抽象方法,從而對剩余的邏輯有不同的實(shí)現(xiàn)。這就是模板方法模式的用意。

          寫代碼的一個(gè)很重要的思考點(diǎn)就是“變與不變”,程序中哪些功能是可變的,哪些功能是不變的,我們可以把不變的部分抽象出來,進(jìn)行公共的實(shí)現(xiàn),把變化的部分分離出來,用接口來封裝隔離,或用抽象類約束子類行為。模板方法就很好的體現(xiàn)了這一點(diǎn)。

          模板方法定義了一個(gè)算法的步驟,并允許子類為一個(gè)或多個(gè)步驟提供實(shí)現(xiàn)。

          模板方法模式是所有模式中最為常見的幾個(gè)模式之一,是基于繼承的代碼復(fù)用的基本技術(shù),我們再看下類圖

          模板方法模式就是用來創(chuàng)建一個(gè)算法的模板,這個(gè)模板就是方法,該方法將算法定義成一組步驟,其中的任意步驟都可能是抽象的,由子類負(fù)責(zé)實(shí)現(xiàn)。這樣可以確保算法的結(jié)構(gòu)保持不變,同時(shí)由子類提供部分實(shí)現(xiàn)。

          再回顧下我們制作咖啡和茶的例子,有些顧客要不希望咖啡加糖或者不希望茶里加檸檬,我們要改造下模板方法,在加相應(yīng)的調(diào)料之前,問下顧客

          public?abstract?class?Drinks?{

          ????void?boilWater()?{
          ????????System.out.println("將水煮沸");
          ????}

          ????abstract?void?brew();

          ????void?pourInCup()?{
          ????????System.out.println("倒入杯子");
          ????}

          ????abstract?void?addCondiments();

          ????public?final?void?makingDrinks()?{
          ????????boilWater();
          ????????brew();
          ????????pourInCup();

          ????????//如果顧客需要,才加料
          ????????if?(customerLike())?{
          ????????????addCondiments();
          ????????}
          ????}

          ????//定義一個(gè)空的缺省方法,只返回?true
          ????boolean?customerLike()?{
          ????????return?true;
          ????}
          }

          如上,我們加了一個(gè)邏輯判斷,邏輯判斷的方法時(shí)一個(gè)只返回 true 的方法,這個(gè)方法我們叫做 鉤子方法。

          鉤子:在模板方法的父類中,我們可以定義一個(gè)方法,它默認(rèn)不做任何事,子類可以視情況要不要覆蓋它,該方法稱為“鉤子”。

          鉤子方法一般是空的或者有默認(rèn)實(shí)現(xiàn)。鉤子的存在,可以讓子類有能力對算法的不同點(diǎn)進(jìn)行掛鉤。而要不要掛鉤,又由子類去決定。

          是不是很有用呢,我們再看下咖啡的制作

          public?class?Coffee?extends?Drinks?{
          ????@Override
          ????void?brew()?{
          ????????System.out.println("沖咖啡粉");
          ????}

          ????@Override
          ????void?addCondiments()?{
          ????????System.out.println("加奶加糖");
          ????}
          ??//覆蓋了鉤子,提供了自己的詢問功能,讓用戶輸入是否需要加料
          ????boolean?customerLike()?{
          ????????String?answer?=?getUserInput();
          ????????if?(answer.toLowerCase().startsWith("y"))?{
          ????????????return?true;
          ????????}?else?{
          ????????????return?false;
          ????????}
          ????}

          ????//處理用戶的輸入
          ????private?String?getUserInput()?{
          ????????String?answer?=?null;
          ????????System.out.println("您想要加奶加糖嗎?輸入 YES 或 NO");
          ????????BufferedReader?reader?=?new?BufferedReader(new?InputStreamReader(System.in));
          ????????try?{
          ????????????answer?=?reader.readLine();
          ????????}?catch?(IOException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????if?(answer?==?null)?{
          ????????????return?"no";
          ????????}
          ????????return?answer;
          ????}
          }

          接著再去測試下代碼,看看結(jié)果吧。

          我想你應(yīng)該知道鉤子的好處了吧,它可以作為條件控制,影響抽象類中的算法流程,當(dāng)然也可以什么都不做。

          模板方法有很多種實(shí)現(xiàn),有時(shí)看起來可能不是我們所謂的“中規(guī)中矩”的設(shè)計(jì)。接下來我們看下 JDK 和 Spring 中是怎么使用模板方法的。

          JDK 中的模板方法

          我們寫代碼經(jīng)常會(huì)用到 comparable 比較器來對數(shù)組對象進(jìn)行排序,我們都會(huì)實(shí)現(xiàn)它的 compareTo() 方法,之后就可以通過 Collections.sort() 或者 Arrays.sort() 方法進(jìn)行排序了。

          具體的實(shí)現(xiàn)類就不寫了(可以去 github:starfish-learning 上看我的代碼),看下使用

          @Override
          public?int?compareTo(Object?o)?{
          ????Coffee?coffee?=?(Coffee)?o;
          ????if(this.price?????????return?-1;
          ????}else?if(this.price?==?coffee.price){
          ????????return?0;
          ????}else{
          ????????return?1;
          ????}
          }
          public?static?void?main(String[]?args)?{
          ??Coffee[]?coffees?=?{new?Coffee("星冰樂",38),
          ??????????????????????new?Coffee("拿鐵",32),
          ??????????????????????new?Coffee("摩卡",35)};
          ?
          ??Arrays.sort(coffees);

          ??for?(Coffee?coffee1?:?coffees)?{
          ????System.out.println(coffee1);
          ??}

          }

          你可能會(huì)說,這個(gè)看著不像我們常規(guī)的模板方法,是的。我們看下比較器實(shí)現(xiàn)的步驟

          1. 構(gòu)建對象數(shù)組
          2. 通過 Arrays.sort 方法對數(shù)組排序,傳參為 Comparable 接口的實(shí)例
          3. 比較時(shí)候會(huì)調(diào)用我們的實(shí)現(xiàn)類的 compareTo() 方法
          4. 將排好序的數(shù)組設(shè)置進(jìn)原數(shù)組中,排序完成

          一臉懵逼,這個(gè)實(shí)現(xiàn)竟然也是模板方法。

          這個(gè)模式的重點(diǎn)在于提供了一個(gè)固定算法框架,并讓子類實(shí)現(xiàn)某些步驟,雖然使用繼承是標(biāo)準(zhǔn)的實(shí)現(xiàn)方式,但通過回調(diào)來實(shí)現(xiàn),也不能說這就不是模板方法。

          其實(shí)并發(fā)編程中最常見,也是面試必問的 AQS 就是一個(gè)典型的模板方法。

          Spring 中的模板方法

          Spring 中的設(shè)計(jì)模式太多了,而且大部分?jǐn)U展功能都可以看到模板方式模式的影子。

          我們看下 IOC 容器初始化時(shí)中的模板方法,不管是 XML 還是注解的方式,對于核心容器啟動(dòng)流程都是一致的。

          AbstractApplicationContextrefresh 方法實(shí)現(xiàn)了 IOC 容器啟動(dòng)的主要邏輯。

          一個(gè) refresh() 方法包含了好多其他步驟方法,像不像我們說的 模板方法,getBeanFactory() 、refreshBeanFactory() 是子類必須實(shí)現(xiàn)的抽象方法,postProcessBeanFactory() 是鉤子方法。

          public?abstract?class?AbstractApplicationContext?extends?DefaultResourceLoader
          ??????implements?ConfigurableApplicationContext?
          {
          ?@Override
          ?public?void?refresh()?throws?BeansException,?IllegalStateException?{
          ??synchronized?(this.startupShutdownMonitor)?{
          ???prepareRefresh();
          ???ConfigurableListableBeanFactory?beanFactory?=?obtainFreshBeanFactory();
          ???prepareBeanFactory(beanFactory);
          ????????????postProcessBeanFactory(beanFactory);
          ????????????invokeBeanFactoryPostProcessors(beanFactory);
          ????????????registerBeanPostProcessors(beanFactory);
          ????????????initMessageSource();
          ????????????initApplicationEventMulticaster();
          ????????????onRefresh();
          ????????????registerListeners();
          ????????????finishBeanFactoryInitialization(beanFactory);
          ????????????finishRefresh();
          ??}
          ?}
          ????//?兩個(gè)抽象方法
          ????@Override
          ?public?abstract?ConfigurableListableBeanFactory?getBeanFactory()?throws???IllegalStateException;?
          ????
          ????protected?abstract?void?refreshBeanFactory()?throws?BeansException,?IllegalStateException;
          ????
          ????//鉤子方法
          ????protected?void?postProcessBeanFactory(ConfigurableListableBeanFactory?beanFactory)?{
          ?}
          ?}

          打開你的 IDEA,我們會(huì)發(fā)現(xiàn)常用的 ClassPathXmlApplicationContextAnnotationConfigApplicationContext 啟動(dòng)入口,都是它的實(shí)現(xiàn)類(子類的子類的子類的...)。

          AbstractApplicationContext的一個(gè)子類 AbstractRefreshableWebApplicationContext 中有鉤子方法 onRefresh()的實(shí)現(xiàn):

          public?abstract?class?AbstractRefreshableWebApplicationContext?extends?……?{
          ????/**
          ??*?Initialize?the?theme?capability.
          ??*/

          ?@Override
          ?protected?void?onRefresh()?{
          ??this.themeSource?=?UiApplicationContextUtils.initThemeSource(this);
          ?}
          }

          看下大概的類圖:

          小總結(jié)

          優(yōu)點(diǎn):1、封裝不變的部分,擴(kuò)展可變的部分。2、提取公共代碼,便于維護(hù)。3、行為由父類控制,子類實(shí)現(xiàn)。

          缺點(diǎn):每一個(gè)不同的實(shí)現(xiàn)都需要一個(gè)子類來實(shí)現(xiàn),導(dǎo)致類的個(gè)數(shù)增加,使得系統(tǒng)更加龐大。

          使用場景:1、有多個(gè)子類共有的方法,且邏輯相同。2、重要的、復(fù)雜的方法,可以考慮作為模板方法。

          注意事項(xiàng):為防止惡意操作,一般模板方法都加上 final 關(guān)鍵詞。

          參考:

          《Head First 設(shè)計(jì)模式》、《研磨設(shè)計(jì)模式》

          https://sourcemaking.com/design_patterns/template_method

          1.?人人都能看懂的 6 種限流實(shí)現(xiàn)方案!

          2.?一個(gè)空格引發(fā)的“慘案“

          3.?大型網(wǎng)站架構(gòu)演化發(fā)展歷程

          4.?Java語言“坑爹”排行榜TOP 10

          5. 我是一個(gè)Java類(附帶精彩吐槽)

          6. 看完這篇Redis緩存三大問題,保你能和面試官互扯

          7. 程序員必知的 89 個(gè)操作系統(tǒng)核心概念

          8. 深入理解 MySQL:快速學(xué)會(huì)分析SQL執(zhí)行效率

          9. API 接口設(shè)計(jì)規(guī)范

          10. Spring Boot 面試,一個(gè)問題就干趴下了!



          掃碼二維碼關(guān)注我


          ·end·

          —如果本文有幫助,請分享到朋友圈吧—

          我們一起愉快的玩耍!



          你點(diǎn)的每個(gè)贊,我都認(rèn)真當(dāng)成了喜歡
          瀏覽 45
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美精品三级在线 | 成人自拍在线观看 | 韩国精品无码电影 | 国产一区二区无码 | 波多野结衣丝袜无码视频 |