【问题标题】:Generic fluent Builder in JavaJava中的通用流利生成器
【发布时间】:2016-08-15 03:49:23
【问题描述】:

我知道有类似的问题。不过,我还没有看到我的问题的答案。

我会用一些简化的代码来展示我想要的东西。假设我有一个复杂的对象,它的一些值是通用的:

public static class SomeObject<T, S> {
    public int number;
    public T singleGeneric;
    public List<S> listGeneric;

    public SomeObject(int number, T singleGeneric, List<S> listGeneric) {
        this.number = number;
        this.singleGeneric = singleGeneric;
        this.listGeneric = listGeneric;
    }
}

我想用流畅的 Builder 语法来构建它。不过,我想让它变得优雅。我希望它像那样工作:

SomeObject<String, Integer> works = new Builder() // not generic yet!
    .withNumber(4) 

    // and only here we get "lifted"; 
    // since now it's set on the Integer type for the list
    .withList(new ArrayList<Integer>()) 

    // and the decision to go with String type for the single value
    // is made here:
    .withTyped("something") 

    // we've gathered all the type info along the way
    .create();

没有不安全的强制转换警告,也不需要预先指定泛型类型(在顶部,构建 Builder 的地方)。

相反,我们让类型信息显式地流入到链的更下游 - 以及 withListwithTyped 调用。

现在,实现它的最优雅的方式是什么?

我知道最常见的技巧,例如使用recursive generics,但我玩弄了一段时间,不知道它如何应用于这个用例。

下面是一个普通的冗长解决方案,它在满足所有要求的意义上工作,但代价是非常冗长 - 它引入了四个构建器(在继承方面无关)​​,代表T 和 @987654327 的四种可能组合@ 类型是否被定义。

它确实有效,但这不是一个值得骄傲的版本,如果我们期望更多的通用参数而不仅仅是两个,那么它就无法维护。

public static class Builder  {
    private int number;

    public Builder withNumber(int number) {
        this.number = number;
        return this;
    }

    public <T> TypedBuilder<T> withTyped(T t) {
        return new TypedBuilder<T>()
                .withNumber(this.number)
                .withTyped(t);
    }

    public <S> TypedListBuilder<S> withList(List<S> list) {
        return new TypedListBuilder<S>()
                .withNumber(number)
                .withList(list);
    }
}

public static class TypedListBuilder<S> {
    private int number;
    private List<S> list;

    public TypedListBuilder<S> withList(List<S> list) {
        this.list = list;
        return this;
    }

    public <T> TypedBothBuilder<T, S> withTyped(T t) {
        return new TypedBothBuilder<T, S>()
                .withList(list)
                .withNumber(number)
                .withTyped(t);
    }

    public TypedListBuilder<S> withNumber(int number) {
        this.number = number;
        return this;
    }
}

public static class TypedBothBuilder<T, S> {
    private int number;
    private List<S> list;
    private T typed;

    public TypedBothBuilder<T, S> withList(List<S> list) {
        this.list = list;
        return this;
    }

    public TypedBothBuilder<T, S> withTyped(T t) {
        this.typed = t;
        return this;
    }

    public TypedBothBuilder<T, S> withNumber(int number) {
        this.number = number;
        return this;
    }

    public SomeObject<T, S> create() {
        return new SomeObject<>(number, typed, list);
    }
}

public static class TypedBuilder<T> {
    private int number;
    private T typed;

    private Builder builder = new Builder();

    public TypedBuilder<T> withNumber(int value) {
        this.number = value;
        return this;
    }

    public TypedBuilder<T> withTyped(T t) {
        typed = t;
        return this;
    }

    public <S> TypedBothBuilder<T, S> withList(List<S> list) {
        return new TypedBothBuilder<T, S>()
                .withNumber(number)
                .withTyped(typed)
                .withList(list);
    }
}

我可以应用更聪明的技术吗?

【问题讨论】:

  • "unmaintainable if we expect more generic parameters than just two" 如果你想保留任意顺序(在你的例子中,你可以同时做withTyped(...).withList(...) withList(...).withTyped(...)) 那么问题就变得非常困难,因为你最终会得到类似n! 类的东西,其中n 是类型参数的数量。如果您采用更传统的 step-builder 方法,那么它会更简单一些。
  • @Radiodef 我认为 2^n 个类:在任何时候,每个泛型类型都可以处于以下两种状态之一:已定义或尚未定义。但是,是的,这是“手动”实现的一个严重缺点。这就是为什么我想知道是否存在更好的解决方案;也许以某种巧妙的方式利用通用约束。

标签: java generics builder fluent


【解决方案1】:

好的,所以更传统的 step-builder 方法应该是这样的。

不幸的是,因为我们混合了泛型和非泛型方法,我们不得不重新声明很多方法。我不认为有一个很好的方法来解决这个问题。

基本思想就是:在接口上定义每一步,然后在私有类上全部实现。我们可以通过从它们的原始类型继承来使用泛型接口来做到这一点。这很丑陋,但它有效。

public interface NumberStep {
    NumberStep withNumber(int number);
}
public interface NeitherDoneStep extends NumberStep {
    @Override NeitherDoneStep withNumber(int number);
    <T> TypeDoneStep<T> withTyped(T type);
    <S> ListDoneStep<S> withList(List<S> list);
}
public interface TypeDoneStep<T> extends NumberStep {
    @Override TypeDoneStep<T> withNumber(int number);
    TypeDoneStep<T> withTyped(T type);
    <S> BothDoneStep<T, S> withList(List<S> list);
}
public interface ListDoneStep<S> extends NumberStep {
    @Override ListDoneStep<S> withNumber(int number);
    <T> BothDoneStep<T, S> withTyped(T type);
    ListDoneStep<S> withList(List<S> list);
}
public interface BothDoneStep<T, S> extends NumberStep {
    @Override BothDoneStep<T, S> withNumber(int number);
    BothDoneStep<T, S> withTyped(T type);
    BothDoneStep<T, S> withList(List<S> list);
    SomeObject<T, S> create();
}
@SuppressWarnings({"rawtypes","unchecked"})
private static final class BuilderImpl implements NeitherDoneStep, TypeDoneStep, ListDoneStep, BothDoneStep {
    private final int number;
    private final Object typed;
    private final List list;

    private BuilderImpl(int number, Object typed, List list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    @Override
    public BuilderImpl withNumber(int number) {
        return new BuilderImpl(number, this.typed, this.list);
    }

    @Override
    public BuilderImpl withTyped(Object typed) {
        // we could return 'this' at the risk of heap pollution
        return new BuilderImpl(this.number, typed, this.list);
    }

    @Override
    public BuilderImpl withList(List list) {
        // we could return 'this' at the risk of heap pollution
        return new BuilderImpl(this.number, this.typed, list);
    }

    @Override
    public SomeObject create() {
        return new SomeObject(number, typed, list);
    }
}

// static factory
public static NeitherDoneStep builder() {
    return new BuilderImpl(0, null, null);
}

由于我们不希望人们访问丑陋的实现,我们将其设为私有并让每个人都通过static 方法。

否则它的工作原理与您自己的想法几乎相同:

SomeObject<String, Integer> works =
    SomeObject.builder()
        .withNumber(4)
        .withList(new ArrayList<Integer>())
        .withTyped("something")
        .create();

// we could return 'this' at the risk of heap pollution

这是怎么回事?好的,所以这里普遍存在一个问题,它是这样的:

NeitherDoneStep step = SomeObject.builder();
BothDoneStep<String, Integer> both =
    step.withTyped("abc")
        .withList(Arrays.asList(123));
// setting 'typed' to an Integer when
// we already set it to a String
step.withTyped(123);
SomeObject<String, Integer> oops = both.create();

如果我们不创建副本,我们现在会将123 伪装成String

(如果您仅将构建器用作流畅的调用集,则不会发生这种情况。)

虽然我们不需要为withNumber 制作副本,但我只是采取了额外的步骤,使构建器不可变。我们创建的对象比我们必须的要多,但实际上并没有其他好的解决方案。如果每个人都以正确的方式使用构建器,那么我们可以使其可变并return this


由于我们对新颖的通用解决方案感兴趣,因此这里是单个类中的构建器实现。

这里的区别在于,如果我们第二次调用它们中的任何一个 setter,我们不会保留 typedlist 的类型。这本身并不是一个真正的缺点,我猜它只是不同。这意味着我们可以这样做:

SomeObject<Long, String> =
    SomeObject.builder()
        .withType( new Integer(1) )
        .withList( Arrays.asList("abc","def") )
        .withType( new Long(1L) ) // <-- changing T here
        .create();
public static class OneBuilder<T, S> {
    private final int number;
    private final T typed;
    private final List<S> list;

    private OneBuilder(int number, T typed, List<S> list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    public OneBuilder<T, S> withNumber(int number) {
        return new OneBuilder<T, S>(number, this.typed, this.list);
    }

    public <TR> OneBuilder<TR, S> withTyped(TR typed) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<TR, S>(this.number, typed, this.list);
    }

    public <SR> OneBuilder<T, SR> withList(List<SR> list) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<T, SR>(this.number, this.typed, list);
    }

    public SomeObject<T, S> create() {
        return new SomeObject<T, S>(number, typed, list);
    }
}

// As a side note,
// we could return e.g. <?, ?> here if we wanted to restrict
// the return type of create() in the case that somebody
// calls it immediately.
// The type arguments we specify here are just whatever
// we want create() to return before withTyped(...) and
// withList(...) are each called at least once.
public static OneBuilder<Object, Object> builder() {
    return new OneBuilder<Object, Object>(0, null, null);
}

创建副本和堆污染也是如此。


现在我们得到了真的小说。这里的想法是我们可以通过导致捕获转换错误来“禁用”每个方法。

解释起来有点复杂,但基本思路是:

  • 每个方法都依赖于在类中声明的类型变量。
  • “禁用”该方法,方法是将其返回类型设置为?
  • 如果我们尝试在该返回值上调用方法,这会导致捕获转换错误。

这个例子和上一个例子的不同之处在于,如果我们再次尝试调用一个setter,我们会得到一个编译器错误:

SomeObject<Long, String> =
    SomeObject.builder()
        .withType( new Integer(1) )
        .withList( Arrays.asList("abc","def") )
        .withType( new Long(1L) ) // <-- compiler error here
        .create();

因此,我们只能调用每个 setter 一次。

这里的两个主要缺点是你:

  • 由于合法原因,不能第二次调用设置器
  • 并且可以使用null 文字第二次调用setter。

我认为这是一个非常有趣的概念验证,即使它有点不切实际。

public static class OneBuilder<T, S, TCAP, SCAP> {
    private final int number;
    private final T typed;
    private final List<S> list;

    private OneBuilder(int number, T typed, List<S> list) {
        this.number = number;
        this.typed  = typed;
        this.list   = list;
    }

    public OneBuilder<T, S, TCAP, SCAP> withNumber(int number) {
        return new OneBuilder<T, S, TCAP, SCAP>(number, this.typed, this.list);
    }

    public <TR extends TCAP> OneBuilder<TR, S, ?, SCAP> withTyped(TR typed) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<TR, S, TCAP, SCAP>(this.number, typed, this.list);
    }

    public <SR extends SCAP> OneBuilder<T, SR, TCAP, ?> withList(List<SR> list) {
        // we could return 'this' at the risk of heap pollution
        return new OneBuilder<T, SR, TCAP, SCAP>(this.number, this.typed, list);
    }

    public SomeObject<T, S> create() {
        return new SomeObject<T, S>(number, typed, list);
    }
}

// Same thing as the previous example,
// we could return <?, ?, Object, Object> if we wanted
// to restrict the return type of create() in the case
// that someone called it immediately.
// (The type arguments to TCAP and SCAP should stay
// Object because they are the initial bound of TR and SR.)
public static OneBuilder<Object, Object, Object, Object> builder() {
    return new OneBuilder<Object, Object, Object, Object>(0, null, null);
}

同样,创建副本和堆污染也是如此。


不管怎样,我希望这能给你一些想法,让你深信不疑。 :)

如果你一般对这类东西感兴趣,我建议学习code generation with annotation processing,因为你可以生成这样的东西比手工编写要容易得多。正如我们在 cmets 中谈到的那样,用手写这样的东西很快就会变得不切实际。

【讨论】:

  • 这是一个比我预期的更全面的答案!很有见地。非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多