具有继承和泛型的流畅API

时间:2014-05-19 06:44:54

标签: java generics fluent-interface

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

为了能够在使用流畅的API时访问子类的方法,我使用泛型来参数化子类,并使所有流畅的方法(以"以"开头)返回泛型类型。请注意,我省略了流体方法的大部分主体;其中有很多配置。

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 to T

取消选中

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

此处有other questions来解决类似问题。他们指出一个解决方案,其中所有中间类都是抽象的,并声明像protected abstract self()这样的方法。最后,它还不安全。

5 个答案:

答案 0 :(得分:19)

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

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

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

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

E.g。

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 :(得分:13)

之前我做过类似的事。它可能变得丑陋。事实上,我尝试过的次数比我用过的次数多;通常它会被删除,我试图找到一个更好的设计。也就是说,为了帮助你在路上走得更远,试试这个:

让您的抽象类声明一个类似的方法:

protected abstract T self();

这可以替换你的return语句中的this。子类将需要返回与T的边界匹配的内容 - 但它不保证它们返回相同的对象。

答案 2 :(得分:8)

如果您更改这样的签名,您既不应该收到任何警告,也不需要任何演员表:

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

答案 3 :(得分: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:用于从Java转换文件的转换软件。

答案 4 :(得分:3)

这不是您原始问题的解决方案。这只是尝试捕捉您的实际意图,并勾画出原始问题未出现的方法。 (我喜欢泛型 - 但像CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>>这样的类名让我不寒而栗......)

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

如果我理解你的意图,你可以考虑让流利的电话处理这些子类型。

这里的想法是,您最初只能 创建一个简单的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,它是德语,但代码很好地展示了如何使用这个概念来构造类型安全的“从哪里选择”查询。

此处,草拟了该方法,大致适合您的用例。当然,有一些细节,实现将取决于实际使用方式 - 但我希望这个想法变得清晰。

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