第五章第二節 設計可複用的軟件
5-1節學習了可複用的層次、形態、表現;本節從類、API、框架三個層面學習如何設計可複用軟件實體的具體技術。java
Outline
- 設計可複用的類——LSP
- 行爲子結構
- 協變與逆變
- Liskov替換原則(LSP)
- 各類應用中的LSP
- 數組是協變的
- 泛型中的LSP
- 爲了解決類型擦除的問題-----Wildcards(通配符)
- 設計可複用的類——委派與組合
- 設計可複用庫與框架
Notes
## 設計可複用的類——LSP
- 在OOP之中設計可複用的類
- 封裝和信息隱藏
- 繼承和重寫
- 多態、子類和重載
- 泛型編程
- LSP原則
- 委派和組合(Composition)
【行爲子結構】程序員
行爲子結構的示例一:編程
- 子類知足相同的不變量(同時附加了一個)
- 重寫的方法有相同的前置條件和後置條件
- 故該結構知足LSP
行爲子結構的示例二:設計模式
- 子類知足相同的不變量(同時附加了一個)
- 重寫的方法 start 的前置條件更弱
- 重寫的方法 brake 的後置條件更強
- 故該結構知足LSP
行爲子結構的示例三:api
【逆變與協變】數組
- 逆變與協變綜述:若是A、B表示類型,f(⋅)表示類型轉換,≤表示繼承關係(好比,A≤B表示A是由B派生出來的子類):
- f(⋅)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
- f(⋅)是協變(covariant)的,當A≤B時有f(A)≤f(B)成立;
- f(⋅)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關係。
- 協變(Co-variance):
- 父類型->子類型:愈來愈具體(specific)。
- 在LSP中,返回值和異常的類型:不變或變得更具體 。
- 栗子:
- 逆變(Contra-variance):
- 父類型->子類型:愈來愈抽象。
- 參數類型:要相反的變化,不變或愈來愈抽象。
- 栗子:
- 但這在Java中是不容許的,由於它會使重載規則複雜化。
總結:安全
(1.子類型(屬性、方法)關係;2.不變性,重寫方法;3.協變,方法返回值變具體;4.逆變,方法參數變抽象;5.協變,參數變的更具體,協變不安全)架構
【Liskov替換原則(LSP)】 更多參考:LSP的筆記框架
- 里氏替換原則的主要做用就是規範繼承時子類的一些書寫規則。其主要目的就是保持父類方法不被覆蓋。
- 含義:
- 子類必須徹底實現父類的方法
- 子類能夠有本身的個性
- 覆蓋或實現父類的方法時輸入參數能夠被放大
- 覆蓋或實現父類的方法時輸出結果能夠被縮小
- LSP是子類型關係的一個特殊定義,稱爲(強)行爲子類型化。在編程語言中,LSP依賴於如下限制:
- 前置條件不能強化
- 後置條件不能弱化
- 不變量要保持或加強
- 子類型方法參數:逆變
- 子類型方法的返回值:協變
- 異常類型:協變
## 各類應用中的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】
- Java中泛型是不變的,但能夠經過通配符"?"實現協變和逆變:
- <? extends>實現了泛型的協變:
- List<? extends Number> list = new ArrayList<Integer>();
- <? super>實現了泛型的逆變:
- List<? super Number> list = new ArrayList<Object>();
- 因爲泛型的協變只能規定類的上界,逆變只能規定下界,使用時須要遵循PECS(producer--extends, consumer-super):
- 要從泛型類取數據時,用extends;
- 要往泛型類寫數據時,用super;
- 既要取又要寫,就不用通配符(即extends與super都不用)。
- 泛型是類型不變的(泛型不是協變的)。舉例來講
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<?>
- 低邊界通配符<? super A> e.g. List<? super Integer> List<Number>
- 上邊界通配符<? extends A> e.g. List<? extends Number> List<Integer>
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);
## 委派與組合
【Comparator 和 Comparable(比較器與可比較的)】
咱們先看一個比較排序的例子:
- 引入 int compare(T o1 , T o2):用於比較兩個變量的大小。
- 若是你的ADT須要比較大小,或者要放入
Collections
或Arrays
進行排序,可實現Comparator
接口並override compare()
函數。下面爲具體例子:
另外一種方法:讓你的ADT實現Comparable
接口,而後override compareTo()
方法。與使用Comparator
的區別:不須要構建新的Comparator
類,比較代碼放在ADT內部。下面爲具體例子。
【委派(Delegation)】
- 委派/委託:一個對象請求另外一個對象的功能 。
- 委派是複用的一種常見形式。
- 分爲顯性委派:將發送對象傳遞給接收對象;
- 以及隱性委派:由語言的成員查找規則。
- 下面是一個栗子:能夠看到,想在B中調用A,須要先委派一個A。
- 委派設計模式:是一種用來實現委派的軟件設計模式;
- 委派依賴於動態綁定,由於它要求給定的方法調用能夠在運行時調用不一樣的代碼段;
- 委派的過程以下:Receiver對象將操做委託給Delegate對象,同時Receiver對象確保客戶端不會濫用委託對象;
- 例子二:使用委派能夠繼承委派類的功能,這裏list是被委派的對象,List的方法add和remove方法均可以經過list在LoggingList中被調用。
【委派與繼承】
- 繼承:經過新操做擴展基類或覆蓋操做。
- 委託:捕獲操做並將其發送給另外一個對象。
- 許多設計模式使用繼承和委派的組合。
- Problem:若是子類只須要複用父類中的一小部分方法,
- Solution:能夠不須要使用繼承,而是經過委派機制來實現。
- 本質上,一個類不須要繼承另外一個類的所有方法,經過委託機制調用部分方法。
【複合繼承原則(CRP)】
- 複合複用原則(CRP):類應當經過它們之間的組合(經過包含其它類的實例來實現指望的功能)達到多態表現和代碼複用,而不只僅是從基礎類或父類繼承。
- 咱們能夠將組合(Composition)理解爲(has a)而繼承理解爲(is a);
- 委派能夠看作Object層面的複用機制,而繼承能夠看作是類的層面;
- 下面咱們看一個關於CRP的栗子:
Employee
類具備計算員工年度獎金的方法:
class Employee {
Money computeBonus() {... // default computation}
... }
Employee
的不一樣子類:Manager
, Programmer
, Secretary
等可能但願重寫此方法以反映某些類型的員工比其餘員工得到更慷慨的獎金這一事實:
class Manager extends Employee {
@Override Money computeBonus() {... // special computation}
... }
- 這個解決方案有幾個問題。 全部
Manager
對象得到相同的獎金。 若是咱們想改變管理者之間的獎金計算怎麼辦?引入Manager
的特殊子類?
class SeniorManager extends Manager {
@Override Money computeBonus() {... // more special computation}
...
}
- 若是咱們想改變特定員工的獎金計算會怎樣? 例如,若是咱們想要將史密斯從
Manager
推廣到SeniorManager
,該怎麼辦?
- 若是咱們決定讓全部
Manager
得到與Programmer
相同的獎金呢? 咱們是否應該將Programmer
中的計算算法複製並粘貼到Manager
中?
- 核心問題:每一個Employee對象的獎金計算方法都不一樣;若是能在object層面實現必定比class層面靈活不少。
- CRP的解決方法:
- 只需針對不一樣子類的對象,委派可以計算該子類的獎金的方法的BonusCalculator。這樣一來就不須要在子類繼承的時候進行重寫。
- 【總結】組合來代替繼承的更廣泛實現:
- 用接口來實現系統的最基礎行爲
- 接口之間用extends來實現系統功能的擴展(接口組合)
- 類implements 組合接口
【委派的類型】
- 臨時性委派(Dependency):最簡單的方法,調用類裏的方法(use a),其中一個類使用另外一個類而不實際地將其做爲屬性。
- 永久性委派(Association):類之中有其它類的具體實例來做爲一個變量(has a)
- 更強的委派,組合(Composition):更強的委派。將一些簡單的對象組合成一個更爲複雜的對象。(is part of)
- 聚合(Aggregation):對象是在類的外部生成的,而後做爲一個參數傳入到類的內部構造器。(has a)
【組合與聚合】
在組合中,當擁有的對象被破壞時,被包含的對象也被破壞。在聚合中,這不必定是真的。以生活中的事物爲例:大學擁有多個部門,每一個部門都有一批教授。 若是大學關閉,部門將不復存在,但這些部門的教授將繼續存在。 一位教授能夠在一個以上的部門工做,但一個部門不能成爲多個大學的一部分。大學與部門之間的關係即爲組合,而部分與教授之間的關係爲聚合。
## 設計可複用庫與框架
之因此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原則
- 方法的設計:不要讓客戶作任何模塊能夠作的事情,及時報錯
【框架】(內容參考白盒框架與黑盒框架)
- 框架(Framework)是整個或部分系統的可重用設計,表現爲一組抽象構件及構件實例間交互的方法;另外一種定義認爲,框架是可被應用開發者定製的應用骨架。前者是從應用方面然後者是從目的方面給出的定義。
- 爲了增長代碼的複用性,可使用委派和繼承機制。同時,在使用這兩種機制增長代碼複用的過程當中,咱們也相應地在不一樣的類之間增長了關係(委派或繼承關係)。而對於一個項目而言,各個不一樣類之間的依賴關係就能夠看作爲一個框架。一個大規模的項目可能由許多不一樣的框架組合而成。
- 框架與設計模式:
- 框架、設計模式這兩個概念總容易被混淆,其實它們之間仍是有區別的。構件一般是代碼重用,而設計模式是設計重用,框架則介於二者之間,部分代碼重用,部分設計重用,有時分析也可重用。在軟件生產中有三種級別的重用:內部重用,即在同一應用中能公共使用的抽象塊;代碼重用,即將通用模塊組合成庫或工具集,以便在多個應用和領域都能使用;應用框架的重用,即爲專用領域提供通用的或現成的基礎結構,以得到最高級別的重用性。
- 框架與設計模式雖然類似,但卻有着根本的不一樣。設計模式是對在某種環境中反覆出現的問題以及解決該問題的方案的描述,它比框架更抽象;框架能夠用代碼表示,也能直接執行或複用,而對模式而言只有實例才能用代碼表示;設計模式是比框架更小的元素,一個框架中每每含有一個或多個設計模式,框架老是針對某一特定應用領域,但同一模式卻可適用於各類應用。能夠說,框架是軟件,而設計模式是軟件的知識。
- 框架分爲白盒框架和黑盒框架。
- 白盒框架:
- 白盒框架是基於面向對象的繼承機制。之因此說是白盒框架,是由於在這種框架中,父類的方法對子類而言是可見的。子類能夠經過繼承或重寫父類的方法來實現更具體的方法。
- 雖然層次結構比較清晰,可是這種方式也有其侷限性,父類中的方法子類必定擁有,要麼繼承,要麼重寫,不可能存在子類中不存在的方法而在父類中存在。
- 軟件構造課程中有關白盒框架的例子:
public abstract class PrintOnScreen {
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow());
frame.dispose();
}
protected abstract String textToShow();
}
public class MyApplication extends PrintOnScreen {
@Override protected String textToShow() {
return "printing this text on " + "screen using PrintOnScreen " + "white Box Framework";
}
}
- 經過子類化和重寫方法進行擴展(使用繼承);
- 通用設計模式:模板方法;
- 子類具備主要方法但對框架進行控制。
- 容許擴展每個非私有方法
- 須要理解父類的實現
- 一次只進行一次擴展
- 一般被認爲是開發者框架
- 黑盒框架:
- 黑盒框架時基於委派的組合方式,是不一樣對象之間的組合。之因此是黑盒,是由於不用去管對象中的方法是如何實現的,只需關心對象上擁有的方法。
- 這種方式較白盒框架更爲靈活,由於能夠在運行時動態地傳入不一樣對象,實現不一樣對象間的動態組合;而繼承機制在靜態編譯時就已經肯定好。
- 黑盒框架與白盒框架之間能夠相互轉換,具體例子能夠看一下,軟件構造課程中有關黑盒框架的例子,更改上面的白盒框架爲黑盒框架:
public interface TextToShow {
String text();
}
public class MyTextToShow implements TextToShow {
@Override
public String text() {
return "Printing";
}
}
public final class PrintOnScreen {
TextToShow textToShow;
public PrintOnScreen(TextToShow tx) {
this.textToShow = tx;
}
public void print() {
JFrame frame = new JFrame();
JOptionPane.showMessageDialog(frame, textToShow.text());
frame.dispose();
}
}
-
-
- 經過實現插件接口進行擴展(使用組合/委派);
- 經常使用設計模式:Strategy, Observer ;
- 插件加載機制加載插件並對框架進行控制。
- 容許在接口中對public方法擴展
- 只須要理解接口
- 一般提供更多的模塊
- 一般被認爲是終端用戶框架,平臺