Java中的通用流利构建器 [英] Generic fluent Builder in Java

查看:178
本文介绍了Java中的通用流利构建器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我知道有类似的问题。虽然我还没有看到我的问题的答案。



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

  public static class SomeObject< T,S> {
public int number;
public T singleGeneric;
public List< S> listGeneric;
$ b $ 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()//不是通用的! 
.withNumber(4)

//只有在这里我们才能解除;
//从现在开始,它被设置在列表的整数类型上
.withList(new ArrayList< Integer>)

//决定使用String类型对于单个值
//是在这里创建的:
.withTyped(something)

//我们已经收集了沿途的所有类型信息
。创建();

没有不安全的转换警告,并且不需要预先指定通用类型(在顶部,Builder是建)。



相反,我们让类型info显式地流入链中 - 连同 withList withTyped 调用。



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

我知道最常用的技巧,比如使用递归泛型,但我玩了一段时间,但无法弄清楚它是如何应用于这个用例的。



下面是一个普通的详细解决方案,它满足所有需求,但是代价很大 - 它引入了四个构建器(在继承方面无关)​​,代表四种可能的组合是否定义了 T S 类型。



它确实有效,但这并不是一个值得骄傲的版本,如果我们期望更多的泛型参数而不仅仅是两个,那么它是不可维护的。 b $ b

  public static class Builder {
private int number;

public Builder withNumber(int number){
this.number = number;
返回此;
}

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

public< S> TypedListBuilder< S> withList(List< S> list){
返回新的TypedListBuilder< S>()
.withNumber(number)
.withList(list);
}
}

public static class TypedListBuilder< S> {
private int number;
私人列表< S>列表;

public TypedListBuilder< S> withList(列表< S>列表){
this.list = list;
返回此;
}

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;
返回此;
}
}

public static class TypedBothBuilder< T,S> {
private int number;
私人列表< S>列表;
private T typed;

public TypedBothBuilder< T,S> withList(列表< S>列表){
this.list = list;
返回此;
}

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

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

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;
返回此;
}

public TypedBuilder< T> withTyped(T t){
typed = t;
返回此;
}

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

有更聪明的技巧可以应用吗?

解决方案

好的,所以更传统的步骤生成器方法就是这样。



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



基本思想是:定义界面上的每一步,然后执行它们全部在私人课上。我们可以通过继承原始类型来实现通用接口。这是丑陋的,但它的工作原理。

  public interface NumberStep {
NumberStep withNumber(int number);
}
public interface NeitherDoneStep extends NumberStep {
@Override NeitherDoneStep withNumber(int number);
< T> TypeDoneStep< T> withTyped(T型);
< S> ListDoneStep< S> withList(列表< S>列表);
}
public interface TypeDoneStep< T>扩展NumberStep {
@Override TypeDoneStep< T> withNumber(int number);
TypeDoneStep< T> withTyped(T型);
< S> BothDoneStep< T,S> withList(列表< S>列表);
}
public interface ListDoneStep< S>扩展NumberStep {
@Override ListDoneStep< S> withNumber(int number);
< T> BothDoneStep< T,S> withTyped(T型);
ListDoneStep< S> withList(列表< S>列表);
}
public interface BothDoneStep< T,S>扩展NumberStep {
@Override BothDoneStep< T,S> withNumber(int number);
BothDoneStep< T,S> withTyped(T型);
BothDoneStep< T,S> withList(列表< S>列表);
SomeObject< T,S>创建();

@SuppressWarnings({rawtypes,unchecked})
private static final class BuilderImpl实现了NeitherDoneStep,TypeDoneStep,ListDoneStep,BothDoneStep {
private final int number;
private final对象类型;
私人决赛名单;

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

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

@Override
public BuilderImpl withTyped(Object typed){
//我们可以返回'this'以承受堆污染的风险
return新的BuilderImpl(this.number,typed,this.list);
}

@Override
public BuilderImpl withList(List list){
//我们可以返回'this'以承受堆污染风险
return新的BuilderImpl(this.number,this.typed,列表);
}

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



//静态工厂
public static NeitherDoneStep builder(){
return new BuilderImpl(0,null,null);





$ b因为我们不想让人们访问丑陋的实现,并让每个人都通过一个静态方法。



否则它和你自己的想法几乎一样:

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




//我们可以返回'this'存在堆积污染风险


这是怎么回事?好吧,所以这里一般都有问题,就像这样:

  NeitherDoneStep step = SomeObject.builder(); 
BothDoneStep< String,Integer> both =
step.withTyped(abc)
.withList(Arrays.asList(123));
//将'typed'设置为一个整数
//我们已经将它设置为一个字符串
step.withTyped(123);
SomeObject< String,Integer> oops = both.create();

如果我们没有创建副本,现在我们会有 123 伪装成一个字符串



(如果您只使用构建器作为)



尽管我们不需要为 withNumber ,我只是走了一步,使建造者不可变。我们正在创造比我们更多的对象,但没有真正的另一个好的解决方案。如果每个人都要以正确的方式使用构建器,那么我们可以使它变为可变的,并且返回这个






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



如果我们再次调用他们的setter,我们不保留 typed list 类型。这本身并不是一个缺点,我猜想它只是不同而已。这意味着我们可以这样做:

  SomeObject< Long,String> = 
SomeObject.builder()
.withType(new Integer(1))
.withList(Arrays.asList(abc,def))
.withType new Long(1L))//< - 在这里改变T
.create();





  public static class OneBuilder< ; T,S> {
private final int number;
private final T typed;
private final List< S>列表;

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){
返回新的OneBuilder< T,S>(number,this.typed,this.list);
}

public< TR> OneBuilder< TR,S> withTyped(TR typed){
//我们可以返回'this'以承受堆污染的风险
返回新的OneBuilder< TR,S>(this.number,typed,this.list);
}

public< SR> OneBuilder< T,SR> withList(List< SR> list){
//我们可以以堆污染的风险返回'this'
返回新的OneBuilder< T,SR>(this.number,this.typed,list) ;
}

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

//请注意,
//我们可以返回例如<?,?>这里如果我们想限制
//在某人
//立即调用它的情况下,create()的返回类型。
//我们在这里指定的类型参数只是
//我们希望create()在withTyped(...)和
// withList(...)之前返回都是至少叫一次。
public static OneBuilder< Object,Object> builder(){
返回新的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))//< - 编译器错误在这里
    .create();

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



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


    • 不能再次为合法调用setters。 em>

    • can 可以使用 null 字面值再次调用setter。



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

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

    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){
    返回新的OneBuilder< T,S,TCAP,SCAP>(number,this.typed,this.list);
    }

    public< TR扩展TCAP> OneBuilder< TR,S,?,SCAP> withTyped(TR typed){
    //我们可以返回'this'以承受堆污染风险
    返回新的OneBuilder< TR,S,TCAP,SCAP>(this.number,typed,this.list );
    }

    public< SR扩展SCAP> OneBuilder< T,SR,TCAP,> withList(List< SR> list){
    //我们可以以堆污染的风险返回'this'
    返回新的OneBuilder< T,SR,TCAP,SCAP>(this.number,this。键入,列表);
    }

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

    //和前面的例子一样,
    //我们可以返回<?,?,Object,Object>如果我们希望
    //在限制
    // //某人立即调用它的情况下限制create()的返回类型。
    //(TCAP和SCAP的类型参数应保持为
    // Object,因为它们是TR和SR的初始边界。)
    public static OneBuilder< Object,Object,Object,Object> ; builder(){
    返回新的OneBuilder< Object,Object,Object,Object>(0,null,null);
    }

    同样,关于创建副本和堆污染也是一样。






    无论如何,我希望这给你一些想法。 :)



    如果您一般对这类事情感兴趣,我建议您学习带有注释处理的代码生成,因为您可以比手动编写这样的事情更容易。正如我们在评论中谈到的那样,用手写这样的东西很快就变得不切实际。


    I'm aware there've been similar questions. I haven't seen an answer to my question though.

    I'll present what I want with some simplified code. Say I have a complex object, some of its values are generic:

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

    I'd like to construct it with fluent Builder syntax. I'd like to make it elegant though. I wish it worked like that:

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

    No unsafe cast warnings, and no need to specify generic types upfront (at the top, where Builder is constructed).

    Instead, we let the type info to flow in explicitly, further down the chain - along with withList and withTyped calls.

    Now, what would be the most elegant way to achieve it?

    I'm aware of the most common tricks, such as the use of recursive generics, but I toyed with it for a while and couldn't figure out how it applies to this use case.

    Below is a mundane verbose solution which works in the sense of satisfying all requirements, but at the cost of great verbosity - it introduces four builders (unrelated in terms of inheritance), representing four possible combinations of T and S types being defined or not.

    It does work, but that's hardly a version to be proud of, and unmaintainable if we expected more generic parameters than just two.

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

    Is there a more clever technique I could apply?

    解决方案

    Okay, so the more traditional step-builder approach would be something like this.

    Unfortunately, because we're mixing generic and non-generic methods, we have to redeclare a lot of methods. I don't think there's a nice way around this.

    The basic idea is just: define each step on an interface, then implement them all on the private class. We can do that with generic interfaces by inheriting from their raw types. It's ugly, but it works.

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

    Since we don't want people accessing the ugly implementation, we make it private and make everyone go through a static method.

    Otherwise it works pretty much the same as your own idea:

    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

    What is this about? Okay, so there's a problem in general here, and it's like this:

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

    If we didn't create copies, we'd now have 123 masquerading around as a String.

    (If you're only using the builder as the fluent set of calls, this can't happen.)

    Although we don't need to make a copy for withNumber, I just went the extra step and made the builder immutable. We're creating more objects than we have to but there isn't really another good solution. If everyone is going to use the builder in the correct manner, then we could make it mutable and return this.


    Since we're interested in novel generic solutions, here is a builder implementation in a single class.

    The difference here is that we don't retain the types of typed and list if we invoke either of their setters a second time. This isn't really a drawback per se, it's just different I guess. It means that we can do this:

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

    Same thing about creating copies and heap pollution.


    Now we're getting really novel. The idea here is that we can "disable" each method by causing a capture conversion error.

    It's a little complicated to explain, but the basic idea is:

    • Each method somehow depends on a type variable which is declared on the class.
    • "Disable" that method by having its return type set that type variable to ?.
    • This causes a capture conversion error if we attempt to invoke the method on that return value.

    The difference between this example and the previous example is that if we try to call a setter a second time, we will get a compiler error:

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

    Thus, we can only call each setter once.

    The two major downsides here are that you:

    • can't call setters a second time for legitimate reasons
    • and can call setters a second time with the null literal.

    I think it's a pretty interesting proof-of-concept, even if it's a little impractical.

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

    Again, same thing about creating copies and heap pollution.


    Anyway, I hope this gives you some ideas to sink your teeth in to. : )

    If you're generally interested in this sort of thing, I recommend learning code generation with annotation processing, because you can generate things like this much easier than writing them by hand. As we talked about in the comments, writing things like this by hand becomes unrealistic pretty quickly.

    这篇关于Java中的通用流利构建器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆