【軟件構造】第五章第二節 設計可復用的軟件
阿新 • • 發佈:2018-06-14
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.size
或List.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 ;
- 插件加載機制加載插件並對框架進行控制。
【軟件構造】第五章第二節 設計可復用的軟件