使用流畅接口重构长方法

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
只是好奇...这不是Facade模式的反向吗?您试图通过提供易于使用的方法或接口来隐藏具有多个方法或接口的复杂性。 - Sandeep G B
@Sandeep,感谢你的观点。没错,但在我看来,这种模式的缺点是方法变得非常庞大(加上它的名称和参数数量)。 - ejaenv
6个回答

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

3
我的反问是,如果有人调用以下内容,预期的行为是什么:
myTravelClub.accumulatePoints(10000000L);

不通过调用buy方法就能生成收据?或者在购买之前生成收据?我认为流畅的接口仍需要遵循其他OO约定。如果你真的想要一个流畅的接口,那么buy()方法将不得不返回另一个对象,而不是TravelClub本身,但是一个“购买对象”,该对象具有accumulatePoints()和generateReceipt()方法。
也许我对你的示例语义的理解过多,但是有一个原因,即维基百科的示例具有可以以任何顺序调用的逻辑方法。我认为Hibernate Criteria API是另一个很好的例子。

2

一个长方法和一个方法名很长是不同的。在你的情况下,我唯一会改变的是方法名:

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

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


好的风格(《Clean Code》书中)建议方法的名称应该告诉程序员方法的作用。所以在我看来,长方法应该有长的名称。也许这是一个新问题在stackoverflow上。 - ejaenv
谢谢您的观点。但是,单个操作会导致多个抽象级别。这就是为什么长方法被分解成更小的方法。 - ejaenv
1
是的,方法应该说明它的作用,但需要说明多详细呢?收据是否“生成”很重要,还是只是我购买物品时得到的东西?从方法返回类型中,我得到收据不是显而易见的吗? - meriton
实现已经被分解了。我说的是类的公共接口。 - meriton

0
只要您使用了适当的验证,流畅的接口就更容易理解,例如可以像下面这样实现:

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

0

我认为这里的难点之一是选择一个能够概括方法所有功能的好的描述性名称。问题在于有时候你有很多复杂的逻辑,很难用简单的名字来描述。

在你的代码示例中,我会考虑将方法本身的名称简化为更加通用的名称:

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

那么我提到的这个逻辑问题怎么样呢?它本身归结为你的方法是否非常固定。如果只能使用购买->积分->收据序列完成交易,那么一个更简单的名称就可以工作,但更具描述性的名称也可以,流畅的接口可能是一个合理的选择。

那么对于那些没有奖励卡或不想要收据的客户呢?对于那些在单个交易中可能购买多件商品的情况呢?假设购买方法可能代表购买商品而不仅仅是在其他地方计算的总额,一旦你开始在序列中引入问题/选择,设计就变得不太明显,命名也变得更加困难。你肯定不想使用像这样疯狂长的名称:

BuyAndAddPointsIfTheCustomerHasACardAndReturnAReceiptIfTheCustomerAsksForIt(...)

没错,它确切地告诉你它的作用,但它也突显出一个潜在的问题,那就是这个方法可能要承担太多的责任,或者说它可能会把更复杂的代码味道藏在它调用的方法中之一背后。同样,像“Transaction”这样简单的方法名可能会过度简化需要更好理解的复杂问题。

流畅的接口可以在这里发挥巨大的益处,因为它引导开发人员对如何应用被调用的流畅方法做出明智的决策。如果调用序列很重要,你需要将返回类型限制为序列中的下一个选择。如果调用序列不那么重要,那么你可以使用一个返回类型,它具有更广义的接口,允许以任意顺序调用多个方法。

关于是否使用流畅接口,我认为这不应该仅仅作为分解难以命名的方法的手段来决定。您正在做一个设计选择,需要在产品的整个生命周期内使用,并且从维护的角度来看,我发现流畅接口可能会使设计更难以可视化、组织和维护。最终,您需要决定是否可以将其作为权衡所带来的好处而接受。对我来说,我通常会先问一下使用情况的组合是固定且简单的,还是相对无限的。如果是后者,流畅接口可能有助于保持代码更清洁,更易于在多种情况下使用。我还会考虑代码是否属于更广泛的层,例如 API,其中流畅接口可能很好地工作,或者是更专业化的东西。

0

使用单一方法的优点是总是调用相同的序列。例如,您不能像您提供的流畅接口示例中那样跳过accumulatePoints

如果调用这些方法的唯一方式是与您的第一个代码块中的顺序相同,请将其保留为单个函数。但是,如果可以在生成收据之前对TravelClub进行任何子集操作,则务必使用流畅接口。这是克服“组合爆炸”代码异味的最佳方法之一(如果不是最佳方法)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接