怎樣設計合適的接口(1)

摘要:我們在設計系統接口時,經常會遇到這樣的問題:
  我們的接口應該提供多少方法才合適?
  我們的接口應該提供"原子方法"還是"複合方法"?
  我們的接口是否應該封裝(或者,能否封裝)所有的細節?
  接口的設計需要考慮用戶的使用習慣、使用的方便程度、使用的安全程度,根據我的編程經驗,下面會詳細討論接口設計的2個需要權衡的方面:接口的單一化 & 複合化。
  接口
  接口提供了不同系統之間或者系統不同組件之間的界定。在軟件中,接口提供了一個屏障,從而從實現中分離目標,從具體中分離抽象,從作者中分離用戶。
  站在用戶的角度看,一個接口建立並命名了一個目標對象的使用方法。一些約束(例如:編譯時的類型系統、運行時的異常機制及返回值)使得類作者的目的得以體現和加強。供給(affordances)指事物的被感知的真實的屬性,這些屬性可以決定事物使用的可能方法,供給提供了對事物操作的線索。
  類設計者的一個職責便是在接口中減小約束與供給之間的隔閡、匹配目標以及一定程度上的自由度,儘可能減小錯誤使用目標對象的可能。
  封裝
  對於封裝來說,遠不止數據私有那麼簡單。在設計中,封裝往往會涉及到自我包含(self-containment)。如果一個類需要你知道如何調用它方法(e.g. 在一個線程的環境中,在一個方法調用後調用另一個方法,你必須明確地同步對象),那麼它的封裝性就不如將所有這些全部包含並隱藏的類(e.g. 這個類是thread-safe的)好。前一個設計存在着設計的漏洞,它的許多限定條件是模糊的,而且把部分責任推給了用戶,而不是讓類提供者做這些工作來完成類的設計。
  在空間或者時間上分離方法的執行(例如,線程,遠程方法調用,消息隊列),能夠對設計的正確性和效率產生意義深遠的影響。這種分離帶來的結果是不可忽視的:
  併發引入了不確定性和環境(context)選擇的開銷;
  分佈引入了回調的開銷,這些開銷可能不斷增加,而且會導致錯誤。
  這些是設計的問題,修改它們可不是象修改bug那樣簡單。
  如果一個接口主要由存取方法(set和get方法)組成,每個方法都相應的直接指向某個私有域,那麼它的封裝性會很差。接口中的域存取方法通常是不會提供信息的:他們在對象的使用中不能通訊、簡單化和抽象化,這通常會導致代碼冗長,並且容易出錯。
  所以,我們首先考慮接口設計的第一個原則:
  命令與查詢分離(Command-Query Separation)
  要求:保證一個方法不是命令(Command)就是查詢(Query)
  定義:
  查詢:當一個方法返回一個值來回應一個問題的時候,它就具有查詢的性質;
  命令:當一個方法要改變對象的狀態的時候,它就具有命令的性質;
  通常,一個方法可能是純的Command模式或者是純的Query模式,或者是兩者的混合體。在設計接口時,如果可能,應該儘量使接口單一化,保證方法的行爲嚴格的是命令或者是查詢,這樣查詢方法不會改變對象的狀態,沒有副作用(side effects),而會改變對象的狀態的方法不可能有返回值。也就是說:如果我們要問一個問題,那麼就不應該影響到它的答案。實際應用,要視具體情況而定,語義的清晰性和使用的簡單性之間需要權衡。
  例如,在java.util.Iterator中,hasNext可以被看作一種查詢,remove是一種命令,next合併了命令和查詢:
  public interface Iterator{
  boolean hasNext();
  Object next();
  void remove();
  }
  這裏,如果不將一個Iterator對象的當前值向前到下一個的話,就不能夠查詢一個Iterator對象。如果沒有提供一個複合方法next,我們將需要定義一系列的命令方法,例如:初始化(initialization)、繼續(continuation)、訪問(access)和前進(advance),它們雖然清晰定義了每個動作,但是,客戶代碼過於複雜:
  for(initialization; continuation condition; advance){
  ... access for use ...
  }
  將Command和Query功能合併入一個方法,方便了客戶的使用,但是,降低了清晰性,而且,可能不便於基於斷言的程序設計並且需要一個變量來保存查詢結果:
  Iterator iterator = collection.iterator();
  while(iterator.hasNext();){
  Object current = iterator.next();
  ... use current...
  }
  下面,我們考慮接口設計的第二個原則:
  組合方法(Combined Method)
  組合方法經常在線程和分佈環境中使用,來保證正確性並改善效率。
  一些接口提供大量的方法,起初,這些方法看來是最小化的,而且相關性強。然而,在使用的過程中,一些接口顯現得過於原始,它們過於簡單化,從而迫使類用戶用更多的工作來實現普通的任務,並且,方法之間的先後順序及依賴性比較強(即,暫時耦合)。這導致了代碼重複,而且非常麻煩和容易出錯。
  一些需要同時執行成功的方法,在多線程、異常、和分佈的情況下會遇到麻煩。如果兩個動作需要同時執行,它們由兩個獨立的方法進行描述,必須都完全成功的執行,否則會導致所有動作的回滾
  線程的引入使這種不確定性大大增加。一系列方法同時調用一個易變的(mutable)對象,如果這個對象在線程之間共享,即使我們假設單獨的方法是線程安全的,也無法確保結果是意料之中的。看下面對Event Source的接口,它允許安置句柄和對事件的查詢:
  interface EventSource{
  Handler getHandler(Event event);
  void installHandler(Event event, Handler newHandler);
  }
  線程之間的交叉調用可能會引起意想不到的結果。假設source域引用一個線程共享的對象,對象很可能在1、2之間被另一個線程安裝了一個新的句柄
  class EventSourceExample{
  public void example(Event event, Handler newHandler){
  oldHandler = eventSource.getHandler(event); // 1
  //對象很可能在這裏被另一個線程安裝了一個新的句柄
  eventSource.installHandler(event, newHandler); // 2
  }
  private EventSource eventSource;
  private Handler oldHandler;
  }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章