程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: Fictitious Implementation錯誤模式,第1部分

診斷Java代碼: Fictitious Implementation錯誤模式,第1部分

編輯:關於JAVA

Java 語言接口是一種強大的工具。它具有多繼承的很多優點,而沒有什麼問題。為客戶希望使用的所有服務指定一個接口,使得在需要時插進這種接口的不同實現成為可能。

遺憾的是,規范中可以被表達的部分只有方法說明。對任何實現來說,很可能還有很多其它不變量希望被掌握,但是 Java 語言沒有提供檢查它們的工具。

臆想錯誤模式

由於這種限制,很可能“實現”了一個接口而實際上沒有滿足預期的語義。由這種 Fictitious Implementation導致的錯誤就是本周專欄的主題。

例如,請看一看下面這個堆棧的接口:

清單 1. 堆棧的接口

public interface Stack {
  public Object pop();
  public void push(Object top);
  public boolean isEmpty();
}

從 Java 類型檢查器的角度看,包含符合如上說明的方法的任何類可以作為 Stack 的合法實現。但是實際上,我們希望堆棧能滿足一些另外的要求。例如:

如果一個對象 o 被壓入堆棧 s ,並且在堆棧上進行的下一步操作是 pop ,那麼這個操作的返回值應該是 o 。

如果對於一個給定的堆棧 s , s.isEmpty() 的返回值是 true ,並且在這個堆棧上進行的下一步操作是 pop ,那麼調用 pop 應該拋出一個 RuntimeException 異常。

還有大量其它的可以指定的不變量。我們希望堆棧怎麼處理多次 push 操作?對於多線程會有什麼行為?很難通過編程來實施這些不變量。我們可以(並且應該)在文檔編寫時提及它們,但是編寫實現的開發者可能容易忽略它們。如果發生這種情況,那麼依賴這些不變量的客戶將不能完成這種實現,就形成了錯誤。我稱這種模式的錯誤為 Fictitious Implementation ,因為我公正地將其歸咎於實現而不是客戶。正如任何錯誤都有自己的模式一樣,Fictitious Implementation 可能不能立刻看出,而是潛伏,一直隱藏到某種不平常的執行路徑發現它。

不要責怪 Java 語言!

在繼續這篇專欄前,我要指出我並不是批評 Java 語言不能指定這種不變量。允許這種規范的任何機制都會有很多隨之而來的缺點。首先,我們想要指定的很多不變量不能被靜態地檢查。雖然類型說明只表達了不變量的一小部分,但是比上面我們概述的用於堆棧的這類約束容易檢查。

在接口中允許更多可表達規范有另一方面的缺點:這樣做,很容易讓 Java 語言背負很多問題,使得語言中到處都是多繼承。請看下面的接口:

清單 2. 彈出器接口

public interface Popper {
  public Object pop();
}

假設這個接口的 pop() 方法有預期的不變量:調用 pop() 絕不會拋出一個 RuntimeException 異常。現在,利用當前 Java 的接口功能,一個類可能同時實現 Stack 和 Popper 。但是,根據預期的規范,每一個類中的 pop() 實現是相互不兼容的。

如果想繼續讓一個類同時實現這兩種接口,還必須能夠提供重載方法。這種方法不僅基於類型說明,而且基於給這種語言添加的額外的不變量規范的種類。但是這將引入一個嚴重的問題:一個給定的方法調用,我們怎麼確定調用這種方法的哪一個版本?通常,這是通過確定方法參數的靜態類型來完成;但是有了額外的不變量,這種技巧就不行了。這種問題對於常進行多繼承編程的程序員來說很熟悉:如果一個以上的父類定義的方法帶有相同名稱和類型說明,怎樣消除調用這些方法的歧義呢?有很多可以手工消除這種調用歧義的方法,但是都易使語言的復雜性大大增加。更糟糕的是,自動地消除這種調用歧義的方案本身很復雜,且易產生錯誤,當預測哪個方法將被調用時,程序員經常犯錯。

因此,選擇將接口不變量的規范限制到類型說明是完全合理的設計。我們沒有必要為了檢測和更正Fictitious Implementation 而放松這種限制。

檢測Fictitious Implementation

當然,Fictitious Implementation 的主要問題是它能順利通過編譯。運行時的症狀通常會令人非常莫名其妙,因為程序員期望接口滿足的額外的不變量經常不被表示出來;程序員甚至可能沒有意識到正希望滿足這些不變量。更正錯誤的過程通常以一個混亂的階段開始;為Fictitious Implementation 模式所絆的程序員可能首先試圖說服他自己他所看到的問題不可能出現。如果您發現自己正處於這種狀況,最好檢查一下您的前提。您沒有提到哪些隱藏的假設呢?怎麼能測試這些假設來徹底消除它們是錯誤的可能性呢?如果您依賴於到系統另一部分的接口,並且該接口的實現自最近一次發布已經修改過了,那麼您可能會碰到Fictitious Implementation 。

解決方法

在這樣一些情況下,接口的維護人員將任何可由客戶程序員假設的不變量 做成文檔是很重要的。事實上,如果客戶程序員發現他所依賴的不變量沒有做成文檔,那麼客戶程序員和接口的維護人員應該坐下來討論一下是否應該讓這種不變量明確些。通常,將一個假設的不變量添加到規范很容易,省去了客戶修改所有依賴這些不變量的代碼的麻煩。

如果接口的維護人員不在,那麼客戶只能依賴已經做成文檔的接口不變量。如果缺乏這些,接口遠遠達不到它所應有的價值。如果客戶選擇依賴沒有做成文檔的不變量,所寫的客戶代碼本身可能很快失去價值,因為它可能與接口實現的未來的發行版不兼容。

如果您在設計一個接口,您有兩個非常強大的工具來防止這種模式的錯誤:單元測試和斷言。在這個系列的第二部分,我將討論這些技術怎麼能被用作一種可執行的文檔來幫助實施接口的不變量。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved