1. 程式人生 > >軟體構造-面向可複用性的程式設計-複習筆記

軟體構造-面向可複用性的程式設計-複習筆記

 

這篇部落格主要摘錄我在複習軟體構造的可複用部分的感悟以及對一些重點內容的摘錄

可複用主要包括了兩個重點內容:

  • 繼承與面對物件程式設計
  • 設計模式

其餘部分就是以這兩個重點為核心的一些補充,這也是軟體構造的的一個非常重要的思想部分

 

這一部分內容實際上是對前面ADT,specification相關內容的繼續,那些內容制定了程式設計過程中的一些規範,這一章就是在規範的基礎上研究軟體的頂層設計。正因為有了specification的保護,使得我們可以不用關心類的內部實現,而能夠把注意力放在功能塊之間的設計上來。

 

繼承與OOP

介面,繼承與泛型

 關於繼承的約定

如果B是A的子型別,那麼A與B之間應該有這樣的關係:

  • 所有的B都是A

而這個關係在程式中的體現就是:

  • B的specification一定要強於A的specification

而這個約定又分為兩種,一種是能夠通過靜態檢查發現的。也就是A中所有的方法都必須在B中出現,而且方法的返回值,簽名等等都必須相同。

當我們使用extends關鍵詞來定義A的子型別的時候,靜態檢查會幫我們保證這一性質。

但是還有一種是靜態檢查所發現不了的,也就是pre-condition和post-condition

繼承保證了共性,但是子型別也一定有其個性。子型別可以通過新增自己的方法來為自己新增細化的性質,也可以重寫父類的方法,因此就有可能改變父類的方法的precondition與postcondition

要想滿足B的specificatioin強於A的specifition這一性質,就必須保證:

  • B的重寫方法的precondition不能強於A中的原方法
  • B的重寫方法的postcondition不能弱於A中的原方法

這是ide無法靜態檢查得到的,需要程式設計師去保證。

一道例題

MIT的readingd提供了一道例題:

這道例題可以這樣思考

題目中父類是rectangle,然後嘗試新增一個square的子類。

乍一想是很合理的,每一個square當然是一個rectangle了,specification明明強化了——多了rectangle相鄰邊必須相等的保證。

但是square中對rectangle的setSize方法的重寫,卻沒有一個能夠滿足條件的,為什麼?

因為題中給定是mutable的型別,應該這樣想:

每一個square一定是一個rectangle,但是能夠任意設定兩條邊長的square並不是能夠任意設定兩條邊長的rectangle

 

另一道例題

這題選B

我是這麼理解了,

Double對Number是加強了postCondition,所以是符合的

事實上根據老師上課時ppt5-2中的內容,在繼承關係中,子類中對父類方法的重寫,變數可以逆變是沒有錯的,只是在java中並不支援這種寫法。

 

因此,我們在需要用到變數的逆變的時候,應該不使用@Override標記,而是直接改寫。這樣對編譯器來說是父類方法的過載,只是在概念上我們會將之當成重寫罷了。

根據Liskov替換原則,這樣的改寫是滿足條件的,也就是如果把任何對父類的引用改成子類,也不會產生任何問題,子類的結果只會更嚴格。

 

關於Liskov原則的理解

LIskov對繼承的規定有如下幾點:

  • 前置條件不能強化
  • 後置條件不能弱化
  • 不變數必須保持
  • 子型別方法引數:逆變
  • 子型別方法返回值:協變
  • 異常型別:協變

其實我傾向於將子型別方法引數與子型別方法返回值的規定歸於對前置條件與後置條件限制

也就是:協變是一種強化,縮小範圍,更加具體。而逆變是一種弱化,擴大範圍,更加抽象。

在此我有一個疑問:為什麼liskov原則不把子型別方法的引數與變數歸於前置條件與後置條件呢?

泛型的繼承規則

一個讓初學者感到出人意料的事實:

ArrayLIst<String> 是 List<String> 的子型別

List<String> 不是 List<Object> 的子型別

java是完全遮蔽了泛型中的繼承檢查,只要是不一致的型別,都會被簡單的當成不同型別

除非用上萬用字元 <?>

 <?> 是任意E : <E> 的超型別

 

 這樣也沒有問題:

 

 這樣也可以:

通過這幾個小例子可以發現,使用了萬用字元的泛型與外面的型別是同等進行靜態檢查的。

也就是說: ArrayList<Integer> 是 List<?> 的子型別,因為內外都是

 

關於equality的討論

在離散數學當中,等價性的原則有三

  • 自反性
  • 對稱性
  • 傳遞性

而在java當中,不同ADT的等價性在滿足這幾個條件的基礎上,還存在著更加嚴格的要求。

我在這裡說的是ADT的等價性而不是物件的等價性,因為物件等價的概念是物理層面的,簡單的理解就是呼叫“==”或者equals()方法能夠得到真值,詳細一些來說就是從java的執行時角度來看,兩個物件的資料值是一樣的。

而ADT則是抽象層面的東西,是我們在設計程式的時候自己定義出來的,可能有不同的規則:
比如說,如果我在討論動物與植物,那麼我可以在概念上把兔子和猴子看成等價的,而猴子和樹是不等價的。

而我們在設計ADT時,就必須使我們定義的等價遵循上面的三個條件。

 

額外的規則

程式設計中的等價是為程式行為服務的,因此應該具有更加多的限制

  • equality應當遵循等價的基本原則,也就是自反,對稱和傳遞,這是基礎
  • equals應當具有行為連續性,如果equals中比較的元素的值沒有改變,那麼對該例項的重複呼叫某一個方法應當始終返回相同的結果。
  • 若x不是null,x.equals(null)應當永遠返回false
  • equals應當與hashcode的比較具有相同的結果

觀察等價性與行為等價性

觀察等價性

即“看起來一樣”。

指的是兩個引用,如果它們是觀察等價的,在不對它們呼叫mutator方法的情況下,任何obsever方法的呼叫都無法區分這兩個引用。

也就是說,client可以只通過observers就區分兩個具有觀察等價性的例項物件。

觀察等價性側重於兩個物件在程式執行的當前時刻是處於相同狀態的

行為等價性


即“用起來一樣”

對於哪怕引用,哪怕呼叫了mutators方法,兩個ADT的行為始終是一樣的(這個行為當然也可以包括observers,所以行為等價性是包含觀察等價性的)。

行為等價性側重於兩個物件現在是一樣的,在未來也會一直是一樣的。

行為等價性的規定如此嚴格,一般來說,只有兩個指向同一個物件的引用才能滿足這個條件。

這對mutable物件來說是合理的,因為mutable物件當中存在著太多的不確定性,比如說經典的“將list放入set中”的例子

 設計模式

設計模式相關的內容汗牛充棟,對於設計模式本身,我沒有什麼好補充的,因為我寫部落格的目的是記錄感想大於總結知識點,我主要想要談一下對設計模式的理解。

設計模式是頂層設計。

如果說繼承與面對物件是程式碼層面的複用,那麼設計模式就是模組層面的複用,通過一些很好用的框架來方便我們的程式的編寫。

起初我對設計模式十分抵觸。原因是我對java語法的掌握不夠熟練,因此在使用設計模式中的工廠模式的時候,在繼承,泛型,型別轉換的各種錯誤之中摸爬滾打了很久,吃了很多苦,最後也沒有體會到工廠模式的妙處所在,反而為其所累。

我是這麼想的:

  • 設計模式不是目的,如果為了使用設計模式而使用設計模式,反而會使程式更加複雜而難以理解。

我的程式碼量實在太小,功能也很簡單,所以沒有嚐到設計模式的好處。

&n