1. 程式人生 > >【軟件構造】第五章第二節 設計可復用的軟件

【軟件構造】第五章第二節 設計可復用的軟件

pre ron start arr 應用 time 抽象 組合 double

第五章第二節 設計可復用的軟件

5-1節學習了可復用的層次、形態、表現;本節從類、API、框架三個層面學習如何設計可復用軟件實體的具體技術。

Outline

  • 設計可復用的類——LSP
    • 行為子結構
    • Liskov替換原則(LSP)
  • 各種應用中的LSP
    • 數組是協變的
    • 泛型中的LSP
    • 為了解決類型擦除的問題-----Wildcards(通配符)
  • 設計可復用的類——委派與組合
  • 設計可復用庫與框架

Notes

## 設計可復用的類——LSP

  • 在OOP之中設計可復用的類
    • 封裝和信息隱藏
    • 繼承和重寫
    • 多態、子類和重載
    • 泛型編程
    • LSP原則
    • 委派和組合(Composition)

【行為子結構】

  • 子類型多態( Subtype polymorphism):客戶端可用統一的方式處理不同類型的對象 。
  • 栗子:
    Animal a = new Animal(); 
    Animal c1 = new Cat(); 
    Cat c2 = new Cat();

    在可以使用a的場景,都可以用c1和c2代替而不會有任何問題。

  • 在java的靜態類型檢查之中,編譯器強調了幾條規則:
    • 子類型可以增加方法,但不可刪
    • 子類型需要實現抽象類型中的所有未實現方法
    • 子類型中重寫的方法必須有相同或子類型的返回值
    • 子類型中重寫的方法必須使用同樣類型的參數
    • 子類型中重寫的方法不能拋出額外的異常
  • 行為子結構也適用於指定的方法:
    • 更強的不變量
    • 更弱的前置條件
    • 更強的後置條件

行為子結構的示例一:

技術分享圖片

  • 子類滿足相同的不變量(同時附加了一個)
  • 重寫的方法有相同的前置條件和後置條件
  • 故該結構滿足LSP

行為子結構的示例二:

技術分享圖片

  • 子類滿足相同的不變量(同時附加了一個)
  • 重寫的方法 start 的前置條件更弱
  • 重寫的方法 brake 的後置條件更強
  • 故該結構滿足LSP

行為子結構的示例三:

技術分享圖片

  • 子類滿足不變量條件更強,故滿足LSP

Liskov替換原則(LSP)】 更多參考:LSP的筆記

  • 裏氏替換原則的主要作用就是規範繼承時子類的一些書寫規則。其主要目的就是保持父類方法不被覆蓋。
  • LSP是子類型關系的一個特殊定義,稱為(強)行為子類型化。在編程語言中,LSP依賴於以下限制:
    • 前置條件不能強化
    • 後置條件不能弱化
    • 不變量要保持或增強
    • 子類型方法參數:逆變
    • 子類型方法的返回值:協變
    • 異常類型:協變
  • 協變(Co-variance):
    • 父類型->子類型:越來越具體(specific)。
    • 在LSP中,返回值和異常的類型:不變或變得更具體 。
    • 栗子:技術分享圖片
  • 逆變(Contra-variance):
    • 父類型->子類型:越來越具體specific 。
    • 參數類型:要相反的變化,不變或越來越抽象。
    • 栗子:技術分享圖片
    • 但這在Java中是不允許的,因為它會使重載規則復雜化。

總結:

技術分享圖片

(1.子類型(屬性、方法)關系;2.不變性,重寫方法;3.協變,方法返回值變具體;4.逆變,方法參數變抽象;5.協變,參數變的更具體,協變不安全)

## 各種應用中的LSP

【數組是協變的】

  • 數組是協變的:一個數組T[ ] ,可能包含了T類型的實例或者T的任何子類型的實例
  • 下面報錯的原因是myNumber指向的還是一個Integer[] 而不是Number[]
Number[] numbers = new Number[2]; 
numbers[0] = new Integer(10); 
numbers[1] = new Double(3.14);
Integer[] myInts
= {1,2,3,4}; Number[] myNumber = myInts;
myNumber[
0] = 3.14; //run-time error!

【泛型中的LSP】

  • 泛型是類型不變的(泛型不是協變的)。舉例來說
    • ArrayList<String>List<String>的子類型
    • List<String>不是List<Object>的子類型
  • 在代碼的編譯完成之後,泛型的類型信息就會被編譯器擦除。因此,這些類型信息並不能在運行階段時被獲得。這一過程稱之為類型擦除(type erasure)。
  • 類型擦除的詳細定義:如果類型參數沒有限制,則用它們的邊界或Object來替換泛型類型中的所有類型參數。因此,產生的字節碼只包含普通的類、接口和方法。
  • 類型擦除的結果: <T>被擦除 T變成了Object

    技術分享圖片

  • Integer是number的子類型,但Box<Integer>也不是Box<Number>的子類型
  • 這對於類型系統來說是不安全的,編譯器會立即拒絕它。

技術分享圖片

【為了解決類型擦除的問題-----Wildcards(通配符)】

  • 無界通配符類型使用通配符()指定,例如List <?>,這被稱為未知類型的列表。
  • 在兩種情況下,無界通配符是一種有用的方法:
    • 如果您正在編寫可使用Object類中提供的功能實現的方法。
    • 當代碼使用泛型類中不依賴於類型參數的方法時。 例如,List.sizeList.clear。 事實上,Class <?>經常被使用,因為Class <T>中的大多數方法不依賴於T

栗子:

public static void printList(List<Object> list) { 
    for (Object elem : list) 
        System.out.println(elem + " "); 
    System.out.println(); 
} 

  printList的目標是打印任何類型的列表,但它無法實現該目標 ,它僅打印Object實例列表; 它不能打印List <Integer>List <String>List <Double>等,因為它們不是List <Object>的子類型。
  要編寫通用的printList方法,請使用List <?>

1 public static void printList(List<?> list) { 
2     for (Object elem: list) 
3 System.out.println(); 
4 } 
5 
6 ist<Integer> li = Arrays.asList(1, 2, 3); 
7 List<String>  ls = Arrays.asList("one", "two", "three"); 
8 printList(li); 
9 printList(ls);

技術分享圖片

## 設計可復用庫與框架

  之所以library和framework被稱為系統層面的復用,是因為它們不僅定義了1個可復用的接口/類,而是將某個完整系統中的所有可復用的接口/類都實現出來,並且定義了這些類之間的交互關系、調用關系,從而形成了系統整體 的“架構”。、

  • 相應術語:
    • API(Application Programming Interface):庫或框架的接口
    • Client(客戶端):使用API的代碼
    • Plugin(插件):客戶端定制框架的代碼
    • Extension Point:框架內預留的“空白”,開發者開發出符合接口要求的代碼( 即plugin) , 框架可調用,從而相當於開發者擴展了框架的功能
    • Protocol(協議):API與客戶端之間預期的交互序列。
    • Callback(反饋):框架將調用的插件方法來訪問定制的功能。
    • Lifecycle method:根據協議和插件的狀態,按順序調用的回調方法。

【API和庫】

  • API是程序員最重要的資產和“榮耀”,吸引外部用戶,提高聲譽。
  • 建議:始終以開發API的標準面對任何開發任務;面向“復用”編程而不是面向“應用”編程。
  • 難度:要有足夠良好的設計,一旦發布就無法再自由改變。
  • 編寫一個API需要考慮以下方面:
    • API應該做一件事,且做得很好
    • API應該盡可能小,但不能太小
    • Implementation不應該影響API
    • 記錄文檔很重要
    • 考慮性能後果
    • API必須與平臺和平共存
    • 類的設計:盡量減少可變性,遵循LSP原則
    • 方法的設計:不要讓客戶做任何模塊可以做的事情,及時報錯

【框架】

框架分為白盒框架和黑盒框架。

  • 白盒框架:
    • 通過子類化和重寫方法進行擴展(使用繼承);
    • 通用設計模式:模板方法;
    • 子類具有主要方法但對框架進行控制。
  • 黑盒框架:
    • 通過實現插件接口進行擴展(使用組合/委派);
    • 常用設計模式:Strategy, Observer ;
    • 插件加載機制加載插件並對框架進行控制。

【軟件構造】第五章第二節 設計可復用的軟件