2011-09-14 24 views
4

我想知道你對使用流暢接口模式重構長方法的看法。用流暢的接口重構長方法

http://en.wikipedia.org/wiki/Fluent_interface

流暢的圖案不包含在重構的書籍。

例如,假設你有這種長法(用長的名字,因爲它 做許多事)

class TravelClub { 

    Receipt buyAndAddPointsAndGetReceipt(long amount, long cardNumber) { 
    buy(amount); 
    accumulatePoints(cardNumber); 
    return generateReceipt(); 

    } 

    void buy(int amount) {...} 

    void accumlatePoints(int cardNumber) {...} 

    void generateRecepit() {...} 

} 

稱爲:

Receipt myReceipt = myTravelClub.buyAndAddPointsAndGetReceipt(543L,12345678L); 

可能被重構爲:

class TravelClub { 

    TravelClub buy(long amount) { 
    //buy stuff 
    return this; 
    } 

    TravelClub accumulatePoints(long cardNumber) { 
    //accumulate stuff 
    return this; 
    } 

    Receipt generateReceipt() { 
    return new Receipt(...); 
    } 


} 

並呼籲:

Receipt myReceipt = myTravelClub.buy(543L).accumulatePoints(12345678L).generateReceipt(); 

從我的角度來看,這是一個很好的方式來分解長方法和 也分解它的名字。

你覺得呢?

+1

只是好奇...是不是門面模式的這種相反,你試圖通過提供簡單的使用方法或接口來隱藏有多個方法或接口的複雜性。 –

+0

@Sandeep,謝謝你的提示。沒錯,但是國際海事組織認爲這種模式是非常大的(加上它的名字,加上參數的數量) – ejaenv

回答

0

使用單一方法的優點是始終調用相同的序列。例如,您可以像在您提供的流暢界面示例中那樣跳過accumulatePoints

如果調用這些方法的唯一方法將與第一個代碼塊中的順序相同,請將其保留爲單個函數。但是,如果在生成收據之前可以在TravelClub上完成操作的任何子集,那麼通過所有方法都可以使用流暢的接口。這是克服「組合爆炸」代碼異味的最好方法之一(如果不是最好的話)。

3

我的反問是那麼,什麼是預期的行爲,如果有人改爲調用:

myTravelClub.accumulatePoints(10000000L); 

,而不調用買?或者在購買前生成收據?我認爲流暢的接口仍然需要遵守其他面向對象的約定。如果你真的想要一個流暢的界面,那麼buy()方法將不得不返回另一個對象,而不是TravelClub本身,而是一個具有accumulatePoints()generateReceipt()方法的「購買對象」。

也許我正在閱讀您的示例的語義,但有一個原因,爲什麼維基百科示例具有邏輯上可以按任何順序調用的方法。我認爲Hibernate標準API是另一個很好的例子。

5

它有一個問題,你必須記住積累點和執行購買(並生成收據,這是一個問題較少,因爲我認爲行動沒有副作用)。在我看來,積分應該在進行購買時自動進行。進行購買時收到一張收據也是很自然的,所以從某種意義上說,您的初始方法很好,除非它讀得不好。

如果你想有一個流暢的界面我引入一個額外的類,它輕輕地引導客戶端代碼到做正確的事情(假設所有購發生了卡累計積分相同):

class TravelClub { 

    OngoingPurchase buyAmount(long amount) { 
     return new OngoingPurchase(amount); 
    } 

    private Receipt buyAndAddPointsAndGetReceipt(long amount, long cardNumber){ 
     // make stuff happen 
    } 

    public class OngoingPurchase { 
     private final long amount; 
     private OngoingPurchase(long amount){ 
     this.amount = amount; 
     } 
     public Receipt withCard(long cardNumber){ 
     return buyAndAddPointsAndGetReceipt(long amount, cardNumber); 
     } 
    } 

} 

// Usage: 
Receipt receipt = travelClub.buyAmount(543).withCard(1234567890L); 

這樣,如果您忘記撥打withCard,則什麼都不會發生。找到丟失的交易比錯誤的交易更容易,並且如果沒有執行完整的交易就無法得到收據。

編輯:順便說一句,這很有趣,認爲我們做的這一切工作,使許多參數可讀,方法時,例如命名參數將使問題完全消失:

Receipt r = travelClub.makePurchase(forAmount: 123, withCardNumber: 1234567890L); 
0

只要你使用了正確的驗證,流利的接口更容易理解,例如,它可以像如下,

類TravelClub {

TravelClub buy(long amount) { 
    buy(amount); 
    return this; 
    } 

    TravelClub accumulatePoints(long cardNumber) { 
    if (!bought) 
    { 
     throw new BusinessException("cannot accumulate points if not bought"); 
    } 
    accumulatePoints(cardNumber); 
    return this; 
    } 

    Receipt generateReceipt() { 
    if (!bought) 
    { 
     throw new BusinessException("cannot generate receipts not bought"); 
    } 
    return new Receipt(...); 
    } 
} 
2

很長的方法與長名稱的方法不一樣。在你的情況,我會改變的僅僅是方法名稱:

public Receipt buy(long amount, long cardNumber) { 
    buy(amount); 
    accumulatePoints(cardNumber); 
    return generateReceipt(); 
} 

(並認爲私營buy方法更具描述性的名稱),因爲所有的三件事(「買入」,accumulatePoints和獲取收據)總是發生在一起,因此從調用代碼的角度來看,它們可以是單個操作。從實施的角度來看,單一操作也更容易。 KISS :-)

+0

好的風格(Clean Code book)說方法的名字應該說程序員該做什麼。所以IMO長期方法涉及長名稱。也許這是一個在stackoverflow中的新問題。 – ejaenv

+0

感謝您的意見。但是單個操作會導致不止一個抽象級別。這就是爲什麼長方法是以較小的方法分解的原因。 – ejaenv

+1

是的,它應該說明該方法的作用,但是在什麼級別的細節中?收據是「生成」還是僅僅是我購買東西時得到的東西?是不是我從方法返回類型獲得顯而易見的接受者? – meriton

0

在我看來,困難的一部分在於選擇一個很好的描述性名稱,它涵蓋了方法所做的一切。這個問題自然是有時你有很多複雜的邏輯,你不能用一個簡單的名字來描述。

在你的代碼示例所構成的情況下,我會忍不住簡化方法本身的東西多一點廣義的名稱:

Receipt Transaction(long amount, long cardNumber) 
{ 
    buy(amount); 
    accumulatePoints(cardNumber); 
    return generateReceipt(); 
} 

所以這個邏輯問題,我提到什麼?這本身就歸結爲你的方法是否在它的功能上很固定。如果只能使用買入 - >點 - >收貨序列完成交易,則可以使用更簡單的名稱,但更具描述性的名稱也適用,並且流暢的界面可能是合理的選擇。

客戶沒有獎勵卡或不想收到回執的情況如何?那些在一次交易中可能會購買多件物品的情況呢?當然,假設購買方法可能代表一種購買物品,而不僅僅是一個在其他地方計算的總量。一旦你開始在序列中引入問題/選擇,設計就會變得不那麼明顯,而且命名更加困難。你肯定不希望使用一個瘋狂的長名字一樣:當然

BuyAndAddPointsIfTheCustomerHasACardAndReturnAReceiptIfTheCustomerAsksForIt(...) 

,它會告訴你到底它做什麼,但它在這也凸顯了一個潛在的問題的方法可能是太多的東西負責,或者它可能隱藏了它調用的其中一種方法後面的更復雜的代碼異味。同樣,一個簡單的方法名稱,例如「交易」可能會過度簡化需要更好理解的複雜問題。

流暢的接口在這裏可以帶來很大的好處,只要它指導開發人員就如何應用流式方法進行明智的決策。如果調用序列很重要,則需要將返回類型限制爲序列中的下一個選項。如果調用序列不那麼重要,那麼可以使用具有更一般化接口的返回類型,該接口允許按任意順序調用選擇的方法。關於是否要使用流暢的界面,我認爲它不應該僅僅作爲一種分解困難的命名方法的手段來決定。你正在做一個設計選擇,你將需要在產品的整個生命週期中一起生活,並且從維護的角度來看,我發現流暢的界面可以使設計更加難以可視化,組織和維護碼。最終,您需要決定這是否可以與您提供的好處進行權衡。對我而言,我通常首先詢問用例組合是固定和簡單的,還是相對無窮無盡。如果是後者,流暢的界面可能有助於保持代碼更清潔並且更易於在多種情況下使用。我還會考慮代碼是否屬於更通用的層,例如API(例如流暢接口可能很好地工作)或更專門的某個層。

相關問題