2011-09-14 44 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(例如流畅接口可能很好地工作)或更专门的某个层。