使用流畅的接口重构长方法

时间:2011-09-14 07:11:08

标签: java refactoring fluent fluent-interface

我想知道您对使用流畅的界面模式重构一个长方法的看法。

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();

从我的观点来看,这是一种很好的分解长方法的方法 也要分解它的名字。

你觉得怎么样?

6 个答案:

答案 0 :(得分: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);

答案 1 :(得分:3)

那么我的反问题是,如果有人反而打电话,预期的行为是什么:

myTravelClub.accumulatePoints(10000000L);

没有打电话给买?或者在购买前生成收据?我认为流畅的接口仍然需要遵守其他OO约定。如果你真的想要一个流畅的界面,那么buy()方法必须返回另一个对象,而不是TravelClub本身,而是一个具有accumulatePoints()generateReceipt()方法的“购买对象”。

也许我正在阅读你的例子的语义,但是有一个原因可以解释为什么维基百科示例具有逻辑上可以按任何顺序调用的方法。我认为Hibernate标准API是另一个很好的例子。

答案 2 :(得分:2)

long方法与长名称的方法不同。在您的情况下,我唯一要改变的是方法名称:

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

(并考虑私有buy方法的更具描述性的名称)因为所有三件事(“购买”,累积积分和获得收据)总是一起发生,所以从调用代码的角度来看,他们可以是一个单一的操作。从实现的角度来看,单个操作也更容易。吻: - )

答案 3 :(得分:0)

使用单一方法的优点是始终调用相同的序列。例如,您不能像您提供的流利界面示例中那样跳过accumulatePoints

如果调用这些方法的唯一方法与第一个代码块的顺序相同,请将其保留为单个函数。但是,如果在生成收据之前可以在TravelClub上完成任何操作子集,则无论如何都要使用流畅的界面。这是克服“组合爆炸”代码气味的最好方法之一(如果不是最好的方法)。

答案 4 :(得分:0)

只要您使用了正确的验证,Fluent界面就会更容易理解,例如它可能如下,

课程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(...);
   }
}

答案 5 :(得分:0)

在我看来,这里的部分困难在于选择一个包含方法所做的一切的良好描述性名称。问题自然是有时候你有很多复杂的逻辑,用一个简单的名字就不能轻易描述。

在您的代码示例中提出的情况中,我很想将方法本身的名称简化为更广义的内容:

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

那么我提到的这个逻辑问题呢?这本身就归结为你的方法是否非常固定。如果只能使用Buy-> Points-> Receipt序列完成交易,那么更简单的名称可以工作,但更具描述性的名称也是如此,流畅的界面可能是一个合理的选择。

如果客户没有奖励卡,或者不希望收据?那些可能在一次交易中购买多件商品的情况怎么样 - 当然假设购买方法可能代表购买商品,而不仅仅是其他地方计算的总数?一旦你开始在序列中引入问题/选择,设计变得不那么明显,并且命名更加困难。你当然不想使用像:

这样疯狂的长名
BuyAndAddPointsIfTheCustomerHasACardAndReturnAReceiptIfTheCustomerAsksForIt(...)

当然,它会告诉你它究竟做了什么,但它也强调了一个潜在的问题,即该方法可能导致太多事情,或者它可能隐藏了一种更复杂的代码嗅觉调用。同样,像“Transaction”这样的简单方法名称可能会过分简化需要更好理解的复杂问题。

流畅的界面在这里可以带来很大的好处,前提是它可以指导开发人员做出关于如何应用所调用的流畅方法的合理决策。如果调用序列很重要,则需要将返回类型限制为序列中的下一个选项。如果调用序列不太重要,那么您可以使用具有更通用接口的返回类型,该接口允许以任何顺序调用选择的方法。

至于是否使用流畅的界面,我认为不应仅仅将其作为分解难以命名的方法的手段。您正在制作一个设计选择,您将需要在产品的整个生命周期中使用,从维护的角度来看,我发现流畅的界面可以使设计更难以在您的设计中进行可视化和组织和维护。码。最终,你需要决定这是否是你可以忍受的东西,作为它给你带来的好处的权衡。对我来说,我通常首先询问用例组合是否固定且简单,或者它们是否相对无穷无尽。如果是后者,流畅的界面可能有助于使您的代码更清洁,更容易在多种场景中使用。我还会考虑代码是否属于更通用的层,例如API,例如,流畅的界面可以很好地工作,或者更专业的东西。