大綱
設計可複用的類
- 繼承和重寫
- 過載(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集合框架(一個例子)