第5章:可複用性的軟件構建方法 5.2面向複用的構造

大綱

設計可複用的類java

  • 繼承和重寫
  • 重載(Overloading)
  • 參數多態和泛型編程
  • 行爲子類型與Liskov替換原則
  • 組合與委託

設計可複用庫與框架算法

  • API和庫 - 框架
  • Java集合框架(一個例子)

設計可複用的類

在OOP中設計可複用的類
封裝和信息隱藏
繼承和重寫
多態性,子類型和重載
泛型編程
行爲子類型和Liskov替代原則(LSP)
組合與委託編程

(1)行爲子類型和Liskov替換原則(LSP)

行爲子類型
子類型多態性:客戶端代碼能夠統一處理不一樣種類的對象。 子類型多態:客戶端可用統一的方式處理不一樣類型的對象設計模式

  • 若是Cat的類型是Animal的一個子類型,那麼只要使用Animal類型的表達式,就可使用Cat類型的表達式。

假設q(x)是T類型對象x可證實的性質,那麼對於S類型的對象y,q(y)應該是可證實的,其中S是T的一個子類型。 - Barbara Liskov數組

Java編譯器執行的規則(靜態類型檢查)安全

  • 子類能夠添加,但不能刪除方法
  • 具體類必須實現全部未定義的方法
  • 重寫方法必須返回相同的類型或子類型
  • 重寫方法必須接受相同的參數類型

重寫方法不會拋出額外的異常數據結構

也適用於指定的行爲(方法):架構

  • 相同或更強的不變量
  • 相同或較弱的先決條件
  • 相同或更強的後置條件

Liskov替代原則(LSP)框架

LSP是一種特定的子類型關係定義,稱爲強行爲子類型化
在編程語言中,LSP依賴於如下限制:編程語言

  • 先決條件不能在子類型中增強。前置條件不能強化
  • 後置條件在子類型中不能被削弱。後置條件不能弱化
  • 超類型的不變式必須保存在一個子類型中。不變量要保持
  • 子類型中方法參數的變換。子類型方法參數:逆變
  • 子類型中返回類型的協邊。子類型方法的返回值:協變
  • 子類型的方法不該引起新的異常,除非這些異常自己是超類型方法拋出的異常的子類型。 異常類型:協變(這將在第7-2節討論)

Covariance (協變)

父類型到子類型:
愈來愈具體specific
返回值類型:不變或變得更具體
異常的類型:也是如此

Contravariance (反協變、逆變)

父類型到子類型:
愈來愈具體specific
參數類型:要相反的變化,要不變或愈來愈抽象

從邏輯上講,它被稱爲子類型中方法參數的逆變。
這在Java中其實是不容許的,由於它會使重載規則複雜化。

協變和反協變

數組是協變的:根據Java的子類型規則,T []類型的數組可能包含T類型的元素或T的任何子類型。
在運行時,Java知道這個數組其實是做爲一個整數數組實例化的,它只是簡單地經過Number []類型的引用來訪問。

  • 區分:對象的類型與引用的類型

考慮泛型中的LSP

泛型是類型不變的

  • ArrayList <String>是List <String>的子類型
  • List <String>不是List <Object>的子類型

編譯完成後,編譯器會丟棄類型參數的類型信息; 所以這種類型的信息在運行時不可用。
這個過程被稱爲類型擦除
泛型不是協變的。

什麼是類型擦除?

類型擦除:若是類型參數是無界的,則將泛型類型中的全部類型參數替換爲它們的邊界或對象。 所以,生成的字節碼只包含普通的類,接口和方法。

泛型中的通配符

無界通配符類型使用通配符(?)指定,例如List <?>。

  • 這被稱爲未知類型的列表。

有兩種狀況,無界通配符是一種有用的方法:

  • 若是您正在編寫可使用Object類中提供的功能實現的方法。
  • 代碼使用泛型類中不依賴於類型參數的方法。 例如,List.size或List.clear。 事實上,Class <?>常常被使用,由於Class <T>中的大多數方法不依賴於T.

下限通配符:<? super A>
上限通配符:<? extends A>

考慮具備通配符的泛型的LSP

List<Number>是List<?>的一個子類
List<Number> 是List<? extends Object>的一個子類
List<Object>是List<? super String>的一個子類

(2)委託和組合

Interface Comparator<T>
int compare(T o1,T o2):比較它的兩個參數的順序。

  • 一個比較函數,它對某些對象集合進行總排序。
  • 能夠將比較器傳遞給排序方法(如Collections.sort或Arrays.sort),以便精確控制排序順序。 比較器也能夠用來控制某些數據結構(例如排序集合或排序映射)的順序,或者爲沒有天然排序的對象集合提供排序。

若是你的ADT須要比較大小,或者要放入Collections或Arrays進行排序,可實現Comparator接口並重寫compare()函數。

該接口對每一個實現它的類的對象進行總排序。

這種順序被稱爲類的天然順序,類的compareTo方法被稱爲其天然比較方法。
另外一種方法:讓你的ADT實現Comparable接口,而後重寫compareTo()方法
與使用Comparator的區別:不須要構建新的Comparator類,比較代碼放在ADT內部。

委託

委託只是當一個對象依賴另外一個對象來實現其功能的某個子集時(一個實體將某個事物傳遞給另外一個實體)
委派/委託:一個對象請求另外一個對象的功能

  • 例如分揀機正在委託比較器的功能

委派是複用的一種常見形式

  • 分揀機能夠重複使用任意的排序順序
  • 比較器能夠重複使用須要比較整數的任意客戶端代碼

委託能夠被描述爲在實體之間共享代碼和數據的低級機制。

  • 顯式委託:將發送對象傳遞給接收對象
  • 隱式委託:由語言的成員查找規則

委託模式是實施委託的一種軟件設計模式,雖然這個術語也用於鬆散地進行諮詢或轉發。

委託依賴於動態綁定,由於它要求給定的方法調用能夠在運行時調用不一樣的代碼段。
處理

  • 接收者對象將操做委託給Delegate對象
  • 接收者對象確保客戶端不會濫用委託對象。

委託與繼承

繼承:經過新操做擴展基類或重寫操做。
委託:捕獲操做並將其發送給另外一個對象。
許多設計模式使用繼承和委派的組合。

將繼承替換爲委派

問題:你有一個只使用其超類的一部分方法的子類(或者它不可能繼承超類數據)。
解決方案:建立一個字段並在其中放入一個超類對象,將方法委託給超類對象,並消除繼承。
實質上,這種重構拆分了兩個類,並使超類成爲子類的幫助者,而不是其父類。

  • 代替繼承全部的超類方法,子類將只有必要的方法來委派給超類對象的方法。
  • 一個類不包含從超類繼承的任何不須要的方法。

合成繼承原則

或稱爲合成複用原則(CRP)

  • 類應該經過它們的組合(經過包含實現所需功能的其餘類的實例)實現多態行爲和代碼複用,而不是從基類或父類繼承。
  • 最好組合一個對象能夠作的事(has_a)而不是擴展它(is_a)。

委託能夠被看做是在對象層次上的複用機制,而繼承是類層次上的複用機制。
「委託」發生在objet層面,而「繼承」發生在類層面

合成繼承原則

組合繼承的實現一般始於建立表明系統必須展示的行爲的各類接口。
實現已識別的接口的類將根據須要構建並添加到業務域類中。
這樣,系統行爲就沒有繼承地實現了。
使用接口定義不一樣側面的行爲
接口之間經過擴展實現行爲的擴展(接口組合)
類實現組合接口

委託的類型

使用(A使用B)
組合/聚合(A擁有B)
關聯(A有B)
這種分類是根據被委託者和委託者之間的「耦合程度」。

(1)依賴:臨時性的委託

使用類的最簡單形式是調用它的方法;
這兩種類別之間的關係形式被稱爲「uses-a」關係,其中一個類使用另外一個類而不實際地將其做爲屬性。 例如,它多是一個參數或在方法中本地使用。
依賴關係:對象須要其餘對象(供應商)實施的臨時關係。

(2)關聯:永久性的委託

關聯:對象類之間的一種持久關係,它容許一個對象實例使另外一個對象表明它執行一個動做。

  • has_a:一個類有另外一個做爲屬性/實例變量
  • 這種關係是結構性的,由於它指定一種對象與另外一種對象相連,並不表明行爲。

(3)組成:更強的委託

組合是一種將簡單對象或數據類型組合成更復雜的對象的方法。

  • is_part_of:一個類有另外一個做爲屬性/實例變量
  • 實現了一個對象包含另外一個對象。

(4)聚合

聚合:對象存在於另外一個以外,在外部建立,因此它做爲參數傳遞給構造者。

  • has_a

組合(Composition)與聚合(Aggregation)

在組合中,當擁有的對象被破壞時,被包含的對象也被破壞。

  • 一所大學擁有多個部門,每一個部門都有一批教授。 若是大學關閉,部門將不復存在,但這些部門的教授將繼續存在。

在聚合中,這不必定是正確的。

  • 大學能夠被看做是一個部門的組合,而部門則擁有一批教授。 一位教授能夠在一個以上的部門工做,但一個部門不能成爲多個大學的一部分。

設計系統級可複用的庫和框架

實際中的庫和框架

定義關鍵抽象及其接口
定義對象交互和不變量
定義控制流程
提供體系結構指導
提供默認值
之因此庫和框架被稱爲系統層面的複用,是由於它們不只定義了1個可複用的接口/類,而是將某個完整系統中的全部可複用的接口/類都實現出來,而且定義了這些類之間的交互關係,調用關係,從而造成了系統總體的「架構」。

更多條款

API:應用程序編程接口,庫或框架的接口
客戶端:使用API的代碼
插件:定製框架的客戶端代碼
擴展點:框架內預留的「空白」,開發者開發出符合接口要求的代碼(即插件),框架可調用,從而至關於開發者擴展了框架的功能
協議:API和客戶端之間預期的交互順序
回調:框架調用來訪問定製功能的插件方法
生命週期方法:根據協議和插件狀態按順序調用的回調方法

(1)API設計

爲何API設計很重要?

若是你編程,你是一個API設計師,而且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決策的性能影響多是真實且永久的

  • Component.getSize()返回Dimension,但Dimension是可變的,所以每一個getSize調用都必須分配Dimension,致使數百萬無用的對象分配

(6)API必須與平臺和平共存

習慣作什麼

  • 遵照標準的命名約定
  • 避免過期的參數和返回類型
  • 模仿核心API和語言中的模式

利用API友好功能

  • 泛型,可變參數,枚舉,函數接口

瞭解並避免API陷阱和陷阱

  • 終結器,公共靜態最終數組等。

不要音譯API

(7)類設計

最小化可變性:除非有充分的理由不然類應該是不可變的

  • 優勢:簡單,線程安全,可重複使用
  • 缺點:爲每一個值分開對象
  • 若是可變,保持狀態空間小,定義明確。

只有子類纔有意義:子類化會影響替代性(LSP)

  • 除非存在某種關係,不然不要繼承。 不然,請使用委託或組合。
  • 不要爲了複用實現而繼承子類。
  • 繼承違反封裝,子類對超類的實現細節很敏感

(8)方法設計

不要讓客戶作任何模塊能夠作的事情

  • 客戶一般經過剪切和粘貼,這是醜陋的,煩人的,錯誤的。

API應該快速失敗:儘快報告錯誤。 編譯時間最好 - 靜態類型,泛型。

  • 在運行時,第一個錯誤的方法調用是最好的
  • 方法應該是失敗原子的

以字符串形式提供對全部可用數據的編程訪問。 不然,客戶端會解析字符串,這對客戶來講很痛苦
過分謹慎。 一般最好使用不一樣的名稱。
使用適當的參數和返回類型。

  • 歡迎界面類型的類輸入靈活性,性能
  • 使用最具體的可能輸入參數類型,從而將錯誤從運行時移到編譯時間。

避免長參數列表。 三個或更少的參數是理想的。

  • 若是你必須使用不少參數呢?

避免須要特殊處理的返回值。 返回零長度數組或空集合,不爲null。

(2)框架設計

白盒和黑盒框架

白盒框架

  • 經過繼承和覆蓋方法進行擴展
  • 通用設計模式:模板方法
  • 子類具備主要方法,但對框架進行控制

黑盒框架

  • 經過實現插件接口進行擴展
  • 通用設計模式:策略,觀察者
  • 插件加載機制加載插件並對框架進行控制

白盒與黑盒框架

白盒框架使用子類/子類型---繼承

  • 容許擴展每一個非私有方法
  • 須要瞭解超類的實現
  • 一次只能有一個分機
  • 彙編在一塊兒
  • 一般所謂的開發者框架

黑盒框架使用組合 - 委派/組合

  • 容許擴展在界面中顯示的功能
  • 只須要了解接口
  • 多個插件
  • 一般提供更多的模塊化
  • 能夠單獨部署(.jar,.dll,...)
  • 一般稱爲最終用戶框架,平臺

框架設計考慮

一旦設計好,改變的機會就很小
關鍵決策:將通用部件與可變部件分開

  • 你想解決什麼問題?

可能的問題:

  • 擴展點太少:限於狹窄的用戶類別
  • 延伸點過多:難以學習,速度緩慢
  • 太通用:不多複用價值

「最大限度地利用重複使用最小化」

典型的框架設計和實現

定義你的域名

  • 識別潛在的公共部分和可變部分
  • 設計和編寫示例插件/應用程序

分解和實施通用部件爲框架

爲可變部分提供插件接口和回調機制

  • 在適當的地方使用衆所周知的設計原則和模式...

得到大量的反饋,並迭代
這一般被稱爲「域工程」。

進化設計:提取共同點

提取界面是進化設計中的一個新步驟:

  • 抽象類是從具體類中發現的
  • 接口是從抽象類中提取的

一旦架構穩定就開始

  • 從課堂上刪除非公開的方法
  • 將默認實現移動到實現接口的抽象類中

運行一個框架

一些框架能夠自行運行

  • 例如 Eclipse

其餘框架必須擴展才能運行

  • Swing,JUnit,MapReduce,Servlets

加載插件的方法:

  • 客戶端寫入main(),建立一個插件並將其傳遞給框架
  • Framework寫入main(),客戶端將plugin的名稱做爲命令行參數或環境變量傳遞
  • Framework在一個神奇的位置查找,而後配置文件或.jar文件被自動加載和處理。

(3)Java集合框架

什麼是收集和收集框架?

集合:對元素進行分組的對象
主要用途:數據存儲和檢索,以及數據傳輸

  • 熟悉的例子:java.util.Vector,java.util.Hashtable,Array

集合框架:一個統一的架構

  • 接口 - 實現獨立
  • 實現 - 可複用的數據結構
  • 算法 - 可複用的功能

最着名的例子

  • C++標準模板庫(STL)
  • Java集合框架(JCF)

同步包裝(不是線程安全的!)

同步包裝:線程安全的新方法

  • 匿名實現,每一個核心接口一個
  • 靜態工廠須要收集適當的類型
  • 若是經過包裝進行所有訪問,線程安全保證
  • 必須手動同步迭代

那時是新的; 如今已經老了!

  • 同步包裝很大程度上已通過時
  • 由同時收集而過期

總結

設計可複用的類

  • 繼承和重寫
  • 超載
  • 參數多態和泛型編程
  • 行爲分類和Liskov替代原則(LSP)
  • 組成和委派

設計系統級可複用的庫和框架

  • API和庫
  • 框架
  • Java集合框架(一個例子)
相關文章
相關標籤/搜索