【问题标题】:refactoring long methods with fluent interfaces用流畅的接口重构长方法
【发布时间】:2011-09-14 07:11:07
【问题描述】:

我想知道您对使用流式接口模式重构长方法的看法。

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

从我的角度来看,这是分解长方法和 还要分解它的名字。

你怎么看?

【问题讨论】:

  • 只是好奇...这不正是 Facade 模式的反面,在这种模式中,您试图通过提供简单易用的方法或接口来隐藏拥有多个方法或接口的复杂性。
  • @Sandeep,感谢您的意见。是的,但 IMO 这种模式的缺点是方法变得非常大(加上它的名称,加上参数的数量)

标签: java refactoring fluent fluent-interface


【解决方案1】:

问题在于,您必须记住既要累积积分又要执行购买(并生成收据,这不是问题,因为我认为操作没有副作用)。在我看来,积分累积应该在购买时自动进行。您在购买时收到收据也是很自然的,所以在某种程度上,您最初的方法很好,只是它读起来不太好。

如果您想要一个流畅的界面,我会引入一个额外的类,它可以温和地引导客户端代码做正确的事情(假设所有购买都使用卡片进行并且以相同的方式累积积分):

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

【讨论】:

    【解决方案2】:

    然后,我的反问是,如果有人改为调用,预期的行为是什么:

    myTravelClub.accumulatePoints(10000000L);
    

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

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

    【讨论】:

      【解决方案3】:

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

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

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

      【讨论】:

      • 良好的风格(清洁代码书)说方法的名称应该说明程序员该方法的作用。所以 IMO 长方法涉及长名称。也许这是 stackoverflow 中的一个新问题。
      • 感谢您的意见。但是只有一个操作会导致不止一个抽象级别。这就是长方法被分解成更小的方法的原因。
      • 是的,它应该说明该方法的作用,但详细程度如何|?收据是“生成”重要还是我买东西时得到的东西?我从方法返回类型中得到的不是很明显吗?
      • 实现已经分解。我说的是类的公共接口。
      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        只要你使用了正确的验证,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(...);
           }
        }
        

        【讨论】:

          【解决方案6】:

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

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

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

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

          如果客户没有奖励卡或不希望有收据,该怎么办?那些可能在一次交易中购买多个物品的情况会怎样——当然假设购买方法可能代表一个购买物品,而不仅仅是在其他地方计算的总数?一旦你开始在序列中引入问题/选择,设计就会变得不那么明显,命名也变得更加困难。您当然不想使用疯狂的长名称,例如:

          BuyAndAddPointsIfTheCustomerHasACardAndReturnAReceiptIfTheCustomerAsksForIt(...)
          

          当然,它会准确地告诉你它做了什么,但它也突出了一个潜在的问题,即该方法可能对太多事情负责,或者它可能在它所使用的方法之一背后隐藏了更复杂的代码气味来电。同样,像 "Transaction" 这样的简单方法名称可能会过度简化需要更好理解的复杂问题。

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

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

          【讨论】:

            猜你喜欢
            • 2019-05-10
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-12-05
            • 1970-01-01
            • 2011-11-20
            相关资源
            最近更新 更多