溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JAVA中如何設計合適的接口

發(fā)布時間:2022-01-13 14:56:42 來源:億速云 閱讀:119 作者:小新 欄目:編程語言

這篇文章主要介紹JAVA中如何設計合適的接口,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

我們在設計系統(tǒng)接口時,經(jīng)常會遇到這樣的問題:

我們的接口應該提供多少方法才合適?

我們的接口應該提供"原子方法"還是"復合方法"?

我們的接口是否應該封裝(或者,能否封裝)所有的細節(jié)?

接口的設計需要考慮用戶的使用習慣、使用的方便程度、使用的安全程度,根據(jù)我的編程經(jīng)驗,下面會詳細討論接口設計的2個需要權衡的方面:接口的單一化 & 復合化。


接口

接口提供了不同系統(tǒng)之間或者系統(tǒng)不同組件之間的界定。在軟件中,接口提供了一個屏障,從而從實現(xiàn)中分離目標,從具體中分離抽象,從作者中分離用戶。

站在用戶的角度看,一個接口建立并命名了一個目標對象的使用方法。一些約束(例如:編譯時的類型系統(tǒng)、運行時的異常機制及返回值)使得類作者的目的得以體現(xiàn)和加強。供給(affordances)指事物的被感知的真實的屬性,這些屬性可以決定事物使用的可能方法,供給提供了對事物操作的線索。

類設計者的一個職責便是在接口中減小約束與供給之間的隔閡、匹配目標以及一定程度上的自由度,盡可能減小錯誤使用目標對象的可能。


封裝

對于封裝來說,遠不止數(shù)據(jù)私有那么簡單。在設計中,封裝往往會涉及到自我包含(self-containment)。如果一個類需要你知道如何調用它方法(e.g. 在一個線程的環(huán)境中,在一個方法調用后調用另一個方法,你必須明確地同步對象),那么它的封裝性就不如將所有這些全部包含并隱藏的類(e.g. 這個類是thread-safe的)好。前一個設計存在著設計的漏洞,它的許多限定條件是模糊的,而且把部分責任推給了用戶,而不是讓類提供者做這些工作來完成類的設計。

在空間或者時間上分離方法的執(zhí)行(例如,線程,遠程方法調用,消息隊列),能夠對設計的正確性和效率產生意義深遠的影響。這種分離帶來的結果是不可忽視的:

并發(fā)引入了不確定性和環(huán)境(context)選擇的開銷;

分布引入了回調的開銷,這些開銷可能不斷增加,而且會導致錯誤。

這些是設計的問題,修改它們可不是象修改bug那樣簡單。

如果一個接口主要由存取方法(set和get方法)組成,每個方法都相應的直接指向某個私有域,那么它的封裝性會很差。接口中的域存取方法通常是不會提供信息的:他們在對象的使用中不能通訊、簡單化和抽象化,這通常會導致代碼冗長,并且容易出錯。

所以,我們首先考慮接口設計的第一個原則:

命令與查詢分離(Command-Query Separation)

要求:保證一個方法不是命令(Command)就是查詢(Query)

定義:
  查詢:當一個方法返回一個值來回應一個問題的時候,它就具有查詢的性質;

  命令:當一個方法要改變對象的狀態(tài)的時候,它就具有命令的性質;

通常,一個方法可能是純的Command模式或者是純的Query模式,或者是兩者的混合體。在設計接口時,如果可能,應該盡量使接口單一化,保證方法的行為嚴格的是命令或者是查詢,這樣查詢方法不會改變對象的狀態(tài),沒有副作用(side effects),而會改變對象的狀態(tài)的方法不可能有返回值。也就是說:如果我們要問一個問題,那么就不應該影響到它的答案。實際應用,要視具體情況而定,語義的清晰性和使用的簡單性之間需要權衡。

例如,在java.util.Iterator中,hasNext可以被看作一種查詢,remove是一種命令,next合并了命令和查詢:

public interface Iterator{boolean hasNext();Object next();void remove();}

這里,如果不將一個Iterator對象的當前值向前到下一個的話,就不能夠查詢一個Iterator對象。如果沒有提供一個復合方法next,我們將需要定義一系列的命令方法,例如:初始化(initialization)、繼續(xù)(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)

組合方法經(jīng)常在線程和分布環(huán)境中使用,來保證正確性并改善效率。

一些接口提供大量的方法,起初,這些方法看來是最小化的,而且相關性強。然而,在使用的過程中,一些接口顯現(xiàn)得過于原始,它們過于簡單化,從而迫使類用戶用更多的工作來實現(xiàn)普通的任務,并且,方法之間的先后順序及依賴性比較強(即,暫時耦合)。這導致了代碼重復,而且非常麻煩和容易出錯。

一些需要同時執(zhí)行成功的方法,在多線程、異常、和分布的情況下會遇到麻煩。如果兩個動作需要同時執(zhí)行,它們由兩個獨立的方法進行描述,必須都完全成功的執(zhí)行,否則會導致所有動作的回滾。

線程的引入使這種不確定性大大增加。一系列方法同時調用一個易變的(mutable)對象,如果這個對象在線程之間共享,即使我們假設單獨的方法是線程安全的,也無法確保結果是意料之中的??聪旅鎸vent 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;}

為了解決問題,也需要由類的使用者而不是類的設計者來完成:

class EventSourceExample{public void example(Event event, Handler newHandler){synchronized(eventSource){oldHandler = eventSource.getHandler(event);eventSource.installHandler(event, newHandler);}}private EventSource eventSource;private Handler oldHandler;}

我們假設:目標對象eventSource是遠程的,執(zhí)行每一個方法體的時間和通訊的延遲相比是很短的。在這個例子中,eventSource的方法被調用了兩次,并可能在其他的實例中重復多次,因而,開銷也是至少兩倍。

此外還有一個問題是對外部的synchronized同步塊的使用需求。對synchronized塊的使用之所以會失敗,主要因為我們通過代理對象來完成工作,所以,調用者的synchronized塊,同步的是代理對象而不是最終的目標對象,調用者不可能對其行為做太多的保證。

Combined Method必須在分布的環(huán)境,或者,線程環(huán)境中同時執(zhí)行。它反映了用戶直接的應用,恢復策略和一些笨拙的方法被封裝到Combined Method中,并簡化了接口,減少了接口中不需要的累贅。Combined Method的效果是支持一種更像事務處理風格的設計。

在一個組合的Command-Query中提供一個單獨的Query方法通常是合理的。提供分離的Command方法是不太常見的,因為Combined Method可以完成這一工作,只要調用者簡單的忽略返回結果。如果返回一個結果招致一個開銷的話,才可能會提供一個單獨的Command方法。

回到前一個例子中,如果installHandler method返回上一次安裝的句柄,則設計變得更加簡單和獨立:

interface EventSource{Handler installHandler(Event event, Handler newHandler);}

客戶代碼如下:
class EventSourceExample{public void example(Event event, Handler newHandler){oldHandler = eventSource.installHandler(event, newHandler);}private EventSource eventSource;private Handler oldHandler;}

這樣,我們給調用者提供了一個更加安全的接口,并且不再需要他們解決線程的問題。從而降低了風險和代碼量,將類設計的職責全部給了類設計者而不是推給用戶,即使有代理對象的出現(xiàn)也不會影響到正確性。

一個Combined Method可以是許多Query的集合,許多Command的集合,或者兩者兼有。這樣,它可能補充Command、Query方法,也可能與之相抵觸。當沖突發(fā)生的時候,優(yōu)先選擇Combined Method會產生一個不同的正確性和適用性。

在另一個例子中,我們考慮獲得資源的情況。假設,在下面的接口中,方法acquire在資源可用前阻塞:

interface Resource{boolean isAcquired();void acquire();void release();}

類似于下面的代碼會在一個線程系統(tǒng)中推薦使用:

class ResourceExample{public void example(){boolean acquired = false;synchronized(resource){if(!resource.isAcquired())resource.acquire();elseacquired = true;}if(!acquired)...}private Resource resource;}

然而,即使我們放棄可讀性和易用性,這樣的設計也不是一個Command-Query分離的設計。如果引入了代理,它就會失?。?br/>
class ActualResource implements Resource {...}class ResourceProxy implements Resource {...}

如果用戶既可以通過ActualResource來完成工作,也可以通過ResourceProxy來完成工作,而且,ActualResource和ResourceProxy都沒有處理同步,則synchronized塊可能會失敗。因為,既然我們可以通過代理對象ResourceProxy來完成工作,那么,調用者的synchronized塊,同步的就是代理對象ResourceProxy而不是最終的目標對象ActualResource。

一個Combined Method解決了這個問題,它使并發(fā)和間接性更加透明。

interface Resource{

boolean tryAcquire();

}

下面的代碼清晰、簡單并且正確:

class ResourceExample{public void example(){if(!resource.tryAcquire())...}private Resource resource;}

Combined Method帶來的一個結果是使一些測試和基于斷言的程序設計變得十分笨拙,然而,它適合解決線程和分布問題。

實際應用中,接口應該單一化還是復合化,要視具體情況而定。

以上是“JAVA中如何設計合適的接口”這篇文章的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

AI