Android架構演進 · 設計模式· 為什麼建議你一定要學透設計模式?

語言: CN / TW / HK

highlight: a11y-dark theme: orange


【小木箱成長營】設計模式系列文章(排期中):

Android架構演進 · 設計模式 · Android常見的4種建立型設計模式(上)

Android架構演進 · 設計模式 · Android常見的4種建立型設計模式(下)

Android架構演進 · 設計模式 · Android常見的6種結構型設計模式(上)

Android架構演進 · 設計模式 · Android常見的6種結構型設計模式(中)

Android架構演進 · 設計模式 · Android常見的6種結構型設計模式(下)

Android架構演進 · 設計模式 · Android常見的8種行為型設計模式(上)

Android架構演進 · 設計模式 · Android常見的8種行為型設計模式(中)

Android架構演進 · 設計模式 · Android常見的8種行為型設計模式(下)

Android架構演進 · 設計模式 · 設計模式在Android實時監控體系的實踐

Tips:

小木箱在BaguTree對設計模式進行了深度分析,關注公眾號小木箱成長營,回覆“設計模式”可免費獲得設計模式線上思維導圖

一、引言

Hello,我是小木箱,歡迎來到小木箱成長營Android架構演進系列教程,今天將分享Android架構演進 · 設計模式· 為什麼建議你一定要學透設計模式?

今天分享的內容主要分為四部分內容,第一部分是設計模式5W2H,第二部分是7大設計原則,第三部分是3大設計模式,最後一部分是總結與展望。

其中,7大設計原則主要包括開閉原則、里氏替換原則、依賴倒置原則、單一職責原則、介面隔離原則、最小知識原則和合成複用原則。3大設計模式主要包括建立型、結構型和行為型。

拿破崙之前說過Every French soldier carries a marshal’s baton in his knapsack,意思是“每個士兵揹包裡都應該裝有元帥的權杖”,本意是激勵每一名上戰場的士兵都要有大局觀,有元帥的思維。

程式設計的海洋裡也是如此,不想當架構師的程式設計師不是好程式設計師,我覺得架構師可能是大部分程式設計師最理想的歸宿。而設計模式是每一個架構師所必備的技能之一,只有學透了設計模式,才敢說真正理解了軟體工程。

希望每個有技術追求的Android開發,可以從程式碼中去尋找屬於自己的那份快樂,通過程式碼構建程式設計生涯的架構思維。

如果學完小木箱Android架構演進設計模式系列文章,那麼任何人都能為社群或企業貢獻優質的SDK設計方案。

二、設計模式5W2H

5W2H又叫七何分析法,5W2H是二戰的時候,美國陸軍兵器修理部發明的思維方法論,便於啟發性的理解深水區知識。5W2H是What、Who、Why、Where、When、How much、How首字母縮寫之和,廣泛用於企業技術管理、頭腦風暴等。小木箱今天嘗試用5W2H分析法分析一定要學透設計模式底層邏輯。

2.1 What: 什麼是設計模式?

首先聊聊設計模式5W2H的What, 什麼是設計模式?sourcemaking曾經提過,在軟體工程中,設計模式是軟體設計中,常見問題的可重複解決方案。設計模式雖然不可以直接轉換為程式碼,然後完成設計。但是設計模式是解決不同情況特定共性問題的通用模板。

不管北京的四合院、廣州的小蠻腰還是上海的東方明珠,都有好的建築地基。設計模式也是如此,設計模式是高效能工程建設基石,如果業務程式碼看作鋼筋水泥,那麼設計模式可以看作是建築地基。只有地基足夠牢固,專案工程才不會因為質量問題爛尾。

如果想高度重用程式碼,那麼建議你一定要學透設計模式。

如果想讓程式碼更容易被理解,那麼建議你一定要學透設計模式。

如果想確保程式碼可靠性、可維護性,那麼建議你一定要學透設計模式。

簡而言之,設計模式不僅是被多數人知曉、被反覆驗證的程式碼設計經驗總結,而且設計模式是特定場景和業務痛點,針對同類問題通用解決方案。

2.2 Who: 誰應該學透設計模式?

聊完設計模式5W2H的What,再聊聊設計模式5W2H的Who,誰應該學透設計模式? 設計模式可以用於所有專案開發工作的一種高價值設計理念,而且在寫原始碼或者閱讀原始碼中經常需要用到。

如果你的工作是偏架構方向,那麼使用設計模式可能像一日三餐一樣頻繁。設計模式既然連結著每個基礎模組的方方面面,就看你想不想讓你的程式設計生涯走的更遠,如果想,就接著往下面看。

設計模式不是程式碼! 設計模式不是程式碼! 設計模式不是程式碼! 重要的事情說三遍,設計模式是程式設計思想。如果參加外企面試,那麼基本不會像國內考各種八股文,有兩個重點考查專案,一方面是演算法,另一方面是系統設計。而系統設計和設計模式息息相關。

如果面試國內中大廠架構組也是,設計模式對於架構組程式設計師而言,基本隨便拿捏,設計模式是區分中級程式設計師和高階程式設計師的關鍵。當國內Android程式設計師疲於業務,導致設計模式在業務方面缺少實踐,如果你掌握的比他們更好,是不是相比之下會更有競爭力。學透設計模式是普通開發逆襲架構師的捷徑,但凡有一定工作年限的Android開發,都必須學透設計模式

2.3 Why: 為什麼要學透設計模式?

聊完設計模式5W2H的Who,再聊聊設計模式5W2H的Why,為什麼要學透設計模式? 關於為什麼要學透設計模式的原因一共有三點。

第一,學透設計模式,為職場晉升開啟綠色通道。 程式碼是程式設計師的一個門面。有些網際網路大廠技術崗晉升,會隨機抽取對方程式碼提交節點,根據對方的程式碼片段,進行review並給予晉升打分。這是平時為什麼要注重程式碼整潔性很重要原因。學透了設計模式並應用於專案開發,等同你有了職場晉升直升飛機。

第二,學透設計模式,編碼全域性觀更強。 正確使用設計模式,無異於站在巨人的肩膀上看世界。前輩們在 Java 、C++等語言領域上,花費幾十年時間,並經過分類、提煉、總結、沉澱和驗證等各個方面的努力,才整理了一套精品架構思想,如果想成為一名Senior Android Dev,那麼有什麼理由不去研究呢?

第三,學透設計模式,抽象建模能力更強。 對於特定場景和特定業務,如果正確的使用設計模式,那麼程式碼質量和業務拓展性會有質的飛躍。

2.4 When: 什麼時候使用設計模式?

說完設計模式5W2H的Who,再聊聊設計模式5W2H的When,關於使用設計模式的時機一共有兩點。

第一點,場景要吻合

第二點,確保原有業務穩定基礎上,套用或靈活運用設計模式,可以解決未來可能出現的拓展性和維護性問題。

2.5 Where: 哪些地方需要使用到設計模式?

說完設計模式5W2H的When,再聊聊設計模式5W2H的Where,哪些地方需要使用到設計模式?多數情況下,如果程式設計師做技術需求要考慮其靈活性的地方,就可以使用設計模式。22種設計模式都有自己的策略,22種設計模式的策略也適合不同的場景。

我們不可能從策略設計模式的業務背景套用狀態設計模式的業務背景,好比女朋友,世界上沒有性格一模一樣的女生,一個女生只能解決當時狀態的情感需要。我們的前任和現任帶給的體感都不完全一樣。因此,每一種設計模式中的策略和女朋友一樣,每一個都是原創

設計模式和女朋友有一個共同特徵就是提供了當時背景下可以解決問題(情緒價值)的結構。在解決實際問題時,必須考慮該問題解決方案的變動,如果業務發生大的變動,那麼需要考慮設計模式是否通用。好比女朋友,如果當下你習慣內卷,但女朋友突然躺平了,那麼後面話題可能越來越少。

使用設計模式切勿生搬硬套,正確的使用設計模式可以很好地將技術需求對映業務模型上。但是如果過度使用不合適的設計模式會造成程式可讀性更差,維護成本變得更高。好比女朋友,如果為了女朋友過度忍讓,那麼最終可能因為關係不平等不歡而散。

那麼,怎樣挑選合適的設計模式呢?使用設計模式的準則是在建立對設計模式有很好認知前提,並習慣這種方法模型,看到一種特定的技術背景,就立馬可以聯想到具體對應的模型。這種地方使用設計模式是最合適不過的。好比追女生,如果能對方興趣愛好和性格特徵能相互吸引,各方面背景匹配度高,會更合適一點。

綜上所述,如果你發現特定業務痛點,剛好符合特定設計原則,或能匹配特定設計模式方法模型,那麼建議你將這種業務抽象成通用模板對映到實際業務裡。

2.6 How much: 學透設計模式的價值點是什麼?

說完設計模式5W2H的Where,再聊聊設計模式5W2H的How much,學透設計模式的價值點是什麼?關於使用設計模式的價值一共有三點,第一點是針對個人,第二點是針對工程質量,最後一點是針對團隊。

對個人而言,正確使用設計模式可以提高程式程式碼設計能力和職場受歡迎度。

對工程質量而言,如果想要程式碼高可用、高複用、可讀性強和擴充套件性強,那麼需要設計模式做支撐。

對團隊而言,在現有工業化和商業化的程式碼設計維度上,設計模式不僅更標準和更工程化,而且設計模式可以提高編碼開發效率,節約解決問題時間。

2.7 How: 怎樣學透設計模式?

說完設計模式5W2H的How much,再聊聊設計模式5W2H的How,怎樣學透設計模式?學透設計模式有四種途徑,分別是網課、文章、書籍、原始碼和專案實戰。網課方面,小木箱推薦大家在B站看一下馬士兵教育圖靈課堂視訊。這兩門課程可以帶大家很輕鬆的入門設計模式。

文章方面,小木箱推薦大家看一下百度工程師教你玩轉設計模式(觀察者模式)提升程式碼質量的方法:領域模型、設計原則、設計模式洞察設計模式的底層邏輯設計模式二三事設計模式在業務系統中的應用Android中竟然包含這麼多設計模式,一起來學一波!當設計模式遇上 Hooks談談我工作中的23個設計模式設計模式之美等文章。

書籍方面,小木箱推薦大家看一下 《head first》 《重學Java設計模式 RPC中介軟體設計應用程式設計程式設計實戰分散式領域驅動設計和設計模式結合》程式碼整潔之道

原始碼方面, Glog日誌框架可以值得一學。

專案實戰方面,學有餘力的同學可以動手用設計模式實現一下定位元件、實時日誌元件和啟動監控元件。

最後,聽說有個叫小木箱這個傢伙設計模式的文章寫的還挺不錯的,可以關注一下~

三、7大設計原則

產生程式碼差的原因,有兩方面,第一方面是外部原因,第二方面是內部原因。外部原因主要有:專案排期急,沒有多少時間去設計;資源短缺,人手不夠,只能怎麼快怎麼來;緊急問題修復,臨時方案快速處理……。內部原因主要有:自我要求不高;無反饋通道

而解決程式碼差的根因主要是方法有三個:領域建模、設計原則、設計模式

分析階段:當拿到一個需求時,先不要著急想著怎麼把這個功能實現,這種很容易陷入事務指令碼的模式。

  1. 分析什麼呢?需要分析需求的目的是什麼、完成該功能需要哪些實體承擔,這一步核心是找實體。

image.png

舉個上面進店Tab展示的例子,它有兩個關鍵的實體:導航欄、Tab,其中導航欄裡面包含了若干個Tab。

設計階段:分析完了有哪些實體後,再分析職責如何分配到具體的實體上,這就要運用一些設計原則去指導

回到上面的例子上,Tab的職責主要有兩個:一個是Tab能否展示,這是它自己的職責,如上新Tab展示的邏輯是店鋪30天內有上架新商品;

另一個職責就是Tab規格資訊的構建,也是它自己要負責的。

導航欄的職責有兩個:一個是接受Tab註冊;另一個是展示。職責分配不合理,也就不滿足高內聚、低耦合的特徵。

打磨階段:這個階段選擇合適的模式去實現,大家一看到模式都會理解它是做什麼的,比如看到模板類,就會知道處理通用的業務流程,具體變化的部分放在子類中處理。

上面的這個例子,用到了2個設計模式:一個是訂閱者模式,Tab自動註冊的過程;另一個是模板模式,先判斷Tab能否展示,然後再構建Tab規格資訊,流程雖然簡單,也可以抽象出來通用的流程出來,子類只用簡單地重寫2個方法。

領域模型主要是和產品和運營梳理業務模型,進行流程化優化,進而判斷需求是否合理可行。

提升程式碼質量還有一個捷徑,那就是要遵循七大原則,七大原則好比毛澤東農村包圍城市指導方針。首先確定統一中國目標,然後是在統治力量薄弱的農村建立革命根據地,等革命隊伍變大,建立農村包圍城市的矩陣,最後採取不同摧毀策略對國民政府不同城市政權進行各個擊破。

如果系統工程業務程式碼混亂,我們首先確保底層程式碼功能不變,然後以點成線,以線成面,以面成網,以網建模。根據設計原則,針對不同的業務痛點,制定單一原則或組合原則技術方案。接著小步快跑,穩定安全地實施軟體工程質量改造規劃,最終達到降低業務冗餘或者降低未來大幅度程式碼變更帶來的風險目的。設計原則的底層邏輯就是讓軟體能夠較好地應對變化,降本增效。

而設計原則又分為七個部分,分別是開閉原則、裡式替換原則、依賴倒置原則、介面隔離原則、最小知識原則、單一職責原則和合成複用原則。

3.1 開閉原則

第一個設計原則是開閉原則,開閉原則簡稱OCP,正如英文定義的那樣the open–closed principle, Entities should be open for extension, but closed for modification 對擴充套件開放,對修改關閉。

這樣做的目的是保護已有程式碼的穩定性、複用性、靈活性、維護性和可擴充套件性,同時又讓系統更具有彈性。

Android需求發生變化的時候,不提倡直接修改Android基類原始碼,儘量擴充套件模組或者擴充套件原有的功能,去實現新需求變更。

關於開閉原則,一般採用繼承或實現的方式,比如: 如果涉及到非通用功能,不要把業務邏輯加到BaseActvity,而是單獨使用ChildActvity類繼承abstract BaseActvity,並讓ChildActvity去拓展abstract BaseActvity的抽象方法。

翻一翻開源庫原始碼,面向抽象類或面向介面去實現的功能場景非常常見。

那麼,為什麼要使用開閉原則呢?

第一,開閉原則可以降低功能設計成本

第二,開閉原則可以提高程式碼穩定性

第三,開閉原則可以提高程式碼可維護性

第四,開閉原則可以降低測試的成本

因為無論是大佬還是小白改動工程陳舊程式碼塊,都無法保證改完後代碼是0風險的。因此,如果遵守開閉原則,那麼可以極大限度的降低變更引發的歷史功能性缺失、邏輯漏洞等風險。

3.1.1 UML圖例

老爸幫小明去買書,書有很多特徵,一種特徵是書是有名字的,一種特徵是書是有價格的,那如果按照開閉原則的話,首先要定義一個IBook介面,描述書的兩種特徵:名稱、價格。

image.png

然後用一個類NovelBook去實現這個介面,方便讀取和修改書的名稱和價格。

根據開閉原則,使用者如果要對書進行比如打折降價活動是不能直接在NovelBook操作的,需要用DisNovelBook繼承NovelBook去拓展NovelBook的getName和getPrice方法。

3.1.2 Bad Code

```java //----------------------------程式碼片段一----------------------------

/* * 功能描述: 定義小說類NovelBook-實現類 / public class NovelBook implements IBook { public String name; public int price;

public NovelBook(String name, int price) {
    this.name = name;
    this.price = price;
}

@Override
public String getName() {
    return this.name;
}

@Override
public int getPrice() {
if (this.price > 50) {
        return (int) (this.price * 0.9);
    } else {
        return this.price;
    }
}

} //----------------------------程式碼片段二---------------------------- /* * * 功能描述: 現在有個書店售書的場景,首先定義一個IBook類,裡面有兩個屬性:名稱、價格。 / public interface IBook{ public String getName(); public int getPrice(); } //----------------------------程式碼片段三---------------------------- public class Client { public static void main(String[] args) { NovelBook novel = new NovelBook("笑傲江湖", 100); System.out.println("書籍名字:" + novel.getName() + "書籍價格:" + novel.getPrice()); } } ```

3.1.3 Good Code

因為如果未來需求變更,如小明要買數學書和化學書,其中化學書價格不能超過15元,數學不能高於30元,且數學書可以使用人教版,而化學書既可以使用湘教版也可以使用人教版。

```java //----------------------------程式碼片段一----------------------------

/* * 功能描述: 定義小說類NovelBook-實現類 / public class NovelBook implements IBook { public String name; public int price;

public NovelBook(String name, int price) {
    this.name = name;
    this.price = price;
}

@Override
public String getName() {
    return this.name;
}

@Override
public int getPrice() {
    return this.price;
}

} //----------------------------程式碼片段二----------------------------

public class DisNovelBook extends NovelBook { public DisNovelBook(String name, int price) { super(name, price); }

// 複寫價格方法,當價格大於50,就打9析
@Override
public int getPrice() {
    if (this.price > 50) {
        return (int) (this.price * 0.9);
    } else {
        return this.price;
    }
}

}

//----------------------------程式碼片段三---------------------------- /* * * 功能描述: 現在有個書店售書的場景,首先定義一個IBook類,裡面有兩個屬性:名稱、價格。 / public interface IBook{ public String getName(); public int getPrice(); }

//----------------------------程式碼片段四---------------------------- public class Client{ public static void main(String[] args){ IBook disnovel = new DisNovelBook ("小木箱成長營",100000); System.out.println("公眾號名字:"+disnovel .getName()+"公眾號粉絲數量:"+disnovel .getPrice()); } } ```

這些邏輯加在一塊的話,因為購買條件不一樣,需要將不變的邏輯抽象成介面實現類NovelBook,但如果不使用開闢原則,直接更改介面實現類NovelBook,隨著需求不斷膨脹,但凡多增加一些控制項,在多人協同開發過程中程式碼維護風險度會越來越高。

3.1.4 使用原則

開闢原則使用原則有2個點,第一個點是抽象約束;第二個點是封裝變化

首先來說一說抽象約束,抽象約束一共有三個方面,第一個方面是介面或抽象類的方法全部要public,方便去使用。

第二個方面是引數型別、引用物件儘量使用介面或者抽象類,而不是實現類;因為使用介面和抽象類可以避免認為更改起始資料;

第三點是抽象層儘量保持穩定,一旦確定即不允許修改。如果抽象層經常變更,會導致所有實現類報錯。

接著來說一說封裝變化,封裝變化一共有兩個方面,第一個方面是相同的邏輯要抽象到一個介面或抽象類中。

第二個方面是將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。

比如上文說的,如果老爸買完書了,準備買菜,那麼要單獨立一個IVegetable的介面。而不是改造原來的IBook。

3.2 里氏替換原則

第二個設計原則是里氏替換原則,里氏替換原則簡稱LSP,正如英文定義的那樣The Liskov Substitution Principle,Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it 。

子類可以替換父類,子類物件能夠替換程式中父類物件出現的任何地方,並且保證原來程式的邏輯行為不變以及正確性不會被破壞。

相當於子類可以擴充套件父類功能。繼承是里氏替換原則的重要表現方式。里氏替換原則用來指導繼承關係中子類該如何設計的。

里氏替換原則,注意事項是儘量不要重寫父類的方法,也是開閉原則的重要方式之一,為什麼不建議重寫父類的方法呢?

因為重寫會覆蓋父類的功能,導致使用者對類預期功能被修改後得到就不是對方想要的功能。

提出問題

下面有個關於Bird鳥類位移時間的技術需求:

已知Bird(基類)的子類Swallow(小燕子)和Ostrich(鴕鳥)位移了300米,Ostrich(鴕鳥)的跑步速度為120米/秒。

Swallow(小燕子)和Ostrich(鴕鳥)的飛行速度為120米/秒和0米/秒,求Swallow(小燕子)和Ostrich(鴕鳥)的位移時間。

分析問題

位移時間,算的是位移距離/跑步速度還是位移距離/飛行速度呢?

Ostrich(鴕鳥)能飛嗎?

Swallow(小燕子)飛行速度能否單獨抽象成一個方法呢?

解決問題

可以參考UML圖例、Good Code、Bad Code和里氏替換使用原則。

3.2.1 UML圖例

image.png

3.2.2 Bad Code

常規方式❌: 定義鳥的基類Bird,Bird(基類)有一個setFlySpeed(飛翔速度)。根據distance(距離)去算出它飛翔的getFlyTime(飛翔時間)。

```java

//----------------------------程式碼片段一---------------------------- public class Bird { private double flySpeed;

public void setFlySpeed(double speed) {
    this.flySpeed = speed;
}

public double getFlyTime(double distance) {
    return distance / flySpeed;
}

}

//----------------------------程式碼片段二---------------------------- public class Swallow extends Bird {} //----------------------------程式碼片段三---------------------------- public class Ostrich extends Bird{

@Override
public void setFlySpeed(double speed) {
    speed  = 0;
}

} //----------------------------程式碼片段四---------------------------- public class Main { public static void main(String[] args) { Bird swallow = new Swallow(); Bird ostrich = new Ostrich(); swallow.setFlySpeed(120); ostrich.setFlySpeed(120); System.out.println("小木箱說,如果飛行300公里:"); try { System.out.println("燕子將飛行: " + swallow.getFlyTime(300) + "小時。"); // 燕子飛行2.5小時。 System.out.println("鴕鳥將飛行: " + ostrich.getFlyTime(300) + "小時。"); // 鴕鳥將飛行Infinity小時。 } catch (Exception err) { System.out.println("發生錯誤了!"); } } } ```

Bird(基類)有兩個子類,一個是Swallow(小燕子),一個是Ostrich(鴕鳥)。

小燕子只要設定正確的setFlySpeed(速度)和distance(距離)即可。

但Ostrich(鴕鳥)不太一樣,Ostrich(鴕鳥)是不會飛的,Ostrich(鴕鳥)只會地上跑。

因為Ostrich(鴕鳥)沒有flySpeed(飛翔速度)。那在構造Ostrich(鴕鳥),去繼承實現這 Bird(基類), Ostrich(鴕鳥)的重寫方法setFlySpeed(設定飛翔速度)傳0.0。

在Bird(基類) 當中去計算getFlyTime(飛翔時間),按照常規的應該distance(距離) / setFlySpeed(設定飛翔速度),就得到了getFlyTime(飛翔時間)。

去呼叫getFlyTime(飛翔時間) 時間的時候,因為對Ostrich(鴕鳥) 的getFlyTime(飛翔時間)的子類的引數speed,重寫了setFlySpeed(設定飛翔速度)方法,並設定該方法speed引數為0,數學裡面0不能作為分母,所以會得到一個無效結果Infinity,重寫過程,違背了里氏替換原則。

結果:

3.2.3 Good Code

正確的方式是✔️:打斷Ostrich(鴕鳥)和Bird(基類)繼承關係,定義Bird(基類)和Ostrich(鴕鳥)的超級父類Animal(動物),讓Animal(動物)有奔跑能力。Ostrich(鴕鳥)的飛行速度雖然為 0,但奔跑速度不為 0,可以計算出其奔跑 300 千米所要花費的時間。

那麼,雖然不能將Ostrich(鴕鳥)的getRunTime(位移時間)抽象成 Bird(基類)的 getFlyTime(飛翔時間)。

但可以利用超級父類Animal(動物)的getRunTime(位移時間),即花費時長,這時Ostrich(鴕鳥)的setRunSpeed(跑步速度)就不為0,因為Ostrich(鴕鳥)複用了超級父類Animal(動物) getRunTime(位移時間)功能。

超級父類Animal(動物)有一個 getRunSpeed(跑步速度) ,而不是Bird(基類)的setFlySpeed那個飛翔速度。

去設定setRunSpeed(跑步速度) 之後。因為位移是動物的天性。鳥類和鴕鳥都具備位移能力。

所以可以在超級父類Animal(動物) 的基礎上,定義Bird(基類) 子類,去繼承 Animal(動物) ,把Animal(動物)的一些能力轉化成Bird(基類) 相關一些能力,這樣就和預期需求是一致的了。

```java //----------------------------程式碼片段一---------------------------- public class Bird extends Animal { private double flySpeed;

public void setFlySpeed(double speed) {
    this.flySpeed = speed;
}

public double getFlyTime(double distance) {
    return distance / flySpeed;
}

} //----------------------------程式碼片段二---------------------------- public class Animal { private double runSpeed;

public double getRunTime(double distance) {
    return distance / speed;
}

public void setRunSpeed(double speed) {
    this.runSpeed = speed;
}

} //----------------------------程式碼片段三---------------------------- public class Swallow extends Bird {}

//----------------------------程式碼片段四---------------------------- public class Ostrich extends Animal{} //----------------------------程式碼片段五---------------------------- public class Main { public static void main(String[] args) { Bird swallow = new Swallow(); Animal ostrich = new Ostrich(); swallow.setFlySpeed(120); ostrich.setRunSpeed(120); System.out.println("如果飛行300公里:"); try { System.out.println("燕子將位移: " + swallow.getFlyTime(300) + "小時。"); System.out.println("鴕鳥將位移: " + ostrich.getRunTime(300) + "小時。"); } catch (Exception err) { System.out.println("發生錯誤了!"); } } } ```

結果:

3.2.4 使用原則

Java中,多型是不是違背了里氏替換原則?

那麼,JAVA中,多型是不是違背了里氏替換原則呢?如果extends的目的是為了多型,而多型的前提就是Swallow(子類)覆蓋並重新定義Bird(基類)的getFlySpeed()。

為了符合LSP,應該將Bird(基類)定義為abstract,並定義getFlySpeed()(抽象方法),讓Swallow(子類)重新定義getFlySpeed()。

當父類是abstract時,Bird(基類)就是不能例項化,所以也不存在可例項化的Bird(基類)物件在程式裡。

```java //----------------------------程式碼片段一---------------------------- public abstract class Bird{ protected abstract double getFlySpeed(); public double getFlyTime(double distance){ return distance / getFlySpeed(); } }

//----------------------------程式碼片段二----------------------------

public class Swallow extends Bird { protected double getFlySpeed() { return 100.0; } } ```

里氏替換原則和開閉原則的區別有哪些?

里氏替換原則和開閉原則的區別在於: 開閉原則大部分是面向介面程式設計,少部分是針對繼承的,而里氏替換原則主要針對繼承的,降低繼承帶來的複雜度

什麼時候使用里氏替換原則?

使用里氏替換原則的時機有兩個,第一個是重新提取公共部分的方法,第二個是改變繼承關係.

首先,重新提取公共部分的方法主要是把公共部分提取出來作為一個抽象基類.

而提取公共部分的時機是程式碼不是很多的時候應用,提取得部分可以作為一個設計工具.

然後,改變繼承關係主要是從父子關係變為委派關係或兄弟關係,可以把它們的一些公有特性提取到一個抽象介面,再分別實現.具體可以看 #3.2.1 UML圖例

3.3 依賴倒置原則

第三個設計原則是依賴倒置原則,依賴倒置原則簡稱DIP,正如英文定義的那樣Dependence Inversion Principle,Abstractions should not depend on details. Details should depend on abstractions,抽象不依賴於細節,而細節依賴於抽象。高層模組不能直接依賴低層模組,而是通過介面或抽象的方式去實現。

從定義也就可以看出來,依賴倒置原則是為了降低類或模組的耦合性,提倡面向介面程式設計,能降低工程維護成本,降低由於類或實現發生變化帶來的修改成本,提高程式碼穩定性。

比如小木箱在元件化設計當中,會員模組、訂單模組和使用者模組不應該直接依賴基礎平臺元件資料庫、網路和統計元件等。

而應該從會員模組、訂單模組和使用者模組抽取BaseModule和中介軟體等模組,橫向依賴基礎平臺元件BaseModule和中介軟體,去實現模組與模組之間的一些訪問與跳轉,這樣層級才會更清晰。

image.png

依賴倒置原則核心思想是面向介面程式設計,因為如果面向實現類,實現類如果發生變化,那麼依賴實現類的實現方法和功能都會產生蝴蝶效應。

提出問題

小木箱剛拿到駕照,準備在電動車、新能源、汽油車三型別進行購車,於是拿沃爾沃、寶馬、特斯拉進行測試,請用程式碼讓這三輛汽車自動跑起來?

分析問題

如果小木箱想把跑起來的自動駕駛程式碼,複用給其他駕駛者,程式碼的健壯性如何?

解決問題

可以參考UML圖例、Good Code、Bad Code和思考覆盤。

3.3.1 UML圖例

image.png

3.3.2 Bad Code

下面程式碼比較劣質的原因在於自動駕駛能力與駕駛者高耦合度,如果想讓其他駕駛者使用自動駕駛系列的車,那麼駕駛者必須將車型例項重新傳給其他駕駛者,沒有做到真正意義上的插拔式註冊,換個駕駛者就不成立了。

```java //----------------------------程式碼片段一---------------------------- public class BMW { public void autoRun() { System.out.println("BMW is running!"); } } //----------------------------程式碼片段二---------------------------- public class Tesla { public void autoRun() { System.out.println("Tesla is running!"); } } //----------------------------程式碼片段三---------------------------- public class Volvo { public void autoRun() { System.out.println("Volvo is running!"); } } //----------------------------程式碼片段四---------------------------- public class AutoDriver {

public void autoDrive(Tesla tesla) {
    tesla.autoRun();
}

public void autoDrive(BMW bm) {
    bm.autoRun();
}
public void autoDrive(Volvo volvo) {
    volvo.autoRun();
}

} //----------------------------程式碼片段四---------------------------- public class Main { public static void main(String[] args) { Tesla tesla = new Tesla(); BMW bm = new BMW(); Volvo volvo = new Volvo(); AutoDriver driver = new AutoDriver(); driver.autoDrive(tesla); driver.autoDrive(bm); driver.autoDrive(volvo); } } ```

結果:

3.3.3 Good Code

那麼,正確實現方式是怎樣的呢? 首先要定義一個自動駕駛介面IAutoDriver。因為自動駕駛,新能源比如說像寶馬、特斯拉、沃爾沃都有實現自動駕駛能力。

但是比如說像紅旗、長城不是一個自動駕駛的實現者。

那對自動駕駛介面IAutoDriver,如果你有自動駕駛能力,那麼你就去實現IAutoDriver,去重寫autoDrive(自動駕駛)的能力。否則,就不實現自動駕駛IAutoDriver介面。

對 AutoDriver 的話,駕駛者是去通過依賴倒置原則,把寶馬、特斯拉、沃爾沃自動駕駛模式介面IAutoCar傳進來,通過autoRun開啟自動駕駛模式。

autoRun是區分了自動駕駛還是普通駕駛模式。具體的程式碼方式很簡單,首先 new一個寶馬例項,然後去實現自動駕駛介面 IAutoCar,最後把寶馬例項傳給 AutoDriver,實現自動駕駛的方式,特斯拉、沃爾沃也是這樣的。

對於自動駕駛技術,不關心駕駛的什麼車,寶馬、特斯拉、沃爾沃還是大眾,只關心你是實現了IAutoDriver介面。只關心你是否有autoDrive(自動駕駛)能力。

如果有自動駕駛能力,使用者就直接呼叫autoDrive(自動駕駛)能力。具體的怎麼實現呢?是AutoDriver的實現類IAutoDriver決定的,這便是依賴倒置原則,不依賴具體的實現,只調IAutoCar介面方法選擇自動駕駛模式autoRun即可.

``` //----------------------------程式碼片段一---------------------------- public interface IAutoCar { public void autoRun(); } //----------------------------程式碼片段二---------------------------- public class BMW implements IAutoCar{ @Override public void autoRun() { System.out.println("BMW is running!"); } } //----------------------------程式碼片段三---------------------------- public class Tesla implements IAutoCar { @Override public void autoRun() { System.out.println("Tesla is running!"); } } //----------------------------程式碼片段四---------------------------- public class Volvo implements IAutoCar{ @Override public void autoRun() { System.out.println("Volvo is running!"); } }

//----------------------------程式碼片段五---------------------------- public interface IAutoDriver { public void autoDrive(IAutoCar car); } //----------------------------程式碼片段六---------------------------- public class AutoDriver implements IAutoDriver{

@Override
public void autoDrive(IAutoCar car) {
    car.autoRun();
}

} //----------------------------程式碼片段六---------------------------- public class Main { public static void main(String[] args) { IAutoDriver driver = new AutoDriver(); driver.autoDrive(new Tesla()); driver.autoDrive(new BMW()); driver.autoDrive(new Volvo()); } } ```

結果:

3.3.4 使用原則

在簡單工廠設計模式和策略設計模式,都是使用依賴倒置原則進行注入,不過簡單工廠設計模式, 使用的是介面方法注入, 而策略設計模式使用的是建構函式注入,這一塊後文詳細介紹。

3.4 單一職責原則

第四個設計原則是單一職責原則,單一職責原則簡稱SRP, 正如英文The Single Responsibility Principle定義的那樣,A class should have one, and only one, reason to change。

單一職責指的是一個類只能因為一個理由被修改,一個類只做一件事。不要設計大而全的類,要設計粒度小、功能單一的類。

類的職能要有界限。單一原則要求類要高內聚,低耦合。意思是為了規避程式碼冗餘,無關職責、無關功能的方法和物件不要引入類裡面。

因為如果一個類承擔的職責過多,就等於把這些職責耦合在一起,一個職責的變化可能會削弱或者抑制這個類完成其他職責的能力。

這種耦合會導致脆弱他的設計,當變化發生時,設計會遭受到意想不到的破壞;軟體設計真正要做的許多內容就是發現職責並把那些職責相互分離。

比如去銀行取錢,取錢的類不應該包含打印發票,取錢的類只管取錢動作,打印發票功能,需要新建類完成。目的是降低類的複雜度,提高閱讀性,降低程式碼變更造成的風險。

再比如Android裡面Activity過於臃腫會讓感覺很頭大,MVP、MVVM、MVP和MVI等架構都是為了讓Activity變得職責單一。

提出問題:

老師去網上採購“ 三國演義 ”、“ 紅樓夢 ”、“ 三國演義 ”、“ 西遊記 ”各一本。

已知“ 紅樓夢 ”50元/本,“ 三國演義 ”40元/本,“ 西遊記 ”30元/本,“ 水滸傳 ”20元/本。

如果“ 紅樓夢 ” 8 折促銷,“ 西遊記 ”6 折促銷,根據書的價格,求所有圖書的總價格。

分析問題:

如果採購1000本書籍,單品折扣策略可能不一樣,如果單品價格隨著單品購買數量變化,那麼購物車價格條件一旦變化,購物車程式碼會因此膨脹,進而影響程式碼可維護性,如何解決這種問題?

3.4.1 UML圖例

image.png

3.4.2 Bad Code

這段壞味道的程式碼問題就在於: 購物車摻雜了價格計算功能,購物車正常只關心對商品的CRUD能力,如果有一天,價格計算方式改變,那這裡就需要動購物車程式碼,購物車變更會引起方法變動,從而帶來風險。

``` //----------------------------程式碼片段一---------------------------- public class WoodBook { private String name; private double price;

public WoodBook(String name, double price) {
    this.name = name;
    this.price = price;
}

public String getName() {
    return name;
}

public double getPrice() {
    return price;
}

} //----------------------------程式碼片段二----------------------------

public class ShoppingCart {

private List<WoodBook> list = new ArrayList<>();

public void addBook(WoodBook book) {
    list.add(book);
}

public double checkOut() {
    double total = 0;
    for (WoodBook book : list) {
        if ("紅樓夢".equals(book.getName())) {
            total = total + book.getPrice() * 0.8;
        } else if ("西遊記".equals(book.getName())) {
            total = total + book.getPrice() * 0.6;
        } else {
            total = total + book.getPrice();
        }
    }
    return total;
}

} //----------------------------程式碼片段三---------------------------- public class Main { public static void main(String[] args) { ShoppingCart shoppingCart = new ShoppingCart(); shoppingCart.addBook(new WoodBook("紅樓夢",50)); shoppingCart.addBook(new WoodBook("三國演義",40)); shoppingCart.addBook(new WoodBook("西遊記",30)); shoppingCart.addBook(new WoodBook("水滸傳",20));

    double total = shoppingCart.checkOut();
    System.out.println("所有圖書價格為:"+total);
}

} ```

3.4.3 Good Code

正確的方式: 首先計算價格的邏輯,交給介面實現,購物車只關心價格計算的結果,並將結果返回即可。然後計算價格介面交給呼叫方實現,使用者不關心紅樓夢和西遊記價格折扣策略還是0折扣策略,最後需求如果發生變更,那麼只需要更改呼叫方實現邏輯即可。

``` //----------------------------程式碼片段一----------------------------

public class WoodBook { private String name; private double price;

public WoodBook(String name, double price) {
    this.name = name;
    this.price = price;
}

public String getName() {
    return name;
}

public double getPrice() {
    return price;
}

} //----------------------------程式碼片段二---------------------------- public class DefaultDiscountStrategy implements DiscountStrategy { @Override public double discount(List list) { double total = 0; for (WoodBook book : list) { total = total + book.getPrice(); } return total; } } //----------------------------程式碼片段三---------------------------- public class SingleDiscountStrategy implements DiscountStrategy {

@Override
public double discount(List<WoodBook> list) {
    double total = 0;
    for (WoodBook book : list) {
        if ("西遊記".equals(book.getName())) {
            total = total + book.getPrice() * 0.6;
        } else if ("紅樓夢".equals(book.getName().toString())) {
            total = total + book.getPrice() * 0.8;
        }else {
            total = total + book.getPrice() ;
        }
    }
    return total;
}

} //----------------------------程式碼片段四---------------------------- public class ShoppingCart {

private List<WoodBook> list = new ArrayList<>();
private DiscountStrategy discountStrategy;

public void addBook(WoodBook book) {
    list.add(book);
}

public void setDiscountStrategy(DiscountStrategy discountStrategy) {
    this.discountStrategy = discountStrategy;
}

public double checkOut() {
    if (discountStrategy == null) {
        discountStrategy = new DefaultDiscountStrategy();
    }
    return discountStrategy.discount(list);
}

} //----------------------------程式碼片段五---------------------------- public interface DiscountStrategy { double discount(List list); } //----------------------------程式碼片段六---------------------------- public class Main { public static void main(String[] args) {

    ShoppingCart shoppingCart = new ShoppingCart();
    shoppingCart.addBook(new WoodBook("紅樓夢",50));
    shoppingCart.addBook(new WoodBook("三國演義",40));
    shoppingCart.addBook(new WoodBook("西遊記",30));
    shoppingCart.addBook(new WoodBook("水滸傳",20));

    shoppingCart.setDiscountStrategy(new SingleDiscountStrategy());
    double total = shoppingCart.checkOut();
    System.out.println("所有圖書價格為:"+total);
}

} ```

結果:

3.4.4 思考覆盤

關於單一職責原則我們有四個問題需要思考

問題一: 如何判斷類的職責是否足夠單一?

判斷類的職責是否足夠單一有五條規則:

規則一: 如果類中的程式碼行數、函式或屬性過多,會影響程式碼的可讀性和可維護性,那麼我們就需要考慮對類進行拆分;

規則二: 如果類依賴的其他類過多,或者依賴類的其他類過多,不符合高內聚、低耦合的設計思想,那麼我們就需要考慮對類進行拆分;

規則三: 如果私有方法過多,我們就要考慮能否將私有方法獨立到新的類中,那麼我們就設定為 public 方法,供更多的類使用,從而提高程式碼的複用性;

規則四: 如果比較難給類起一個合適名字,很難用一個業務名詞概括,或者只能用一些籠統的Manager、Context 之類的詞語來命名,那麼這就說明類的職責定義得可能不夠清晰

規則五: 如果類中大量的方法都是集中操作類中的某幾個屬性,比如: 在 UserInfo 例子中,如果一半的方法都是在操作 address 資訊,那麼可以考慮將這幾個屬性和對應的方法拆分出來

問題二: 類的職責是否設計得越單一越好?

類的職責單一性標準有四方面。

第一方面,單一職責原則通過避免設計大而全的類,避免將不相關的功能耦合在一起,來提高類的內聚性。

第二方面,類職責單一,類依賴的和被依賴的其他類也會變少,減少了程式碼的耦合性,以此來實現程式碼的高內聚、低耦合。

第三方面,如果拆分得過細,實際上會適得其反,反倒會降低內聚性,也會影響程式碼的可維護性。

第四方面,根據不同的場景對某個類或模組單一職責的判斷是不同的,不能為了拆分而拆分,造成過度設計,難以維護。

問題三: 單一職責原則為什麼要這麼設計?

那麼單一職責原則為什麼要這麼設計?因為如果一個類承擔的職責過多,即耦合性太高一個職責的變化可能會影響到其他的職責。

問題四: Hook違背了單一職責原則嗎?

那麼,Hook違背了單一職責原則嗎?Hook突破了Java層OOP系統層設計理念,也就違背了單一職責原則。Hook雖好,不建議廣泛使用,因為在開發過程中可能導致依賴不清晰、命名衝突、來源不清晰等問題。

3.5 介面隔離原則

第五個原則是介面隔離原則,介面隔離原則指的是介面隔離原則是指客戶端不應該依賴於它不需要的介面。介面隔離原則簡稱ISP,正如英文定義的那樣interface-segregation principle,Clients should not be forced to depend upon interfaces that they do not use. 客戶端不應該被強迫依賴它不需要的介面。其中的 “客戶端”,可以理解為介面的呼叫者或者使用者。

介面隔離原則是儘量將臃腫龐大的介面顆粒度拆得更細。和單一原則類似,一個介面,涵蓋的職責實現的功能儘量簡單單一,只跟介面自身想實現的功能相關,不能把別人乾的活也涵蓋進來,讓實現者只關心介面獨立單元方法。

我在架構組設計對外的 API 或對外能力,接口乾的職責,要非常明確的,介面不能做與介面無關工作或隱藏邏輯,一個類對一個類依賴應建立在最小介面依賴基礎之上。

提出問題

小木箱是一名AndroidDev也是一名DevopsDev,請用程式碼分類列印標記小木箱的技能樹。

分析問題

首先將技能樹全部存放到技能清單IDev,然後讓AndroidDev和DevopsDev分別實現技能清單IDev,最後在AndroidDev和DevopsDev匹配的技能樹列印標記。

解決問題

可以參考UML圖例、Good Code、Bad Code和介面隔離使用原則。

3.5.1 UML圖例

image.png

3.5.2 Bad Code

比如小木箱做了AndroidDev和DevopsDev兩份簡歷,而AndroidDev簡歷和DevopsDev簡歷所具備的技術棧又各不相同,但歸檔在小木箱同一份IDev技能樹清單裡面。

如果小木箱把AndroidDev簡歷和DevopsDev簡歷實現技能樹清單介面,那麼勢必會導致AndroidDev簡歷既有Devops簡歷也有AndroidDev技能樹,DevopsDev簡歷既有DevopsDev技能樹也有AndroidDev技能樹。

如果有一天小木箱技能樹清單介面技能發生相應的變化,那麼很容易給兩份簡歷帶來一些風險和改變。

```java //--------------------------------程式碼塊一--------------------------------- public interface IDev { void framework();

void ci2cd();

void jetpack();

void java();

} //--------------------------------------程式碼塊二--------------------------------------- public class AndroidDev implements IDev{ @Override public void framework() { System.out.println("CrazyCodingBoy is a Android developer and he knows framework"); }

@Override
public void ci2cd() {}


@Override
public void jetpack() {
    System.out.println("CrazyCodingBoy is a Android developer and he knows jetpack");
}

@Override
public void java() {
    System.out.println("CrazyCodingBoy is a Android developer and he knows java");
}

} //--------------------------------------程式碼塊三--------------------------------------- public class DevopsDev implements IDev {

@Override
public void framework() {}

@Override
public void ci2cd() {
    System.out.println("CrazyCodingBoy is a Devops developer and he knows CI and CD");
}

@Override
public void jetpack() {}

@Override
public void java() {
    System.out.println("CrazyCodingBoy is a Devops developer and he knows java");
}

} //--------------------------------------程式碼塊四--------------------------------------- public class Main { public static void main(String[] args) { AndroidDev androidDev = new AndroidDev(); DevopsDev devopsDev = new DevopsDev(); androidDev.framework(); androidDev.jetpack(); devopsDev.ci2cd(); androidDev.java(); devopsDev.java(); // TODO: delete 無效空實現 androidDev.ci2cd(); devopsDev.framework(); devopsDev.jetpack();

}

```

結果:

3.5.3 Good Code

介面隔離原則是把臃腫龐大的IDev技能樹清單介面,拆分成力度更小的ICi2cd、IFramework、IJetpack和IJava介面,提高整個系統和介面的一個靈活性和可維護性,同時提高整個系統內聚性,減少對外互動。

ICi2cd只關心小木箱CI/CD的研發能力,誰想持有這個能力就交給誰去實現,不同的技能樹,交給不同的去完成自己的能力。

否則,IDev介面功能發生變化,就得去改AndroidDev和DevopsDev的邏輯。

如果程式碼臃腫,程式碼量大,那麼容易手抖或改了不該改的,造成線上事故。

如果通過介面或模組隔離方式實現,那麼就可以降低修改成本。

```java //--------------------------------程式碼塊一--------------------------------- public interface ICi2cd { void ci2cd(); } //--------------------------------------程式碼塊二--------------------------------------- public interface IFramework { void framework(); } //--------------------------------------程式碼塊三--------------------------------------- public interface IJetpack { void jetpack(); } //--------------------------------------程式碼塊四--------------------------------------- public interface IJava { void java(); } //--------------------------------------程式碼塊五--------------------------------------- public class AndroidDev implements IFramework , IJetpack , IJava { @Override public void framework() { System.out.println("CrazyCodingBoy is a Android developer and he knows framework"); }

@Override
public void jetpack() {
    System.out.println("CrazyCodingBoy is a Android developer and he knows jetpack");
}

@Override
public void java() {
    System.out.println("CrazyCodingBoy is a Android developer and he knows java");
}

}

//--------------------------------------程式碼塊六--------------------------------------- public class DevopsDev implements ICi2cd , IJava {

@Override
public void ci2cd() {
    System.out.println("CrazyCodingBoy is a Devops developer and he knows CI and CD");
}

@Override
public void java() {
    System.out.println("CrazyCodingBoy is a Devops developer and he knows java");
}

} //--------------------------------------程式碼塊七--------------------------------------- public class Main { public static void main(String[] args) { AndroidDev androidDev = new AndroidDev(); DevopsDev devopsDev = new DevopsDev(); androidDev.framework(); androidDev.jetpack(); androidDev.java(); devopsDev.ci2cd(); devopsDev.java(); } } ```

結果:

3.5.4 思考覆盤

接著我們聊聊思考覆盤,思考覆盤分為兩方面,第一方面是介面隔離原則和單一職責原則的區別?第二方面介面隔離原則優點。

介面隔離原則和單一職責原則的區別?

介面隔離原則和單一職責原則的區別有兩個,第一,單一職責原則指的是類、介面和方法的職責是單一的,強調的是職責,也就是說在一個接口裡,只要職責是單一的,有10個方法也是可以的。

第二,介面隔離原則指的是在介面中的方法儘量越來越少,介面隔離原則的前提必須先符合單一職責,在單一職責的前提下,介面儘量是單一介面。

介面隔離原則優點

介面隔離原則優點有三個。

第一,隱藏實現細節

第二,降低耦合性

第三,提高程式碼的可讀性

3.6 最小知識原則

第六個設計原則是最小知識原則,最小知識原則簡稱LOD,正如英文定義的那樣Law of Demeter

,a module should not have knowledge of the inner details of the objects it manipulates 。不該有直接依賴關係的類,不要有依賴;

有依賴關係的類之間,儘量只依賴必要的介面。最小知識原則是希望減少類之間的耦合,讓類越獨立越好,每個類都應該少了解系統的其他部分,一旦發生變化,需要了解這一變化的類就會比較少。

最小知識原則和單一職責的目的都是實現高內聚低耦合,但是出發的角度不一樣,單一職責是從自身提供的功能出發,最小知識原則是從關係出發。

提出問題

如果我們把一個物件看作是一個人,那麼要實現“一個人應該對其他人有最少的瞭解”,做到兩點就足夠了: 第一點,只和直接的朋友交流; 第二點,減少對朋友的瞭解。下面就詳細說說如何做到這兩點。

最小知識原則還有一個英文解釋是:talk only to your immediate friends(只和直接的朋友交流)。

分析問題

什麼是朋友呢?每個物件都必然會與其他的物件有耦合關係,兩個物件之間的耦合就會成為朋友關係。

那麼什麼又是直接的朋友呢?出現在成員變數、方法的輸入輸出引數中的類就是直接的朋友。最小知識原則要求只和直接的朋友通訊。

解決問題

可以參考UML圖例、Good Code、Bad Code和最小知識原則使用原則。

3.6.1 UML圖例

image.png

3.6.2 Bad Code

很簡單的例子:老師讓班長清點全班同學的人數。這個例子中總共有三個類:老師Teacher、班長GroupLeader和學生Student。

在這個例子中,我們的Teacher有幾個朋友?兩個,一個是GroupLeader,它是Teacher的command()方法的入參;另一個是Student,因為在Teacher的command()方法體中使用了Student。

那麼Teacher有幾個是直接的朋友?按照直接的朋友的定義

出現在成員變數、方法的輸入輸出引數中的類就是直接的朋友

只有GroupLeader是Teacher的直接的朋友。

Teacher在command()方法中建立了Student的陣列,和非直接的朋友Student發生了交流,所以,上述例子違反了最小知識原則

方法是類的一個行為,類竟然不知道自己的行為與其他的類產生了依賴關係,這是不允許的,嚴重違反了最小知識原則

```java //--------------------------------------程式碼塊一--------------------------------------- public interface IStudent { } //--------------------------------------程式碼塊二--------------------------------------- public class Student implements IStudent {} //--------------------------------------程式碼塊三--------------------------------------- public interface IGroupLeader { void count(List students); }

//--------------------------------------程式碼塊四--------------------------------------- public interface IGroupLeader { void count(List students); } //--------------------------------------程式碼塊五--------------------------------------- public class GroupLeader implements IGroupLeader{

@Override
public void count(List<Student> students) {
    System.out.println("The number of students attending the class is: " + students.size());
}

} //--------------------------------------程式碼塊六--------------------------------------- public interface ITeacher { void command(IGroupLeader groupLeader); } //--------------------------------------程式碼塊七--------------------------------------- public class Teacher implements ITeacher{ @Override public void command(IGroupLeader groupLeader) { List allStudent = new ArrayList<>(); allStudent.add(new Student()); allStudent.add(new Student()); allStudent.add(new Student()); allStudent.add(new Student()); allStudent.add(new Student()); groupLeader.count(allStudent); } } //--------------------------------------程式碼塊八--------------------------------------- public class Main { public static void main(String[] args) { ITeacher teacher = new Teacher(); teacher.command(new GroupLeader()); } } ```

結果:

3.6.3 Good Code

我們打斷學生和GroupLeader聯絡,直接的聯絡每個類都只和直接的朋友交流,有效減少了類之間的耦合

```java //--------------------------------------程式碼塊一--------------------------------------- public interface IStudent { } //--------------------------------------程式碼塊二--------------------------------------- public class Student implements IStudent {} //--------------------------------------程式碼塊三--------------------------------------- public interface IGroupLeader {

void count();

} //--------------------------------------程式碼塊四--------------------------------------- public class GroupLeader implements IGroupLeader { private List students;

public GroupLeader(List<Student> students) {
    this.students = students;
}

@Override
public void count() {
    System.out.println("The number of students attending the class is: " + students.size());
}

} //--------------------------------------程式碼塊五--------------------------------------- public interface ITeacher { void command(IGroupLeader groupLeader); } //--------------------------------------程式碼塊六--------------------------------------- public class Teacher implements ITeacher { @Override public void command(IGroupLeader groupLeader) { groupLeader.count(); } } //--------------------------------------程式碼塊七--------------------------------------- public class Main { public static void main(String[] args) { ITeacher teacher = new Teacher(); List allStudent = new ArrayList(4);

    allStudent.add(new Student());
    allStudent.add(new Student());
    allStudent.add(new Student());
    allStudent.add(new Student());

    teacher.command(new GroupLeader(allStudent));
}

} ```

結果:

3.6.4 使用原則

最少知識原則的使用原則有6個。

第一,在類的劃分上,應當建立弱耦合的類,類與類之間的耦合越弱,就越有利於實現可複用的目標。 第二,在類的結構設計上,每個類都應該降低成員的訪問許可權。 第三,在類的設計上,只要有可能,一個類應當設計成不變的類。 第四,在對其他類的引用上,一個物件對其他類的物件的引用應該降到最低。 第五,儘量限制區域性變數的有效範圍,降低類的訪問許可權。

第六,謹慎使用Serializable。

3.7 合成複用原則

最後一個原則是合成複用原則。合成複用原則簡稱CARP,正如英文定義的那樣Composite/Aggregate Reuse Principle, try to use composite/aggregate 合成複用原則要求我們在軟體設計的過程中,儘量不要通過繼承方式實現功能和類的一些組合。

因為在 Java 只支援單繼承的, C 、 C ++支援多繼承。所以設計模式在 Java 這一塊的規範,它是不提倡繼承來解決問題的,所以更提倡是合成複用,一個類持有另外一個物件,把能力交給另外的物件去完成。

因為繼承破壞了會繼承複用的和破壞類的一個封裝性,子類和父類耦合度會比較大,因此推薦使用合成複用原則

最小知識原則,如果因為手抖,可能會不小心改了父類,最小知識原則限制複用靈活性,合成複用原則可以維持類的封裝性,降低類與類的耦合度,提高功能的靈活性。

合成複用原則可以將已知的物件和成員變數納入新的物件和成員變數,方法裡邊去呼叫成員變數的具體的功能。就達成了一個合成複用原則。

3.7.1 UML圖例

image.png

3.7.2 Bad Code

汽車從能源的角度來說,分為電動車ETCar和汽油車PCar。

電動車ETCar和汽油車PCar有很多顏色,如: 白色、紅色。

如果後期新增黃色,那麼需要電動車ETCar和汽油車PCar去繼承Car,並讓紅色車RedPCar和白色車WhiteETCar去繼承電動車ETCar和汽油車PCar。繼承的方式可以實現類組合,但缺點是顏色和車型組合越多,類組合會呈N 倍遞,導致類爆炸。

java //--------------------------------------程式碼塊一--------------------------------------- public abstract class Car { public abstract void move(); } //--------------------------------------程式碼塊二--------------------------------------- public abstract class ETCar extends Car{ } //--------------------------------------程式碼塊三--------------------------------------- public abstract class PCar extends Car { } //--------------------------------------程式碼塊四--------------------------------------- public class RedETCar extends Car{ @Override public void move() { System.out.println("Red ETCar is running!"); } } //--------------------------------------程式碼塊五--------------------------------------- public class RedPCar extends PCar { @Override public void move() { System.out.println("Red PCar is running!"); } } //--------------------------------------程式碼塊六--------------------------------------- public class WhiteETCar extends ETCar { @Override public void move() { System.out.println("White ETCar is running!"); } } //--------------------------------------程式碼塊七--------------------------------------- public class WhitePCar extends PCar { @Override public void move() { System.out.println("White PCar is running!"); } } //--------------------------------------程式碼塊八--------------------------------------- public class Main { public static void main(String[] args) { new RedETCar().move(); new RedPCar().move(); new WhitePCar().move(); new WhiteETCar().move(); } }

結果:

3.7.3 Good Code

正確的方式是: 定義一個抽象基類汽車Car。汽車Car分為兩種,一種是油車PCar,一種是電動車ETCar。

因為抽象基類汽車Car合成複用了IColor介面物件,所以子類油車PCar和電動車ETCar可以持有抽象基類Car的IColor介面物件。

因為IColor物件一個介面,介面有多種顏色: 白色、黑色、黃色、綠色、棕等等。

如果每增加一種顏色,那麼實現IColor介面即可,不需要像Bad Code通過繼承方式進行類組合,不但解決了類爆炸的問題,而且解決了繼承帶來的高耦合弊端。因此,在類組合問題上,我們可以利用合成複用原則解決程式碼冗餘問題。

```java //--------------------------------------程式碼塊一--------------------------------------- public interface IColor { String getName(); } //--------------------------------------程式碼塊二--------------------------------------- public class RedColor implements IColor { @Override public String getName() { return "Red"; } } //--------------------------------------程式碼塊三--------------------------------------- public class WhiteColor implements IColor { @Override public String getName() { return "White"; } } //--------------------------------------程式碼塊四--------------------------------------- public abstract class Car { private IColor color; public abstract void move();

public IColor getColor() {
    return color;
}

public Car setColor(IColor color) {
    this.color = color;
    return this;
}

} //--------------------------------------程式碼塊五--------------------------------------- public class PCar extends Car { @Override public void move() { System.out.println(getColor().getName() + " "+PCar.class.getSimpleName() +" is running!" ); } } //--------------------------------------程式碼塊六--------------------------------------- public class ETCar extends Car { @Override public void move() { System.out.println(getColor().getName() + " "+PCar.class.getSimpleName() +" is running!" ); } } //--------------------------------------程式碼塊七--------------------------------------- public class Main { public static void main(String[] args) { PCar pCar = new PCar(); ETCar etCar = new ETCar();

    RedColor redColor = new RedColor();
    WhiteColor whiteColor = new WhiteColor();

    pCar.setColor(redColor).move();
    pCar.setColor(whiteColor).move();

    etCar.setColor(redColor).move();
    etCar.setColor(whiteColor).move();
}

} ```

結果:

3.7.4 思考覆盤

組合和聚合到底有什麼區別呢?

聚合關係的類裡有另外一個類作為引數。BirdGroup類被gc之後,bird類的引用依然建在。這就是聚合。

java public class BirdGroup{ public Bird bird; public BirdGroup(Bird bird){ this.bird = bird; } }

組合關係的類裡有另外一個類的例項化,如果Bird這個類被GC了,內部的類的引用,隨之消失了,這就是組合。

java public class Bird{ public Wings wings; public Bird(){ wings = new Wings () ; } }

合成複用原則的優點

使系統更加靈活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較小。

合成複用原則的缺點

破壞了包裝,同時包含的類的實現細節被隱藏。

好了,七大設計原則到現在已經說完了,我們簡單的總結一下:

image.png

如果大家覺的上面表格比較複雜,那麼用七句話總結就是:

單一職責原則告訴我們實現類要職責單一;

里氏替換原則告訴我們不要破壞繼承體系;

依賴倒置原則告訴我們要面向介面程式設計;

介面隔離原則告訴我們在設計介面的時候要精簡單一;

最小知識原則告訴我們要降低耦合;

合成複用原則告訴我們不要通過繼承方式實現功能和類組合;

而開閉原則是總綱,告訴我們要對擴充套件開放,對修改關閉。

四、3大設計模式

說完七大設計原則,我們再說說3大設計模式,設計模式一般分為三種,第一種是建立型模式,第二種是結構型模式,第三種是行為型模式。

當我們關注類的物件,比如如何孵化出來類的物件?如何建立類的物件?如何new出來類的物件?如何維護類的物件關係?我們就需要使用到建立型模式。

當我們關注類與類之間的關係,如 A 跟 B 類組合或生產關係的時候。我們就需要使用到結構型模式。

當我們關注類某一個方法功能的一個實現,我們就需要使用到行為型模式。

建立型模式、結構型模式和行為型模式又分為23 種,由於篇幅有限,今天主要講解建立型模式的建造者設計模式,結構型模式的介面卡設計模式,行為型模式的策略設計模式和模板方法設計模式。剩餘19種設計模式,小木箱將在後續文章進行講解和梳理。

4.1 建立型模式

建立型模式本質上是處理類的例項化,封裝了具體類的資訊和隱藏了類的例項化過程。今天主要講解建造者設計模式

4.1.1 建造者設計模式

4.1.1.1 定義

建造者模式所完成的內容就是通過將多個簡單物件通過一步步的組裝構建出一個複雜物件的過程。

建造者設計模式滿足了單一職責原則以及可複用的技術、建造者獨立、易擴充套件、便於控制細節風險。

但同時當出現特別多的物料以及很多的組合後,類的不斷擴充套件也會造成難以維護的問題。

建造者設計模式可以把重複的內容抽象到資料庫中,按照需要配置。這樣就可以減少程式碼中大量的重複。

4.1.1.2 B站視訊

《重學Java設計模式》第6章:建造者模式

4.1.1.3 Bad Code

這裡我們模擬裝修公司對於設計出一些套餐裝修服務的場景。

很多裝修公司都會給出自家的套餐服務,一般有;歐式豪華、輕奢田園、現代簡約等等,而這些套餐的後面是不同的商品的組合。例如;一級&二級吊頂、多樂士塗料、聖象地板、馬可波羅地磚等等,按照不同的套餐的價格選取不同的品牌組合,最終再按照裝修面積給出一個整體的報價。

這裡我們就模擬裝修公司想推出一些套餐裝修服務,按照不同的價格設定品牌選擇組合,以達到使用建造者模式的過程。

在模擬工程中提供了裝修中所需要的物料;ceilling(吊頂)coat(塗料)floor(地板)tile(地磚),這麼四項內容。(實際的裝修物料要比這個多的多

4.1.1.3.1 程式碼結構

  • 物料介面: Matter

    • 物料介面提供了基本的資訊,以保證所有的裝修材料都可以按照統一標準進行獲取。

```java public interface Matter {

String scene();      // 場景;地板、地磚、塗料、吊頂

String brand();      // 品牌

String model();      // 型號

BigDecimal price();  // 價格

String desc();       // 描述

} ```

  • 吊頂(ceiling)

    • 一級頂: LevelOneCeiling

```java public class LevelOneCeiling implements Matter {

        public String scene() {
            return "吊頂";
        }

        public String brand() {
            return "裝修公司自帶";
        }

        public String model() {
            return "一級頂";
        }

        public BigDecimal price() {
            return new BigDecimal(260);
        }

        public String desc() {
            return "造型只做低一級,只有一個層次的吊頂,一般離頂120-150mm";
        }

    }

```

  • 二級頂: LevelTwoCeiling

``` public class LevelTwoCeiling implements Matter {

public String scene() {
    return "吊頂";
}

public String brand() {
    return "裝修公司自帶";
}

public String model() {
    return "二級頂";
}

public BigDecimal price() {
    return new BigDecimal(850);
}

public String desc() {
    return "兩個層次的吊頂,二級吊頂高度一般就往下吊20cm,要是層高很高,也可增加每級的厚度";
}

} ```

  • 塗料(coat)

    • 多樂士: DuluxCoat

```java public class DuluxCoat implements Matter {

public String scene() {
    return "塗料";
}

public String brand() {
    return "多樂士(Dulux)";
}

public String model() {
    return "第二代";
}

public BigDecimal price() {
    return new BigDecimal(719);
}

public String desc() {
    return "多樂士是阿克蘇諾貝爾旗下的著名建築裝飾油漆品牌,產品暢銷於全球100個國家,每年全球有5000萬戶家庭使用多樂士油漆。";
}

} ```

  • 立邦: LiBangCoat

```java public class LiBangCoat implements Matter {

public String scene() {
    return "塗料";
}

public String brand() {
    return "立邦";
}

public String model() {
    return "預設級別";
}

public BigDecimal price() {
    return new BigDecimal(650);
}

public String desc() {
    return "立邦始終以開發綠色產品、注重高科技、高品質為目標,以技術力量不斷推進科研和開發,滿足消費者需求。";
}

} ```

  • 地板(floor)

    • 德爾

```java public class DerFloor implements Matter {

public String scene() {
    return "地板";
}

public String brand() {
    return "德爾(Der)";
}

public String model() {
    return "A+";
}

public BigDecimal price() {
    return new BigDecimal(119);
}

public String desc() {
    return "DER德爾集團是全球領先的專業木地板製造商,北京2008年奧運會家裝和公裝地板供應商";
}

} ```

  • 聖象

``` public class ShengXiangFloor implements Matter {

public String scene() {
    return "地板";
}

public String brand() {
    return "聖象";
}

public String model() {
    return "一級";
}

public BigDecimal price() {
    return new BigDecimal(318);
}

public String desc() {
    return "聖象地板是中國地板行業著名品牌。聖象地板擁有中國馳名商標、中國名牌、國家免檢、中國環境標誌認證等多項榮譽。";
}

} ```

  • 地磚(tile)

```java public class DongPengTile implements Matter {

public String scene() {
    return "地磚";
}

public String brand() {
    return "東鵬瓷磚";
}

public String model() {
    return "10001";
}

public BigDecimal price() {
    return new BigDecimal(102);
}

public String desc() {
    return "東鵬瓷磚以品質鑄就品牌,科技推動品牌,口碑傳播品牌為宗旨,2014年品牌價值132.35億元,位列建陶行業榜首。";
}

} ```

  • 馬可波羅

```java public class MarcoPoloTile implements Matter {

public String scene() {
    return "地磚";
}

public String brand() {
    return "馬可波羅(MARCO POLO)";
}

public String model() {
    return "預設";
}

public BigDecimal price() {
    return new BigDecimal(140);
}

public String desc() {
    return "“馬可波羅”品牌誕生於1996年,作為國內最早品牌化的建陶品牌,以“文化陶瓷”佔領市場,享有“仿古磚至尊”的美譽。";
}

} ```

以上就是本次裝修公司所提供的裝修配置單,接下我們會通過案例去使用不同的物料組合出不同的套餐服務。

```java public class DecorationPackageController {

public String getMatterList(BigDecimal area, Integer level) {

    List<Matter> list = new ArrayList<Matter>(); // 裝修清單
    BigDecimal price = BigDecimal.ZERO;          // 裝修價格

    // 豪華歐式
    if (1 == level) {

        LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊頂,二級頂
        DuluxCoat duluxCoat = new DuluxCoat();                   // 塗料,多樂士
        ShengXiangFloor shengXiangFloor = new ShengXiangFloor(); // 地板,聖象

        list.add(levelTwoCeiling);
        list.add(duluxCoat);
        list.add(shengXiangFloor);

        price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
        price = price.add(area.multiply(new BigDecimal("1.4")).multiply(duluxCoat.price()));
        price = price.add(area.multiply(shengXiangFloor.price()));

    }

    // 輕奢田園
    if (2 == level) {

        LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊頂,二級頂
        LiBangCoat liBangCoat = new LiBangCoat();                // 塗料,立邦
        MarcoPoloTile marcoPoloTile = new MarcoPoloTile();       // 地磚,馬可波羅

        list.add(levelTwoCeiling);
        list.add(liBangCoat);
        list.add(marcoPoloTile);

        price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
        price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
        price = price.add(area.multiply(marcoPoloTile.price()));

    }

    // 現代簡約
    if (3 == level) {

        LevelOneCeiling levelOneCeiling = new LevelOneCeiling();  // 吊頂,二級頂
        LiBangCoat liBangCoat = new LiBangCoat();                 // 塗料,立邦
        DongPengTile dongPengTile = new DongPengTile();           // 地磚,東鵬

        list.add(levelOneCeiling);
        list.add(liBangCoat);
        list.add(dongPengTile);

        price = price.add(area.multiply(new BigDecimal("0.2")).multiply(levelOneCeiling.price()));
        price = price.add(area.multiply(new BigDecimal("1.4")).multiply(liBangCoat.price()));
        price = price.add(area.multiply(dongPengTile.price()));
    }

    StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
            "裝修清單" + "\r\n" +
            "套餐等級:" + level + "\r\n" +
            "套餐價格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
            "房屋面積:" + area.doubleValue() + " 平米\r\n" +
            "材料清單:\r\n");

    for (Matter matter: list) {
        detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米價格:").append(matter.price()).append(" 元。\n");
    }

    return detail.toString();

}

} ```

  • 測試入口: Main

```java

public class Main { public static void main(String[] args) {

DecorationPackageController decoration = new DecorationPackageController();
// 豪華歐式
System.out.println(decoration.getMatterList(new BigDecimal("132.52"),1));
// 輕奢田園
System.out.println(decoration.getMatterList(new BigDecimal("98.25"),2));
// 現代簡約
System.out.println(decoration.getMatterList(new BigDecimal("85.43"),3));

}

} ```

總結:

  1. 首先這段程式碼所要解決的問題就是接收入參;裝修面積(area)、裝修等級(level),根據不同型別的裝修等級選擇不同的材料。
  2. 其次在實現過程中可以看到每一段if塊裡,都包含著不同的材料(吊頂,二級頂、塗料,立邦、地磚,馬可波羅),最終生成裝修清單和裝修成本。
  3. 最後提供獲取裝修詳細資訊的方法,返回給呼叫方,用於知道裝修清單。

4.1.1.3.2 輸出結果

```java

裝修清單 套餐等級:1 套餐價格:198064.39 元 房屋面積:132.52 平米 材料清單: 吊頂:裝修公司自帶、二級頂、平米價格:850 元。 塗料:多樂士(Dulux)、第二代、平米價格:719 元。 地板:聖象、一級、平米價格:318 元。


裝修清單 套餐等級:2 套餐價格:119865.00 元 房屋面積:98.25 平米 材料清單: 吊頂:裝修公司自帶、二級頂、平米價格:850 元。 塗料:立邦、預設級別、平米價格:650 元。 地磚:馬可波羅(MARCO POLO)、預設、平米價格:140 元。


裝修清單 套餐等級:3 套餐價格:90897.52 元 房屋面積:85.43 平米 材料清單: 吊頂:裝修公司自帶、一級頂、平米價格:260 元。 塗料:立邦、預設級別、平米價格:650 元。 地磚:東鵬瓷磚、10001、平米價格:102 元。 ```

4.1.1.4 Good Code

工程結構

python ├── Builder.java ├── DecorationPackageMenu.java ├── IMenu.java ├── Main.java ├── ceiling │   ├── LevelOneCeiling.java │   ├── LevelTwoCeiling.java │   └── Matter.java ├── coat │   ├── DuluxCoat.java │   └── LiBangCoat.java ├── floor │   ├── DerFloor.java │   └── ShengXiangFloor.java └── tile ├── DongPengTile.java └── MarcoPoloTile.java

建造者模型結構

工程中有三個核心類和一個測試類,核心類是建造者模式的具體實現。與ifelse實現方式相比,多出來了兩個二外的類。具體功能如下;

  • Builder,建造者類具體的各種組裝由此類實現。
  • DecorationPackageMenu,是IMenu介面的實現類,主要是承載建造過程中的填充器。相當於這是一套承載物料和建立者中間銜接的內容。

好,那麼接下來會分別講解幾個類的具體實現

定義裝修包介面

```java public interface IMenu {

IMenu appendCeiling(Matter matter); // 吊頂

IMenu appendCoat(Matter matter);    // 塗料

IMenu appendFloor(Matter matter);   // 地板

IMenu appendTile(Matter matter);    // 地磚

String getDetail();                 // 明細

} ```

  • 介面類中定義了填充各項物料的方法;吊頂塗料地板地磚,以及最終提供獲取全部明細的方法。

裝修包實現

```java public class DecorationPackageMenu implements IMenu {

private List<Matter> list = new ArrayList<Matter>();  // 裝修清單
private BigDecimal price = BigDecimal.ZERO;      // 裝修價格

private BigDecimal area;  // 面積
private String grade;     // 裝修等級;豪華歐式、輕奢田園、現代簡約

private DecorationPackageMenu() {
}

public DecorationPackageMenu(Double area, String grade) {
    this.area = new BigDecimal(area);
    this.grade = grade;
}

public IMenu appendCeiling(Matter matter) {
    list.add(matter);
    price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
    return this;
}

public IMenu appendCoat(Matter matter) {
    list.add(matter);
    price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
    return this;
}

public IMenu appendFloor(Matter matter) {
    list.add(matter);
    price = price.add(area.multiply(matter.price()));
    return this;
}

public IMenu appendTile(Matter matter) {
    list.add(matter);
    price = price.add(area.multiply(matter.price()));
    return this;
}

public String getDetail() {

    StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n" +
            "裝修清單" + "\r\n" +
            "套餐等級:" + grade + "\r\n" +
            "套餐價格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元\r\n" +
            "房屋面積:" + area.doubleValue() + " 平米\r\n" +
            "材料清單:\r\n");

    for (Matter matter: list) {
        detail.append(matter.scene()).append(":").append(matter.brand()).append("、").append(matter.model()).append("、平米價格:").append(matter.price()).append(" 元。\n");
    }

    return detail.toString();
}

} ```

  • 裝修包的實現中每一個方法都會了 this,也就可以非常方便的用於連續填充各項物料。
  • 同時在填充時也會根據物料計算平米數下的報價,吊頂和塗料按照平米數適量乘以常數計算。
  • 最後同樣提供了統一的獲取裝修清單的明細方法。

建造者方法

```java public class Builder {

public IMenu levelOne(Double area) {
    return new DecorationPackageMenu(area, "豪華歐式")
            .appendCeiling(new LevelTwoCeiling())    // 吊頂,二級頂
            .appendCoat(new DuluxCoat())             // 塗料,多樂士
            .appendFloor(new ShengXiangFloor());     // 地板,聖象
}

public IMenu levelTwo(Double area){
    return new DecorationPackageMenu(area, "輕奢田園")
            .appendCeiling(new LevelTwoCeiling())   // 吊頂,二級頂
            .appendCoat(new LiBangCoat())           // 塗料,立邦
            .appendTile(new MarcoPoloTile());       // 地磚,馬可波羅
}

public IMenu levelThree(Double area){
    return new DecorationPackageMenu(area, "現代簡約")
            .appendCeiling(new LevelOneCeiling())   // 吊頂,二級頂
            .appendCoat(new LiBangCoat())           // 塗料,立邦
            .appendTile(new DongPengTile());        // 地磚,東鵬
}

} ```

測試方法:

java @Test public void test_Builder(){ Builder builder = new Builder(); // 豪華歐式 System.out.println(builder.levelOne(132.52D).getDetail()); // 輕奢田園 System.out.println(builder.levelTwo(98.25D).getDetail()); // 現代簡約 System.out.println(builder.levelThree(85.43D).getDetail()); }

結果:

```java

裝修清單 套餐等級:豪華歐式 套餐價格:198064.39 元 房屋面積:132.52 平米 材料清單: 吊頂:裝修公司自帶、二級頂、平米價格:850 元。 塗料:多樂士(Dulux)、第二代、平米價格:719 元。 地板:聖象、一級、平米價格:318 元。


裝修清單 套餐等級:輕奢田園 套餐價格:119865.00 元 房屋面積:98.25 平米 材料清單: 吊頂:裝修公司自帶、二級頂、平米價格:850 元。 塗料:立邦、預設級別、平米價格:650 元。 地磚:馬可波羅(MARCO POLO)、預設、平米價格:140 元。


裝修清單 套餐等級:現代簡約 套餐價格:90897.52 元 房屋面積:85.43 平米 材料清單: 吊頂:裝修公司自帶、一級頂、平米價格:260 元。 塗料:立邦、預設級別、平米價格:650 元。 地磚:東鵬瓷磚、10001、平米價格:102 元 ```

  • 測試結果是一樣的,呼叫方式也基本類似。但是目前的程式碼結構卻可以讓你很方便的很有調理的進行擴充套件業務開發。而不是像以往一樣把所有程式碼都寫到ifelse裡面。
4.1.1.5 Source Code

建造者不拘泥於形式,建造者模式用於建立一個複雜物件。在android中,Dialog就用到了建造者模式,第三方庫的okhttp、Retrofit等

```java public class Dialog { String title; boolean mCancelable = false;

Dialog(String title,boolean mCanclable){
    this.title = title;
    this.mCancelable = mCanclable;
}

public void show() {
    System.out.print("show");
}

static class Builder{
    String title;
    boolean mCancelable  = false;

    public Builder setCancelable(boolean flag) {
        mCancelable = flag;
        return this;
    }

    public Builder setTitle(String title) {
        this.title = title;
        return this;
    }

    public Dialog build(){
        return new Dialog(this.title,this.mCancelable);
    }
}

} ```

4.1.1.6 注意事項

優點:

客戶端不比知道產品內部細節,將產品本身與產品建立過程解耦,使得相同的建立過程可以建立不同的產品物件可以更加精細地控制產品的建立過程,將複雜物件分門別類抽出不同的類別來,使得開發者可以更加方便地得到想要的產品

缺點:

產品屬性之間差異很大且屬性沒有預設值可以指定,這種情況是沒法使用建造者模式的,我們可以試想,一個物件20個屬性,彼此之間毫無關聯且每個都需要手動指定,那麼很顯然,即使使用了建造者模式也是毫無作用

4.2 結構型模式

建立型模式本質上是處理類或物件的組合,常見的結構模型有類結構型和物件結構型。今天主要講解介面卡設計模式

4.2.1 介面卡設計模式
4.1.1.1 定義

介面卡模式把一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起工作的兩個類能夠在一起工作,是作為兩個不相容的介面之間的橋樑。

這種型別的設計模式屬於結構型模式,它結合了兩個獨立介面的功能,介面卡分為類介面卡和物件介面卡.

主要解決在軟體系統中,常常要將一些"現存的物件"放到新的環境中,而新環境要求的介面是現物件不能滿足的;

4.1.1.2 UML圖例

image.png

4.1.1.3 類介面卡

類介面卡是通過類的繼承來實現的。Adpater直接繼承了Target和Adaptee中的所有方法,並進行改寫,從而實現了Target中的方法。

類介面卡的缺點就是必須實現Target和Adaptee中的方法,由於Java不支援多繼承,所以通常將Target設計成介面,Adapter繼承自Adaptee然後實現Target介面。使用類介面卡的方式來實現一下上邊的用雄蜂來冒充鴨子。

我們可以看到下面的案例雄蜂(Drone)具有蜂鳴聲(beep)、轉子旋轉(spin_rotors)和起飛(take_off)行為,鴨子Duck具有嘎嘎叫(quack)和飛(fly)行為

那麼如何找到一個介面卡讓雄蜂(Drone)的蜂鳴聲beep和鴨子(Duck)的嘎嘎叫(quack)適配呢

又如何找到一個介面卡讓鴨子(鴨子)飛(fly)和雄蜂(Drone)的轉子旋轉(spin_rotors)、起飛(take_off)適配呢?

很顯然雄蜂介面卡(DroneAdapter)嘎嘎叫(quack)可以適配雄蜂(Drone)蜂鳴聲(beep)

雄蜂介面卡(DroneAdapter)嘎嘎叫(fly)也可以適配雄蜂(Drone)轉子旋轉(spin_rotors)和起飛(take_off)

```java //--------------------------------------程式碼塊一--------------------------------------- public interface Drone { void beep(); void spin_rotors(); void take_off(); } //--------------------------------------程式碼塊二--------------------------------------- public class SuperDrone implements Drone { public void beep() { System.out.println("Beep beep beep"); } public void spin_rotors() { System.out.println("Rotors are spinning"); } public void take_off() { System.out.println("Taking off"); } } //--------------------------------------程式碼塊三--------------------------------------- public interface Duck { public void quack(); public void fly(); } //--------------------------------------程式碼塊四--------------------------------------- public class DroneAdapter implements Duck { Drone drone;

public DroneAdapter(Drone drone) { this.drone = drone; }

public void quack() { drone.beep(); }

public void fly() { drone.spin_rotors(); drone.take_off(); } } //--------------------------------------程式碼塊五--------------------------------------- public class DuckTestDrive { public static void main(String[] args) { Drone drone = new SuperDrone(); Duck droneAdapter = new DroneAdapter(drone); droneAdapter.quack(); droneAdapter.fly(); } } ```

結果:

4.1.1.4 物件介面卡

物件介面卡是使用組合的方法,在Adapter中會保留一個原物件(Adaptee)的引用,介面卡的實現就是講Target中的方法委派給Adaptee物件來做,用Adaptee中的方法實現Target中的方法

物件介面卡的好處就是,Adpater只需要實現Target中的方法就好啦。現在我們通過一個用火雞冒充鴨子的例子來看看如何使用介面卡模式。

火雞(Turkey)具備火雞叫(gobble)和飛(fly)行為,鴨子(Duck)具備嘎嘎叫(quack)和飛(fly)的行為,找一個火雞介面卡(TurkeyAdapter)讓鴨子(Duck)的嘎嘎叫(quack)適配火雞(Turkey)的火雞叫(gobble).讓鴨子(Duck)的飛(fly)適配火雞(Turkey)的飛(fly),只要把火雞(Turkey)的物件傳給火雞介面卡(TurkeyAdapter)即可.不改變野火雞(WildTurkey)火雞叫(gobble)和飛(fly)的行為.同時,不改變綠頭鴨(MallardDuck)的嘎嘎叫(quack) 和飛(fly)的行為.

```java //--------------------------------------程式碼塊一--------------------------------------- public interface Duck { public void quack(); public void fly(); } //--------------------------------------程式碼塊二--------------------------------------- public interface Turkey { public void gobble(); public void fly(); } //--------------------------------------程式碼塊三--------------------------------------- public class TurkeyAdapter implements Duck { Turkey turkey;

public TurkeyAdapter(Turkey turkey) { this.turkey = turkey; }

public void quack() { turkey.gobble(); }

public void fly() { turkey.fly(); } } //--------------------------------------程式碼塊四--------------------------------------- public class WildTurkey implements Turkey { public void gobble() { System.out.println("Gobble gobble"); }

public void fly() { System.out.println("I'm flying a short distance"); } } //--------------------------------------程式碼塊五--------------------------------------- public class MallardDuck implements Duck { public void quack() { System.out.println("Quack"); }

public void fly() { System.out.println("I'm flying"); } } //--------------------------------------程式碼塊六--------------------------------------- public class DuckTestDrive { public static void main(String[] args) { Duck duck = new MallardDuck();

    Turkey turkey = new WildTurkey();
    Duck turkeyAdapter = new TurkeyAdapter(turkey);

    System.out.println("The Turkey says...");
    turkey.gobble();
    turkey.fly();

    System.out.println("\nThe Duck says...");
    testDuck(duck);

    System.out.println("\nThe TurkeyAdapter says...");
    testDuck(turkeyAdapter);
}

static void testDuck(Duck duck) {
    duck.quack();
    duck.fly();
}

} ```

鴨子和火雞有相似之處,他們都會飛,雖然飛的不遠,他們不太一樣的地方就是叫聲不太一樣,現在我們有一個火雞的類,有鴨子的抽象類也就是介面。

我們的介面卡繼承自鴨子類並且保留了火雞的引用,重寫鴨子的飛和叫的方法,但是是委託給火雞的方法來實現的。在客戶端中,我們給介面卡傳遞一個火雞的物件,就可以把它當做鴨子來使用了。

結果:

4.1.1.5 Source Code

介面卡模式可以用繼承實現,這裡沒有更高的抽象,當然也可以把Adapter的內容抽象出去,僅僅演示,ListView、GridView適配了Adapter類。

```java //定義介面卡類 public class Adapter { public void getView(int i){ System.out.println("給出View"+i); } } //ListView 繼承了Adapter public class ListView extends Adapter{

public void show(){
    System.out.print("迴圈顯示View");
    for(int i=0;i<3;i++){
        getView(i);
    }
}

} //GridView繼承了Adapter public class GridView extends Adapter{

public void show(){
   ...
   getView(i);
}

} ```

在android中,ListView、RecyclerView都是用了介面卡模式,ListView適配了Adapter,ListView只管ItemView,不管具體怎麼展示,Adapter只管展示。就像讀卡器,讀卡器作為記憶體和電腦之間的介面卡。

4.1.1.6 注意事項

介面卡模式的優點:

  1. 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,而無須修改原有程式碼。
  2. 增加了類的透明性和複用性,將具體的實現封裝在適配者類中,對於客戶端類來說是透明的,而且提高了適配者的複用性。
  3. 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合“開閉原則”。

介面卡模式的缺點:

  1. 過多地使用介面卡,會讓系統非常零亂,不易整體進行把握。比如,明明看到呼叫的是 A 介面,其實內部被適配成了 B 介面的實現,一個系統如果太多出現這種情況,無異於一場災難。因此如果不是很有必要,可以不使用介面卡,而是直接對系統進行重構。
  2. 由於 JAVA 至多繼承一個類,所以至多隻能適配一個適配者類,而且目標類必須是抽象類。
  3. 一次最多隻能適配一個適配者類,不能同時適配多個適配者。
  4. 目標抽象類只能為介面,不能為類,其使用有一定的侷限性;

介面卡模式的使用時機:

  1. 在實際的開發過程中,一個介面有大量的方法,但是對應的不同類只需要關注部分方法,其他無關的方法全都實現過於繁瑣,尤其是涉及的實現類過多的情況。
  2. 想要建立一個可以重複使用的類,用於與一些彼此之間沒有太大關聯的一些類,包括一些可能在將來引進的類一起工作。

如: 現有一個需要的目標介面物件 Target,定義了大量相關的方法。但是在實際使用過程只需分別關注其中部分方法,而不是全部實現。在此場景中:被依賴的目標物件TargetObj、介面卡Adapter、客戶端Client等

```java // 目標物件:定義了大量的相關方法 public interface TargetObj { void operation1(); void operation2(); void operation3(); void operation4(); void operation5(); }

// 介面卡:將目標介面定義的方法全部做預設實現 public abstract class Adapter implements TargetObj { void operation1(){} void operation2(){} void operation3(){} void operation4(){} void operation5(){} }

// 客戶端:採用匿名內部類的方式實現需要的介面即可完成適配 public class Client {

public static void main(String[] args) {
    Adapter adapter1 = new Adapter() {
        @Override
        public void operation3() {
        // 僅僅實現需要關注的方法即可
        System.out.println("operation3")
        }
    }

    Adapter adapter2 = new Adapter() {
        @Override
        public void operation5() {
        // 僅僅實現需要關注的方法即可
        System.out.println("operation5")
        }
    }

    adapter1.operation3();
    adapter2.operation5();

}

} ```

4.3 行為型模式

4.3.1 策略設計模式
4.1.1.1 定義

策略模式定義是一系列封裝起來的一種演算法,讓演算法與演算法之間可以相互替換。策略模式把演算法委託於使用者,策略模式可以獨立變化。

比如我們要去某個地方,會根據距離的不同(或者是根據手頭經濟狀況)來選擇不同的出行方式(共享單車、坐公交、滴滴打車等等),這些出行方式即不同的策略。

再比如活動促銷,打 9 折、打 3 折、打 7 折還是打 8 折?涉及具體的策略選擇時候,讓使用者選擇,使用者只關心對演算法的封裝,我怎麼樣去實現演算法。使用者不需要管。下面我們就用策略設計模式實現一個圖書購買系統.

4.1.2.2 Code Case

在一個圖書購買系統中,主要由一些幾種不同的折扣:

折扣一(NoDiscountStrategy):對有些圖書沒有折扣。折扣演算法物件返還0作為折扣值。

折扣二(FlatRateStrategy):對有些圖書提供一個固定量值為1元的折扣。

折扣三(PercentageStrategy):對有些圖書提供一個百分比的折扣,比如本書價格為 20元,折扣百分比為7%,那麼折扣值就是20×7%=1.4(元)。

image.png

```java //--------------------------------------程式碼塊一--------------------------------------- public class Book { private String name; private DiscountStrategy strategy;

public Book(String name, DiscountStrategy strategy) {
    this.name = name;
    this.strategy = strategy;
}

public void setStrategy(DiscountStrategy strategy) {
    this.strategy = strategy;
}

public void getDiscount(){
    System.out.println("book name:"+ name + " ,the discount algorithm is: "+ strategy.getClass().getSimpleName()+",the discounted price is: " + strategy.calcDiscount());
}

} //--------------------------------------程式碼塊二--------------------------------------- public abstract class DiscountStrategy { private double price = 0; private int copies;

public DiscountStrategy() {}

public DiscountStrategy(double price, int copies) {
    this.price = price;
    this.copies = copies;
}

abstract double calcDiscount();

public double getPrice() {
    return price;
}

public int getCopies() {
    return copies;
}

} //--------------------------------------程式碼塊三--------------------------------------- public class FlatRateStrategy extends DiscountStrategy{

private int discountPrice;
public FlatRateStrategy(double price, int copies) {
    super(price,copies);
}

public void setDiscountPrice(int discountPrice) {
    this.discountPrice = discountPrice;
}

@Override
double calcDiscount() {
    return discountPrice * getCopies();
}

} //--------------------------------------程式碼塊四--------------------------------------- public class NoDiscountStrategy extends DiscountStrategy{ @Override double calcDiscount() { return 0; } } //--------------------------------------程式碼塊五--------------------------------------- public class PercentageStrategy extends DiscountStrategy{ private double discountPercent; public PercentageStrategy(double price, int copies) { super(price, copies); } public void setDiscountPercent(double discountPercent) { this.discountPercent = discountPercent; } @Override double calcDiscount() { return getCopies() * getPrice() * discountPercent; } } //--------------------------------------程式碼塊六--------------------------------------- public class Client {

public static void main(String[] args) {
    Book book1 = new Book("java design pattern", new NoDiscountStrategy());
    book1.getDiscount();

    FlatRateStrategy rateStrategy = new FlatRateStrategy(23.0, 5);
    rateStrategy.setDiscountPrice(1);
    Book book2 = new Book("java design pattern",rateStrategy);
    book2.getDiscount();

    System.out.println("Revise《java design pattern》discount algorithm\n:");
    PercentageStrategy percentageStrategy = new PercentageStrategy(23, 5);
    percentageStrategy.setDiscountPercent(0.07);
    book2.setStrategy(percentageStrategy);
    book2.getDiscount();
}

} ```

結果:

4.1.2.3 Android Code

Android中RecyclerView的例子,我們給RecyclerView選擇佈局方式的時候,就是選擇的策略模式

```java //假如RecyclerView 這樣寫 public class RecyclerView {

private Layout layout;

public void setLayout(Layout layout) {
    this.layout = layout;

    if(layout == "橫著"){

    }else if(layout == "豎著"){

    }else if(layout=="格子"){

    }else{

    }  
    this.layout.doLayout();
}

} //這樣寫if就很多了 //排列的方式 public interface Layout { void doLayout(); } //豎著排列 public class LinearLayout implements Layout{ @Override public void doLayout() { System.out.println("LinearLayout"); } } //網格排列 public class GridLayout implements Layout{ @Override public void doLayout() { System.out.println("GridLayout"); } } public class RecyclerView { private Layout layout;

public void setLayout(Layout layout) {
    this.layout = layout;
    this.layout.doLayout();
}

} ```

當然Android的原始碼裡面動畫時間插值器,用的也是策略設計模式,程式碼就不貼了,大家可以結合原始碼和Android設計模式之策略模式在專案中的實際使用總結文章中的UML圖進行學習.

4.1.2.4 注意事項

為什麼要用策略設計模式?

比如我們有微信支付,有支付寶支付,還有銀聯支付和招商支付。如果邏輯都通過 if else 實現,那麼 if-else 塊中的程式碼量比較大時候,後續程式碼的擴充套件和維護就會逐漸變得非常困難且容易出錯,就算使用Switch也同樣違反了:

if (微信支付) { // 邏輯1 } else if (支付寶支付) { // 邏輯2 } else if (銀聯支付) { // 邏輯3 } else if(招商支付){ // 邏輯4 }else{ // 邏輯5 }

單一職責原則(一個類應該只有一個發生變化的原因):因為之後修改任何一個邏輯,當前類都會被修改

開閉原則(對擴充套件開放,對修改關閉):如果此時需要新增(刪除)某個邏輯,那麼不可避免的要修改原來的程式碼

什麼時候使用策略設計模式?

  1. 如果在一個系統裡面有許多類,它們之間的區別僅在於它們的行為,那麼使用策略模式可以動態地讓一個物件在許多行為中選擇一種行為。 一個系統需要動態地在幾種演算法中選擇一種。
  2. 如果一個物件有很多的行為,如果不用恰當的模式,這些行為就只好使用多重的條件選擇語句來實現。
  3. 不希望客戶端知道複雜的、與演算法相關的資料結構,在具體策略類中封裝演算法和相關的資料結構,提高演算法的保密性與安全性。

策略模式的優缺點是什麼?

優點:

  • 策略模式提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。
  • 策略模式提供了管理相關的演算法族的辦法。
  • 策略模式提供了可以替換繼承關係的辦法。
  • 使用策略模式可以避免使用多重條件轉移語句。

缺點:

  • 客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。
  • 策略模式將造成產生很多策略類,可以通過使用享元模式在一定程度上減少物件的數量。
4.3.2 模板方法設計模式
4.3.2.1 定義

模版模式是說對一個執行過程進行抽象分解,通過骨架和擴充套件方法完成一個標準的主體邏輯和擴充套件。我們很多時候,做監控平臺也都是這樣的:對過程進行標準化,對變化進行定義,形成一個平臺邏輯和業務擴充套件,完成一個產品模版。

4.3.2.2 UML 圖例

通過以下AbstractClass模板類我們可以看出來,PrivitiveOperation1()和PrivitiveOperation2()全部封裝在TemplateMethod()抽象方法裡面,TemplateMethod()抽象方法父類控制執行順序,子類負責實現即可。通過封裝不變部分,擴充套件可變部分和提取公共部分程式碼,便於維護和可拓展性。

提出問題

小木箱準備煮茶和煮咖啡,煮茶的步驟有燒水、泡茶、加檸檬、倒水四個步驟,而煮咖啡的步驟有燒水、過濾咖啡、倒水、加牛奶四個步驟,請在控制檯列印煮茶和煮咖啡的執行流程。

分析問題

煮茶和煮咖啡的步驟中燒水和倒水動作是重複的,能不能抽取成模板方法呢?

解決問題

可以參考UML圖例、Good Code、Bad Code和模板方法設計模式原始碼分析。

image.png

4.3.2.3 Bad Code

錯誤的編碼方式:將煮茶的步驟燒水→泡茶→倒水→加檸檬按順序執行,煮咖啡的步驟燒水→過濾咖啡→倒水→加牛奶也按順序執行,這樣的缺點是如果步驟很多,那麼程式碼顯得比較臃腫,程式碼維護成本也會越來越高。

```java //--------------------------------------程式碼塊一--------------------------------------- public class Tea { void prepareRecipe() { boilWater(); steepTeaBag(); pourInCup(); addLemon(); }

public void boilWater() {
    System.out.println("Boiling water");
}

public void steepTeaBag() {
    System.out.println("Steeping the tea");
}

public void addLemon() {
    System.out.println("Adding Lemon");
}

public void pourInCup() {
    System.out.println("Pouring into cup");
}

} //--------------------------------------程式碼塊二--------------------------------------- public class Coffee { void prepareRecipe() { boilWater(); brewCoffeeGrinds(); pourInCup(); addSugarAndMilk(); }

public void boilWater() {
    System.out.println("Boiling water");
}

public void brewCoffeeGrinds() {
    System.out.println("Dripping Coffee through filter");
}

public void pourInCup() {
    System.out.println("Pouring into cup");
}

public void addSugarAndMilk() {
    System.out.println("Adding Sugar and Milk");
}

} //--------------------------------------程式碼塊三--------------------------------------- public class Barista { public static void main(String[] args) { Tea tea = new Tea(); Coffee coffee = new Coffee(); System.out.println("Making tea..."); tea.prepareRecipe(); System.out.println("Making coffee..."); coffee.prepareRecipe(); } } ```

結果:

4.3.2.4 Good Code

正確的編碼方式:首先將煮茶和煮咖啡共同動作燒水和倒水抽取成模板方法,並在父類執行,然後煮茶的泡茶、加檸檬步驟,煮咖啡的過濾咖啡、加牛奶步驟分別差異化實現即可,最後要確保四個步驟執行鏈準確性。

``` //--------------------------------------程式碼塊一--------------------------------------- public abstract class CaffeineBeverage { final void prepareRecipe() { boilWater(); brew(); pourInCup(); addCondiments(); }

abstract void brew();

abstract void addCondiments();

void boilWater() {
    System.out.println("Boiling water");
}

void pourInCup() {
    System.out.println("Pouring into cup");
}

} //--------------------------------------程式碼塊二--------------------------------------- public class Tea extends CaffeineBeverage { public void brew() { System.out.println("Steeping the tea"); } public void addCondiments() { System.out.println("Adding Lemon"); } } //--------------------------------------程式碼塊三--------------------------------------- public class Coffee extends CaffeineBeverage { public void brew() { System.out.println("Dripping Coffee through filter"); } public void addCondiments() { System.out.println("Adding Sugar and Milk"); } } //--------------------------------------程式碼塊四--------------------------------------- public class Barista { public static void main(String[] args) {

    Tea tea = new Tea();
    Coffee coffee = new Coffee();

    System.out.println("\nMaking tea...");
    tea.prepareRecipe();

    System.out.println("\nMaking coffee...");
    coffee.prepareRecipe();

}

```

結果:

4.3.2.5 Source Code

當然Android的AsyncTask也能體現模板方法設計模式,我們可以看到execute方法內部封裝了onPreExecute, doInBackground, onPostExecute這個演算法框架。

使用者可以根據自己的需求來在覆寫這幾個方法,使得使用者可以很方便的使用非同步任務來完成耗時操作,又可以通過onPostExecute來完成更新UI執行緒的工作。

``` //--------------------------------------程式碼塊一--------------------------------------- public final AsyncTask execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); }

public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
        Params... params) {

//............................................................................

    mStatus = Status.RUNNING;
    // TODO: 關鍵模板方法
    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

//--------------------------------------程式碼塊二--------------------------------------- public AsyncTask() { mWorker = new WorkerRunnable() { public Result call() throws Exception { mTaskInvoked.set(true);

            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            // TODO: 關鍵執行方法
            return postResult(doInBackground(mParams));
        }
    };
}

//--------------------------------------程式碼塊三--------------------------------------- private Result postResult(Result result) { Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult(this, result)); message.sendToTarget(); return result; } //--------------------------------------程式碼塊四--------------------------------------- private static class InternalHandler extends Handler { public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } }

//--------------------------------------程式碼塊五--------------------------------------- private void finish(Result result) { if (isCancelled()) { onCancelled(result); } else { // TODO: 關鍵模板方法 onPostExecute(result); } mStatus = Status.FINISHED; } ```

4.3.2.6 注意事項

當然模板方法如果沒有梳理好方法與方法的呼叫鏈關係,那麼模板方法會帶來程式碼閱讀的難度,會讓人覺得難以理解。

五、總結與展望

《Android架構演進 · 設計模式· 為什麼建議你一定要學透設計模式》一文首先通過5W2H全方位的講解了設計模式對Android開發的價值,然後通過UML圖例、BadCode、Good Code、使用原則和思考覆盤多維度分析了7大設計原則優劣勢和核心思想,最後分別對建立型模式、行為型模式和結構型模式的案例剖析了三大設計模式的實現細節。

因為如果功能簡單,套用設計模式搭建,反而會增加了成本和系統的複雜度。因此,在工作中我們既不要生搬硬套設計模式,也不要過度去設計。我們要根據功能需求的複雜性設計系統。

在理解設計模式思想的基礎上,小木箱強烈建議大家結合框架原始碼和專案原始碼對每一個設計模式和設計原則,進行深度理解和思考,最後才能針對合適的場景和問題正確的運用。

當然很多設計模式使用場景不是一種模式的唯一實現,可能是多種模式混合實現。因此,對Android同學發散思維和業務理解深度提出苛刻的要求。有的時候架構能力是倒逼的,面對複雜的業務頻繁的變化,我們要勇於不斷的挑戰!

這也是小木箱強烈建議大家學透設計模式很重要的原因。希望通過這篇文章能夠讓你意識到學會設計模式的重要性。

下一章Android架構演進 · 設計模式 · Android常見的4種建立型設計模式會從上而下帶大家揭祕常見建立型設計模式。我是 小木箱,我們下一篇見~

參考資料