<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ù)用代碼的

          共 4150字,需瀏覽 9分鐘

           ·

          2020-09-26 14:41

          前言
          模板,顧名思義,它是一個(gè)固定化、標(biāo)準(zhǔn)化的東西。
          模板方法模式是一種行為設(shè)計(jì)模式, 它在超類中定義了一個(gè)算法的框架, 允許子類在不修改結(jié)構(gòu)的情況下重寫算法的特定步驟。


          場(chǎng)景問(wèn)題

          程序員不愿多扯,上來(lái)先干兩行代碼
          網(wǎng)上模板方法的場(chǎng)景示例特別多,個(gè)人感覺(jué)還是《Head First 設(shè)計(jì)模式》中的例子比較好。
          假設(shè)我們是一家飲品店的師傅,起碼需要以下兩個(gè)手藝
          真簡(jiǎn)單哈,這么看,步驟大同小異,我的第一反應(yīng)就是寫個(gè)業(yè)務(wù)接口,不同的飲品實(shí)現(xiàn)其中的方法就行,像這樣
          畫完類圖,猛地發(fā)現(xiàn),第一步和第三步?jīng)]什么差別,而且做飲品是個(gè)流程式的工作,我希望使用時(shí),直接調(diào)用一個(gè)方法,就去執(zhí)行對(duì)應(yīng)的制作步驟。
          靈機(jī)一動(dòng),不用接口了,用一個(gè)抽象父類,把步驟方法放在一個(gè)大的流程方法 makingDrinks() 中,且第一步和第三步,完全一樣,沒(méi)必要在子類實(shí)現(xiàn),改進(jìn)如下
          再看下我們的設(shè)計(jì),感覺(jué)還不錯(cuò),現(xiàn)在用同一個(gè) makingDrinks() 方法來(lái)處理咖啡和茶的制作,而且我們不希望子類覆蓋這個(gè)方法,所以可以申明為 final,不同的制作步驟,我們希望子類來(lái)提供,必須在父類申明為抽象方法,而第一步和第三步我們不希望子類重寫,所以我們聲明為非抽象方法
          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與模式》一書中開(kāi)頭是這樣描述模板方法(Template Method)模式的:
          模板方法模式是類的行為模式。準(zhǔn)備一個(gè)抽象類,將部分邏輯以具體方法以及具體構(gòu)造函數(shù)的形式實(shí)現(xiàn),然后聲明一些抽象方法來(lái)迫使子類實(shí)現(xiàn)剩余的邏輯。不同的子類可以以不同的方式實(shí)現(xiàn)這些抽象方法,從而對(duì)剩余的邏輯有不同的實(shí)現(xiàn)。這就是模板方法模式的用意。
          寫代碼的一個(gè)很重要的思考點(diǎn)就是“變與不變”,程序中哪些功能是可變的,哪些功能是不變的,我們可以把不變的部分抽象出來(lái),進(jìn)行公共的實(shí)現(xiàn),把變化的部分分離出來(lái),用接口來(lái)封裝隔離,或用抽象類約束子類行為。模板方法就很好的體現(xiàn)了這一點(diǎn)。
          模板方法定義了一個(gè)算法的步驟,并允許子類為一個(gè)或多個(gè)步驟提供實(shí)現(xiàn)。
          模板方法模式是所有模式中最為常見(jiàn)的幾個(gè)模式之一,是基于繼承的代碼復(fù)用的基本技術(shù),我們?cè)倏聪骂悎D
          模板方法模式就是用來(lái)創(chuàng)建一個(gè)算法的模板,這個(gè)模板就是方法,該方法將算法定義成一組步驟,其中的任意步驟都可能是抽象的,由子類負(fù)責(zé)實(shí)現(xiàn)。這樣可以確保算法的結(jié)構(gòu)保持不變,同時(shí)由子類提供部分實(shí)現(xiàn)
          再回顧下我們制作咖啡和茶的例子,有些顧客要不希望咖啡加糖或者不希望茶里加檸檬,我們要改造下模板方法,在加相應(yīng)的調(diào)料之前,問(wèn)下顧客
          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)。鉤子的存在,可以讓子類有能力對(duì)算法的不同點(diǎn)進(jìn)行掛鉤。而要不要掛鉤,又由子類去決定。
          是不是很有用呢,我們?cè)倏聪驴Х鹊闹谱?/span>
          public?class?Coffee?extends?Drinks?{
          ????@Override
          ????void?brew()?{
          ????????System.out.println("沖咖啡粉");
          ????}

          ????@Override
          ????void?addCondiments()?{
          ????????System.out.println("加奶加糖");
          ????}
          ??//覆蓋了鉤子,提供了自己的詢問(wèn)功能,讓用戶輸入是否需要加料
          ????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;
          ????}
          }
          接著再去測(cè)試下代碼,看看結(jié)果吧。
          我想你應(yīng)該知道鉤子的好處了吧,它可以作為條件控制,影響抽象類中的算法流程,當(dāng)然也可以什么都不做。
          模板方法有很多種實(shí)現(xiàn),有時(shí)看起來(lái)可能不是我們所謂的“中規(guī)中矩”的設(shè)計(jì)。接下來(lái)我們看下 JDK 和 Spring 中是怎么使用模板方法的。

          JDK 中的模板方法

          我們寫代碼經(jīng)常會(huì)用到 comparable 比較器來(lái)對(duì)數(shù)組對(duì)象進(jìn)行排序,我們都會(huì)實(shí)現(xiàn)它的 compareTo() 方法,之后就可以通過(guò) 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("星冰樂(lè)",38),
          ??????????????????????new?Coffee("拿鐵",32),
          ??????????????????????new?Coffee("摩卡",35)};
          ?
          ??Arrays.sort(coffees);

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

          }
          你可能會(huì)說(shuō),這個(gè)看著不像我們常規(guī)的模板方法,是的。我們看下比較器實(shí)現(xiàn)的步驟
          1. 構(gòu)建對(duì)象數(shù)組
          2. 通過(guò) Arrays.sort 方法對(duì)數(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)方式,但通過(guò)回調(diào)來(lái)實(shí)現(xiàn),也不能說(shuō)這就不是模板方法。
          其實(shí)并發(fā)編程中最常見(jiàn),也是面試必問(wèn)的 AQS 就是一個(gè)典型的模板方法。

          Spring 中的模板方法

          Spring 中的設(shè)計(jì)模式太多了,而且大部分?jǐn)U展功能都可以看到模板方式模式的影子。
          我們看下 IOC 容器初始化時(shí)中的模板方法,不管是 XML 還是注解的方式,對(duì)于核心容器啟動(dòng)流程都是一致的。
          AbstractApplicationContextrefresh 方法實(shí)現(xiàn)了 IOC 容器啟動(dòng)的主要邏輯。
          一個(gè) refresh() 方法包含了好多其他步驟方法,像不像我們說(shuō)的 模板方法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)?{
          ?}
          ?}
          打開(kāi)你的 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è)子類來(lái)實(shí)現(xiàn),導(dǎo)致類的個(gè)數(shù)增加,使得系統(tǒng)更加龐大。
          使用場(chǎng)景:1、有多個(gè)子類共有的方法,且邏輯相同。2、重要的、復(fù)雜的方法,可以考慮作為模板方法。
          注意事項(xiàng):為防止惡意操作,一般模板方法都加上 final 關(guān)鍵詞。


          參考:

          《Head First 設(shè)計(jì)模式》、《研磨設(shè)計(jì)模式》
          https://sourcemaking.com/design_patterns/template_method

          有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
          歡迎大家關(guān)注Java之道公眾號(hào)

          好文章,我在看??
          瀏覽 58
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  天天大香蕉 | 亚洲黄色电影网址 | 亚洲精品一二三四五区 | 成人福利视频在线 | 日本视频在线三区 |