【问题标题】:Fluent API with inheritance and generics具有继承和泛型的流畅 API
【发布时间】:2014-05-19 06:44:54
【问题描述】:

我正在编写一个流畅的 API 来配置和实例化一系列“消息”对象。我有一个消息类型的层次结构。

为了在使用 fluent API 时能够访问子类的方法,我使用泛型对子类进行参数化,并使所有 fluent 方法(以“with”开头)返回泛型类型。请注意,我省略了 fluent 方法的大部分主体;他们进行了很多配置。

public abstract class Message<T extends Message<T>> {

    protected Message() {

    }

    public T withID(String id) {
        return (T) this;
    }
}

具体的子类类似地重新定义了泛型。

public class CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public T withCommand(String command) {
        return (T) this;
    }
}

public class CommandWithParamsMessage extends
    CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }
}

此代码有效,即我可以实例化任何类并使用所有流畅的方法:

CommandWithParamsMessage msg = CommandWithParamsMessage.newMessage()
        .withID("do")
        .withCommand("doAction")
        .withParameter("arg", "value");

以任何顺序调用流利的方法是这里的主要目标。

但是,编译器警告所有return (T) this 都是不安全的。

类型安全:从 Message 到 T 的未经检查的强制转换

我不确定如何重新组织层次结构以使这段代码真正安全。尽管它有效,但以这种方式使用泛型确实令人费解。 特别是,如果我忽略警告,我无法预见会发生运行时异常的情况。 会有新的消息类型,所以我需要保持代码的可扩展性。 如果解决方案是完全避免继承,我还想获得替代建议。

在 SO 上有 other questions 解决了类似的问题。他们指出了一个解决方案,其中所有中间类都是抽象的,并声明了一个类似protected abstract self() 的方法。不过,到头来还是不安全的。

【问题讨论】:

标签: java generics fluent-interface


【解决方案1】:

您的代码基本上是对泛型的不安全使用。例如,如果我编写了一个扩展消息的新类,比如说 Threat,并有一个新方法 doSomething(),然后我创建了一个由这个新类参数化的消息,它创建了一个 Message 的实例,然后尝试对其进行转换到它的子类。但是,由于它是 Message 的实例,而不是 Threat 的实例,因此尝试调用此消息将导致异常。因为 Message 不可能 doSOthing()。

此外,这里也没有必要使用泛型。普通的旧继承可以正常工作。由于子类型可以通过更具体的返回类型来覆盖方法,因此您可以:

public abstract class Message {

    protected Message() {

    }

    public Message withID(String id) {
        return this;
    }
}

然后

public class CommandMessage extends Message {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public CommandMessage withCommand(String command) {
        return this;
    }
}

这可以正常工作,前提是您以正确的顺序调用参数:

CommandWithParamsMessage.newMessage()
    .withID("do")
    .withCommand("doAction")
    .withParameter("arg", "value");

会失败,但是

CommandWithParamsMessage.newMessage().withParameter("arg", "value")
.withCommand("doAction").withID("do")

会成功,因为它只是“向上类型”,最后返回一个“消息”类。如果你不希望它“uptype”,那么只需覆盖继承的命令,现在你可以按任何顺序调用方法,因为它们都返回原始类型。

例如

public class CommandWithParamsMessage extends
CommandMessage {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }

    @Override
    public CommandWithParamsMessage withCommand(String command){
        super.withCommand(command);
        return this;
   }

    @Override
    public CommandWithParamsMessage withID(String s){
        super.withID(s);
        return this;
    }
}

现在您将使用上述两个流畅调用中的任何一个流畅地返回 CommandWithParamsMessage。

这是否解决了您的问题,还是我误解了您的意图?

【讨论】:

  • +1 表示“子类型可以通过使其返回类型更具体来覆盖方法”
  • 我之前尝试过这个解决方案,但我的意图是能够以任何顺序调用方法,因此为什么 withID() of Message 返回了 T。
  • 你可以,如果你覆盖超类型的方法来覆盖类型,就像上一个例子一样。
【解决方案2】:

我以前做过类似的事情。它会变得丑陋。事实上,我尝试过的次数比使用过的次数还要多。通常它会被删除,我会尝试找到更好的设计。也就是说,为了帮助您走得更远,试试这个:

让您的抽象类声明如下方法:

protected abstract T self();

这可以替换您返回语句中的this。子类将被要求返回与T 的界限相匹配的东西——但不能保证它们返回相同的对象。

【讨论】:

  • 这部分有效,因为它消除了抽象类 Message 和最底层的具体类中的警告。请注意,CommandMessage 不是抽象的。
【解决方案3】:

如果您像这样更改签名,您既不会收到任何警告,也不需要任何强制转换:

abstract class Message<T extends Message<T>> {

    public T withID(String id) {
        return self();
    }

    protected abstract T self();
}

abstract class CommandMessage<T extends CommandMessage<T>> extends Message<T> {

    public T withCommand(String command) {
        // do some work ...
        return self();
    }
}

class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName, String paramValue) {
        // do some work ...
        return this;
    }

    @Override protected CommandWithParamsMessage self() {
        return this;
    }
}

【讨论】:

    【解决方案4】:

    编译器会警告您这种不安全的操作,因为它实际上无法检查您的代码的正确性。事实上,这使得它不安全,您无法采取任何措施来阻止此警告。即使不安全的操作没有经过编译检查,它在运行时仍然是合法的。如果您绕过编译器检查,那么您的工作就是验证您自己的代码是否使用了正确的类型,这就是 @SupressWarning("unchecked") 注释的用途。

    将此应用于您的示例:

    public abstract class Message<T extends Message<T>> {
    
      // ...
    
      @SupressWarning("unchecked")
      public T withID(String id) {
        return (T) this;
      }
    }
    

    很好,因为事实上您可以确定地告诉这个Message 实例始终是T 所表示的类型。但是 Java 编译器(还)不能。与其他抑制警告一样,使用注释的关键是最小化其范围!否则,在您进行代码更改后,您很容易意外保留注释抑制,从而使您以前的手动类型安全检查无效。

    由于您只返回一个 this 实例,因此您可以轻松地将任务外包给另一个答案中推荐的特定方法。定义一个protected 方法,比如

    @SupressWarning("unchecked")
    public T self() {
      (T) this;
    }
    

    而且你总是可以像这里这样调用 mutator:

    public T withID(String id) {
      return self();
    }
    

    作为另一种选择,如果您可以实现,请考虑一个不可变的构建器,它仅通过接口公开其 API,但实现了一个完整的构建器。这就是我现在通常构建流畅界面的方式:

    interface Two<T> { T make() }
    interface One { <S> Two<S> do(S value) }
    
    class MyBuilder<T> implements One, Two<T> {
    
      public static One newInstance() {
        return new MyBuilder<Object>(null);
      }
    
      private T value; // private constructors omitted
    
      public <S> Two<S> do(S value) {
        return new MyBuilder<S>(value);
      }
    
      public T make() {
        return value;
      }
    }
    

    当然,您可以创建更智能的结构来避免未使用的字段。如果您想查看我使用这种方法的示例,请查看我的两个大量使用流式接口的项目:

    1. Byte Buddy:用于在运行时定义 Java 类的 API。
    2. PDF converter: 一个使用 MS Word 从 Java 转换文件的转换软件。

    【讨论】:

      【解决方案5】:

      这不是您原来问题的解决方案。这只是试图捕捉你的实际意图,并勾勒出一个没有出现原始问题的方法。 (我喜欢泛型——但是像CommandMessage&lt;T extends CommandMessage&lt;T&gt;&gt; extends Message&lt;CommandMessage&lt;T&gt;&gt; 这样的类名让我不寒而栗……)

      我知道这在结构上与您最初询问的内容有很大不同,您可能在问题中省略了一些细节,这些细节缩小了可能答案的范围,因此以下内容不再是适用。

      但是如果我理解你的意图是正确的,你可以考虑让子类型由流利的调用来处理。

      这里的想法是,您最初可以创建一个简单的Message

      Message m0 = Message.newMessage();
      Message m1 = m0.withID("id");
      

      在此消息上,您可以调用 withID 方法 - 这是所有消息共有的唯一方法。在这种情况下,withID 方法返回 Message

      到目前为止,消息既不是CommandMessage,也不是任何其他特殊形式。但是,当您调用withCommand 方法时,您显然想要构造一个CommandMessage - 所以您现在只需返回一个CommandMessage

      CommandMessage m2 = m1.withCommand("command");
      

      同样,当您调用withParameter 方法时,您会收到CommandWithParamsMessage

      CommandWithParamsMessage m3 = m2.withParameter("name", "value");
      

      这个想法大致(!)灵感来自blog entry,这是德语,但代码很好地展示了如何使用这个概念来构建类型安全的“Select-From-Where”查询。

      在这里,该方法被勾勒出来,大致适用于您的用例。当然,有一些细节的实现将取决于它的实际使用方式 - 但我希望这个想法变得清晰。

      import java.util.HashMap;
      import java.util.Map;
      
      
      public class FluentTest
      {
          public static void main(String[] args) 
          {
              CommandWithParamsMessage msg = Message.newMessage().
                      withID("do").
                      withCommand("doAction").
                      withParameter("arg", "value");
      
      
              Message m0 = Message.newMessage();
              Message m1 = m0.withID("id");
              CommandMessage m2 = m1.withCommand("command");
              CommandWithParamsMessage m3 = m2.withParameter("name", "value");
              CommandWithParamsMessage m4 = m3.withCommand("otherCommand");
              CommandWithParamsMessage m5 = m4.withID("otherID");
          }
      }
      
      class Message 
      {
          protected String id;
          protected Map<String, String> contents;
      
          static Message newMessage()
          {
              return new Message();
          }
      
          private Message() 
          {
              contents = new HashMap<>();
          }
      
          protected Message(Map<String, String> contents) 
          {
              this.contents = contents;
          }
      
          public Message withID(String id) 
          {
              this.id = id;
              return this;
          }
      
          public CommandMessage withCommand(String command) 
          {
              Map<String, String> newContents = new HashMap<String, String>(contents);
              newContents.put("command", command);
              return new CommandMessage(newContents);
          }
      
      }
      
      class CommandMessage extends Message 
      {
          protected CommandMessage(Map<String, String> contents) 
          {
              super(contents);
          }
      
          @Override
          public CommandMessage withID(String id) 
          {
              this.id = id;
              return this;
          }
      
          public CommandWithParamsMessage withParameter(String paramName, String paramValue) 
          {
              Map<String, String> newContents = new HashMap<String, String>(contents);
              newContents.put(paramName, paramValue);
              return new CommandWithParamsMessage(newContents);
          }
      
      }
      
      class CommandWithParamsMessage extends CommandMessage 
      {
          protected CommandWithParamsMessage(Map<String, String> contents) 
          {
              super(contents);
          }
      
          @Override
          public CommandWithParamsMessage withID(String id) 
          {
              this.id = id;
              return this;
          }
      
          @Override
          public CommandWithParamsMessage withCommand(String command) 
          {
              this.contents.put("command", command);
              return this;
          }
      }
      

      【讨论】:

      • 谢谢。我想我需要按照这些思路去做,即使它不会产生真正流畅的 API。我正在考虑保持不安全的操作,但限制 API 的可扩展性以避免误用。
      • @WillV 不要放弃。这只是对您的意图的猜测,可能有一个“不错”的解决方案,它更类似于您的原始代码的结构。 (对于“nice”,我的意思是:类型安全,并且可能没有像 CommandMessage&lt;T extends CommandMessage&lt;T&gt;&gt; extends Message&lt;CommandMessage&lt;T&gt;&gt; 这样的结构)。应该会有更多的答案。 WilliamPrice 的答案已经是一步,有时被称为“getThis”技巧。也看看angelikalanger.com/GenericsFAQ/FAQSections/… - 这个网站上的一些有价值的信息,也许有帮助
      • 我也看过thesequestions。仅当所有中间类都是抽象类并声明像self() 这样的方法时,它才有效。最后是不安全的。我仍然需要消化这些,看看如何更改我的 API。
      • 我也刚刚经历了同样的旅程。 self() 方法有效,但如果没有大量样板代码,就很难继承。我什至尝试将 TConcrete 泛型参数传递给抽象类,以便方法可以返回具体类型。但是同样有太多的代码复制和粘贴,你最好避免继承,在创建一个新的流利类时复制粘贴一个骨架。
      猜你喜欢
      • 2011-12-19
      • 1970-01-01
      • 1970-01-01
      • 2020-05-17
      • 2014-09-07
      • 2021-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多