可維護性的軟件構造技術
一.可維護性的常見度量指標:
- 圈複雜度:圈複雜度大說明程序代碼可能質量低且難於測試和維護
- 代碼行數
- 可維護性指數(MI):0-100 利用公式計算
- 繼承的層次數:層次越多越不好維護。CRP原則,儘量使用代理而不是繼承。
- 類之間的耦合度
- 單元測試的覆蓋度
二.聚合度與耦合度:
1.耦合度:衡量兩個模塊間的依賴關係,即其中一個模塊的變化是否影響另一個
影響因素:模塊間接口的數量和每個接口的複雜程度
2.內聚:衡量模塊內的方法或屬性的關聯關係的強弱
模塊之間聯繫越緊密,其耦合性就越強,模塊之間獨立性則越差
追求高內聚低耦合
三.OO設計原則:SOLID
- (SRP) The Single Responsibility Principle 單一責任原則
- (OCP) The Open-Closed Principle 開放-封閉原則
- (LSP) The Liskov Substitution Principle Liskov替換原則
- (DIP) The Dependency Inversion Principle 依賴轉置原則
- (ISP) The Interface Segregation Principle 接口聚合原則
-
SRP:不應該有多於1個原因讓你的ADT發生變化,否則就拆分開
否則會導致:1. 引入額外的包,佔據資源 2. 導致頻繁的重新配置、部署等 -
OCP:開放封閉原則
對擴展性的開放:模塊的行爲應是可擴展的,從而該模塊可表現出新的行爲以滿足需求的變化
對修改的封閉:1. 模塊自身的代碼是不應被修改的。 2. 擴展模塊行爲的一般途徑是修改模塊的內部實現。3. 如果一個模塊不能被修改,那麼它通常被認爲是具有固定的行爲 -
LSP
子類型必須能夠替換其基類型
派生類必須能夠通過其基類的接口使用,客戶端無需瞭解二者之間的差異 -
ISP:接口隔離原則
不能強迫客戶端依賴於它們不需要的接口:只提供必需的接口
將含有多個功能的接口分解爲多個小接口,不同的接口向不同的客戶端提供服務,客戶端只訪問自己所需要的端口
//bad example (polluted interface)
interface Worker {
void work();
void eat();
}
ManWorker implements Worker {
void work() {…};
void eat() {…};
}
RobotWorker implements Worker {
void work() {…};
void eat() {//Not Appliciable for a RobotWorker};
}
//Solution: split into two
interface Workable {
public void work();
}
interface Feedable{
public void eat();
}
//只需實現對應的接口即可
- DIP:依賴轉置原則
delegation的時候,要通過interface建立聯繫,而非具體子類
健壯性
健壯性:系統在不正常輸入或不正常外部環境下仍能夠表現正常的程度
正確性:程序按照spec加以執行的能力,是最重要的質量指標!
健壯性:儘可能保持軟件運行而不是總是退出
正確性:永不給用戶錯誤的結果
健壯性與正確性的比較
正確性傾向於直接報錯(error),健壯性則傾向於容錯(fault-tolerance)
健壯性:
讓用戶變得更容易:出錯也可以容忍,程序內部已有容錯機制
正確性:
讓開發者變得更容易:用戶輸入錯誤,直接結束。(不滿足precondition的調用)
對外的接口,傾向於健壯;對內的實現,傾向於正確
錯誤與異常
Error:程序員通常無能爲力,一旦發生,想辦法讓程序優雅的結束
發生的原因:用戶輸入錯誤,設備錯誤,物理限制
Exception:程序的問題,可以捕獲處理
異常處理
異常:程序執行中的非正常事件,程序無法再按預想的流程執行
將錯誤信息傳遞給上層調用者,並報告“案發現場”的信息
return之外的第二種退出途徑
異常的種類:
- 運行時異常:由程序員在代碼裏處理不當造成
程序源代碼中引入的故障所造成的 ;例如:ArrayIndexOutOfBoundsException, NullPointerException
如果在代碼中提前進行驗證,這些故障就可以避免 - 其他異常:由外部原因造成
程序員無法完全控制的外在問題所導致的;例如:FIleNotFoundException
即使在代碼中提前加以驗證(文件是否存在),也無法完全避免失效發生。
Checked exceptions:
需要從Exception派生出子類型
必須捕獲並指定錯誤處理器handler,否則編譯無法通過,類似於靜態類型檢查
Unchecked exceptions:
從RuntimeException派生出子類型
可以不處理,編譯沒問題,但執行時出現就導致程序失敗,代表程序中的潛在bug,類似於動態類型檢查。
也可捕獲,但不需要----相當於已知代碼中的錯誤但不進行修改
eg:ArrayIndexOutOfBoundsException,NullPointerException
異常處理中的關鍵字:
Declaring exceptions (throws) 聲明“本方法可能會發生XX異常” —在spec中使用
Throwing an exception (throw) 拋出XX異常
Catching an exception (try, catch, finally) 捕獲並處理XX異常
Checked exceptions和Unchecked exceptions的區別
子類型異常處理規範:(LSP)
- 如果子類型中override了父類型中的函數,那麼子類型中方法拋出的異常不能比父類型拋出的異常類型更寬泛
- 子類型方法可以拋出更具體的異常,也可以不拋出任何異常
- 如果父類型的方法未拋出異常,那麼子類型的方法也不能拋出異常。
finally語句:
當異常拋出時,方法中正常執行的代碼被終止
如果異常發生前曾申請過某些資源,那麼異常發生後這些資源要被恰當的清理
try-catch-finally結構
- 當try中代碼不拋出異常時
不管程序是否碰到異常,finally都會被執行 - try中代碼拋出異常被catch捕獲
處理結束後仍會執行finally
但如果catch塊中出現throws異常 上圖只會執行1 3 5
- try中代碼拋出異常但未被catch捕獲
出現異常後,try中剩餘的語句會被跳過,然後finally執行,並將異常返回給調用者
eg:如下程序只執行1.5
斷言與防禦式編程
避免引入bug
- 靜態類型檢查
- 動態類型檢查
- 不可變性
- 不可變的值:final修飾
- 不可變引用:final修飾
限制bug作用範圍
- 限定在一個方法內部,不擴散
- fail fast:儘快失敗,就容易發現、越早修復
Pre-condition如果違反,該方法可以做任何事
應該儘可能早的指出client的bug
斷言Assertions
斷言:在開發階段的代碼中嵌入,檢驗某些“假設”是否成立。若成立,表明程序運行正常,否則表明存在錯誤。出現AssertionError,意味着內部某些假設被違反了
使用斷言的主要目的是爲了在開發階段調試程序、儘快避免錯誤
eg:檢測pre-condition 是否成立
assertion使用規範:
assert condition ;
//所構造的message在發生錯誤時顯示給用戶,便於快速發現錯誤所在
assert condition : message;
可檢測的情況:
- 內部不變量:assert x>0;
- 表示不變量:checkRep();
- 控制流不變量:在控制流不應該到達的位置添加斷言 eg:switch-case中的default
- 方法的前置條件
- 方法的後置條件
不可使用的情況:
// don't do this:
x = y + 1;
assert x == y+1;
// don't do this: 因爲代碼中的assert可被disable,需要執行的程序結構不能在assert中
assert list.remove(x);
// do this:
boolean found = list.remove(x);
assert found;
斷言非常影響運行時的性能,可使用-da進行disable
斷言與異常的比較
使用異常來處理你“預料到可以發生”的不正常情況
使用斷言處理“絕不應該發生”的情況
如果參數來自於外部(不受自己控制),使用異常處理
如果來自於自己所寫的其他代碼,可以使用斷言來幫助發現錯誤,檢測非public類中的前置條件和所有方法中的postcondition
斷言和異常處理都可以處理同樣的錯誤
開發階段用斷言儘可能消除bugs,在發行版本里用異常處理機制處理漏掉的錯誤