設計可複用的類java
設計可複用庫與框架算法
在OOP中設計可複用的類
封裝和信息隱藏
繼承和重寫
多態性,子類型和重載
泛型編程
行爲子類型和Liskov替代原則(LSP)
組合與委託編程
行爲子類型
子類型多態性:客戶端代碼能夠統一處理不一樣種類的對象。 子類型多態:客戶端可用統一的方式處理不一樣類型的對象設計模式
假設q(x)是T類型對象x可證實的性質,那麼對於S類型的對象y,q(y)應該是可證實的,其中S是T的一個子類型。 - Barbara Liskov數組
Java編譯器執行的規則(靜態類型檢查)安全
重寫方法不會拋出額外的異常數據結構
也適用於指定的行爲(方法):架構
Liskov替代原則(LSP)框架
LSP是一種特定的子類型關係定義,稱爲強行爲子類型化
在編程語言中,LSP依賴於如下限制:編程語言
Covariance (協變)
父類型到子類型:
愈來愈具體specific
返回值類型:不變或變得更具體
異常的類型:也是如此
Contravariance (反協變、逆變)
父類型到子類型:
愈來愈具體specific
參數類型:要相反的變化,要不變或愈來愈抽象
從邏輯上講,它被稱爲子類型中方法參數的逆變。
這在Java中其實是不容許的,由於它會使重載規則複雜化。
協變和反協變
數組是協變的:根據Java的子類型規則,T []類型的數組可能包含T類型的元素或T的任何子類型。
在運行時,Java知道這個數組其實是做爲一個整數數組實例化的,它只是簡單地經過Number []類型的引用來訪問。
考慮泛型中的LSP
泛型是類型不變的
編譯完成後,編譯器會丟棄類型參數的類型信息; 所以這種類型的信息在運行時不可用。
這個過程被稱爲類型擦除
泛型不是協變的。
什麼是類型擦除?
類型擦除:若是類型參數是無界的,則將泛型類型中的全部類型參數替換爲它們的邊界或對象。 所以,生成的字節碼只包含普通的類,接口和方法。
泛型中的通配符
無界通配符類型使用通配符(?)指定,例如List <?>。
有兩種狀況,無界通配符是一種有用的方法:
下限通配符:<? super A>
上限通配符:<? extends A>
考慮具備通配符的泛型的LSP
List<Number>是List<?>的一個子類
List<Number> 是List<? extends Object>的一個子類
List<Object>是List<? super String>的一個子類
Interface Comparator<T>
int compare(T o1,T o2):比較它的兩個參數的順序。
若是你的ADT須要比較大小,或者要放入Collections或Arrays進行排序,可實現Comparator接口並重寫compare()函數。
該接口對每一個實現它的類的對象進行總排序。
這種順序被稱爲類的天然順序,類的compareTo方法被稱爲其天然比較方法。
另外一種方法:讓你的ADT實現Comparable接口,而後重寫compareTo()方法
與使用Comparator的區別:不須要構建新的Comparator類,比較代碼放在ADT內部。
委託
委託只是當一個對象依賴另外一個對象來實現其功能的某個子集時(一個實體將某個事物傳遞給另外一個實體)
委派/委託:一個對象請求另外一個對象的功能
委派是複用的一種常見形式
委託能夠被描述爲在實體之間共享代碼和數據的低級機制。
委託模式是實施委託的一種軟件設計模式,雖然這個術語也用於鬆散地進行諮詢或轉發。
委託依賴於動態綁定,由於它要求給定的方法調用能夠在運行時調用不一樣的代碼段。
處理
委託與繼承
繼承:經過新操做擴展基類或重寫操做。
委託:捕獲操做並將其發送給另外一個對象。
許多設計模式使用繼承和委派的組合。
將繼承替換爲委派
問題:你有一個只使用其超類的一部分方法的子類(或者它不可能繼承超類數據)。
解決方案:建立一個字段並在其中放入一個超類對象,將方法委託給超類對象,並消除繼承。
實質上,這種重構拆分了兩個類,並使超類成爲子類的幫助者,而不是其父類。
合成繼承原則
或稱爲合成複用原則(CRP)
委託能夠被看做是在對象層次上的複用機制,而繼承是類層次上的複用機制。
「委託」發生在objet層面,而「繼承」發生在類層面
合成繼承原則
組合繼承的實現一般始於建立表明系統必須展示的行爲的各類接口。
實現已識別的接口的類將根據須要構建並添加到業務域類中。
這樣,系統行爲就沒有繼承地實現了。
使用接口定義不一樣側面的行爲
接口之間經過擴展實現行爲的擴展(接口組合)
類實現組合接口
委託的類型
使用(A使用B)
組合/聚合(A擁有B)
關聯(A有B)
這種分類是根據被委託者和委託者之間的「耦合程度」。
(1)依賴:臨時性的委託
使用類的最簡單形式是調用它的方法;
這兩種類別之間的關係形式被稱爲「uses-a」關係,其中一個類使用另外一個類而不實際地將其做爲屬性。 例如,它多是一個參數或在方法中本地使用。
依賴關係:對象須要其餘對象(供應商)實施的臨時關係。
(2)關聯:永久性的委託
關聯:對象類之間的一種持久關係,它容許一個對象實例使另外一個對象表明它執行一個動做。
(3)組成:更強的委託
組合是一種將簡單對象或數據類型組合成更復雜的對象的方法。
(4)聚合
聚合:對象存在於另外一個以外,在外部建立,因此它做爲參數傳遞給構造者。
組合(Composition)與聚合(Aggregation)
在組合中,當擁有的對象被破壞時,被包含的對象也被破壞。
在聚合中,這不必定是正確的。
實際中的庫和框架
定義關鍵抽象及其接口
定義對象交互和不變量
定義控制流程
提供體系結構指導
提供默認值
之因此庫和框架被稱爲系統層面的複用,是由於它們不只定義了1個可複用的接口/類,而是將某個完整系統中的全部可複用的接口/類都實現出來,而且定義了這些類之間的交互關係,調用關係,從而造成了系統總體的「架構」。
更多條款
API:應用程序編程接口,庫或框架的接口
客戶端:使用API的代碼
插件:定製框架的客戶端代碼
擴展點:框架內預留的「空白」,開發者開發出符合接口要求的代碼(即插件),框架可調用,從而至關於開發者擴展了框架的功能
協議:API和客戶端之間預期的交互順序
回調:框架調用來訪問定製功能的插件方法
生命週期方法:根據協議和插件狀態按順序調用的回調方法
爲何API設計很重要?
若是你編程,你是一個API設計師,而且API能夠是你最大的資產之一
也能夠是你最大的責任
公共API是永遠的
(1)API應該作一件事,作得好
功能應該很容易解釋
(2)API應該儘量小,但不能更小
API應該知足其要求
尋找一個很好的功率重量比
(3)實施不該該影響API
API中的實施細節是有害的
請注意什麼是實施細節
例如:不要指定散列函數
不要讓實現細節「泄露」到API中
儘可能減小一切的可達性(信息隱藏)
(4)文件事宜
記錄每一個類,接口,方法,構造函數,參數和異常
先決條件,後置條件,反作用
文件線程安全
若是類是可變的,則記錄狀態空間
重複使用比說要容易得多。 這樣作須要良好的設計和很是好的文檔。 即便咱們看到良好的設計(這仍然不常見),若是沒有良好的文檔,咱們也不會看到組件被複用。 - D. L. Parnas軟件老化,ICSE 1994
(5)考慮績效後果
很差的決定會限制性能
不要扭曲API來得到性能
良好的設計一般與良好的性能相吻合
糟糕的API決策的性能影響多是真實且永久的
(6)API必須與平臺和平共存
習慣作什麼
利用API友好功能
瞭解並避免API陷阱和陷阱
不要音譯API
(7)類設計
最小化可變性:除非有充分的理由不然類應該是不可變的
只有子類纔有意義:子類化會影響替代性(LSP)
(8)方法設計
不要讓客戶作任何模塊能夠作的事情
API應該快速失敗:儘快報告錯誤。 編譯時間最好 - 靜態類型,泛型。
以字符串形式提供對全部可用數據的編程訪問。 不然,客戶端會解析字符串,這對客戶來講很痛苦
過分謹慎。 一般最好使用不一樣的名稱。
使用適當的參數和返回類型。
避免長參數列表。 三個或更少的參數是理想的。
避免須要特殊處理的返回值。 返回零長度數組或空集合,不爲null。
白盒和黑盒框架
白盒框架
黑盒框架
白盒與黑盒框架
白盒框架使用子類/子類型---繼承
黑盒框架使用組合 - 委派/組合
框架設計考慮
一旦設計好,改變的機會就很小
關鍵決策:將通用部件與可變部件分開
可能的問題:
「最大限度地利用重複使用最小化」
典型的框架設計和實現
定義你的域名
分解和實施通用部件爲框架
爲可變部分提供插件接口和回調機制
得到大量的反饋,並迭代
這一般被稱爲「域工程」。
進化設計:提取共同點
提取界面是進化設計中的一個新步驟:
一旦架構穩定就開始
運行一個框架
一些框架能夠自行運行
其餘框架必須擴展才能運行
加載插件的方法:
什麼是收集和收集框架?
集合:對元素進行分組的對象
主要用途:數據存儲和檢索,以及數據傳輸
集合框架:一個統一的架構
最着名的例子
同步包裝(不是線程安全的!)
同步包裝:線程安全的新方法
那時是新的; 如今已經老了!
設計可複用的類
設計系統級可複用的庫和框架