关于java:在单独的类中保持构建器(流畅的界面)

Keeping builder in separate class (fluent interface)

1
2
3
4
5
Foo foo = Foo.builder()
    .setColor(red)
    .setName("Fred")
    .setSize(42)
    .build();

所以我知道在调用方法时有以下"Builder"解决方案用于创建命名参数。 虽然,这似乎只适用于内部静态类作为构建器,或者我错了吗? 我看了一些构建器模式的教程,但是对于我想要做的事情,它们看起来非常复杂。 是否有任何方法可以保持Foo类和Builder类分离,同时享受上述代码等命名参数的好处?

以下是典型设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Foo {
    public static class Builder {
        public Foo build() {
            return new Foo(this);
        }

        public Builder setSize(int size) {
            this.size = size;
            return this;
        }

        public Builder setColor(Color color) {
            this.color = color;
            return this;
        }

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        // you can set defaults for these here
        private int size;
        private Color color;
        private String name;
    }

    public static Builder builder() {
        return new Builder();
    }

    private Foo(Builder builder) {
        size = builder.size;
        color = builder.color;
        name = builder.name;
    }

    private final int size;
    private final Color color;
    private final String name;
}


您可以确定将Builder类的字段更改为私有 - 然后您只需要为构建器上的每个"属性"使用(公共)getter方法;并且Foo中的构造函数调用这些方法;而不是仅仅获取Builder对象中的字段。

然后你可以将你的Builder类移出Foo。简单明了。

但请记住:最后,Builder和Foo密切相关。它们按设计共享一组共同的字段。所以对Foo的任何改变都会影响Builder;反之亦然。因此,让它们"紧密结合"是很有意义的。也许不是内部/外部类,但可能仍然在同一个源文件中!但是......其中只有一个可以公开。这真的是你想要的吗?!

换句话说:不要只是"因为你可以"撕裂事物。只有在你有充分理由这样做的情况下才能做到这一点,并且如果出现的事情比你当前的解决方案更好!

编辑:您的问题可能不是Foo和Builder的分离,而是您的Foo类首先包含太多字段。不要忘记单一的责任原则......当你的班级需要超过5,6个领域......它可能做得太多而且应该进一步切片!请记住:良好的OO设计首先是关于行为;不是在某个对象中有10,20个字段!


很难严格定义"构建器模式?",并且设计选择有几个自由度。有些概念很容易被混合或滥用,除此之外,说"你总是必须这样做"通常很难(而且几乎总是错误的)。

问题是应用"模式"应该实现什么。在您的问题和示例中,您已经混合了两个概念,即构建器模式和流畅的接口。扮演魔鬼的拥护者,人们甚至可以偷偷地争辩说,你的案例中的"建造者"只是托马斯已经提到过的参数对象,它是以一种特殊的方式(流利地)构建的,并且富含了public可见度。

构建器模式的一些可能目标是重叠或齐头并进。但你应该问问自己,你的主要目标是什么:

  • 结果对象应该是不可变的吗?

    • 它应该是真正不可变的,只有final最后的字段,还是可能还有不应该公开的setter? (建造者仍然可以称这些非公开的制定者!)
  • 目标是限制一般的可见性吗?
  • 应该有多态实例化吗?
  • 总结大量构造函数参数的主要目标是什么?
  • 主要目标是使用流畅的界面提供简单的配置,并管理"默认"值吗?
    ...

因为所有这些问题都会对设计中的微妙差异产生影响。但是,关于你的实际的,高水平的,"句法"问题:

  • 您可以将构建器设计为public static内部类(您在示例中所做的操作)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Person {
        ...
        public static PersonBuilder create() { ... }

        public static class PersonBuilder {
            ...
            public Person build() { ... }
        }
    }

    这提供了最严格的隐私形式:PersonPersonBuilder的构造函数都可以是private

  • 您还可以将实际的类及其构建器放在单独的文件中:

    1
    2
    3
    public class Person {
        ...
    }

    1
    2
    3
    public class PersonBuilder {
        ...
    }

    这里可以实现合理程度的隐私:两者的构造函数可以是包私有的(即具有默认可见性)。

在这两种情况下,除了构建器类的名称(package.Person.PersonBuilder vs. package.PersonBuilder)之外,客户端的实际使用情况都是相同的。课程的"内容"也是相同的(除了略微不同的可见性)。在这两种情况下,如果需要,您可以创建Person的子类,具体取决于构建器配置,构建器本身可以具有流畅的接口。


作为构建器模式的替代方法,您还可以使用参数对象:

1
2
3
4
5
class FooParams {
  public int size;
  public Color color;
  public String name;
}

如果您愿意,可以在此处使用getter和setter,而不是公共字段。

然后Foo构造函数将其中一个作为参数:

1
2
3
4
5
public Foo(FooParams params) {
  this.size = params.size;
  this.color = params.color;
  this.name = params.name;
}


使用成分。为了使事情更简单,更清晰,请不要复制源(Foo)和构建器(Builder)类中的所有属性。

例如,在Builder中包含Foo类而不是每个Foo属性。

简单的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import java.util.*;

class UserBasicInfo{
    String nickName;
    String birthDate;
    String gender;

    public UserBasicInfo(String name,String date,String gender){
        this.nickName = name;
        this.birthDate = date;
        this.gender = gender;        
    }

    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Name:DOB:Gender:").append(nickName).append(":").append(birthDate).append(":").
        append(gender);
        return sb.toString();
    }
}

class ContactInfo{
    String eMail;
    String mobileHome;
    String mobileWork;

    public ContactInfo(String mail, String homeNo, String mobileOff){
        this.eMail = mail;
        this.mobileHome = homeNo;
        this.mobileWork = mobileOff;
    }    
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("email:mobile(H):mobile(W):").append(eMail).append(":").append(mobileHome).append(":").append(mobileWork);
        return sb.toString();
    }
}
class FaceBookUser {
    String userName;
    UserBasicInfo userInfo;
    ContactInfo contactInfo;

    public FaceBookUser(String uName){
        this.userName = uName;
    }    
    public void setUserBasicInfo(UserBasicInfo info){
        this.userInfo = info;
    }
    public void setContactInfo(ContactInfo info){
        this.contactInfo = info;
    }    
    public String getUserName(){
        return userName;
    }
    public UserBasicInfo getUserBasicInfo(){
        return userInfo;
    }
    public ContactInfo getContactInfo(){
        return contactInfo;
    }

    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("|User|").append(userName).append("|UserInfo|").append(userInfo).append("|ContactInfo|").append(contactInfo);
        return sb.toString();
    }

    static class FaceBookUserBuilder{
        FaceBookUser user;
        public FaceBookUserBuilder(String userName){
            this.user = new FaceBookUser(userName);
        }
        public FaceBookUserBuilder setUserBasicInfo(UserBasicInfo info){
            user.setUserBasicInfo(info);
            return this;
        }
        public FaceBookUserBuilder setContactInfo(ContactInfo info){
            user.setContactInfo(info);
            return this;
        }
        public FaceBookUser build(){
            return user;
        }
    }
}
public class BuilderPattern{
    public static void main(String args[]){
        FaceBookUser fbUser1 = new FaceBookUser.FaceBookUserBuilder("Ravindra").build(); // Mandatory parameters
        UserBasicInfo info = new UserBasicInfo("sunrise","25-May-1975","M");

        // Build User name + Optional Basic Info
        FaceBookUser fbUser2 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                                setUserBasicInfo(info).build();

        // Build User name + Optional Basic Info + Optional Contact Info
        ContactInfo cInfo = new ContactInfo("[email protected]","1111111111","2222222222");
        FaceBookUser fbUser3 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                                setUserBasicInfo(info).
                                                setContactInfo(cInfo).build();

        System.out.println("Facebook user 1:"+fbUser1);
        System.out.println("Facebook user 2:"+fbUser2);
        System.out.println("Facebook user 3:"+fbUser3);
    }
}

输出:

1
2
3
Facebook user 1:|User|Ravindra|UserInfo|null|ContactInfo|null
Facebook user 2:|User|Ravindra|UserInfo|Name:DOB:Gender:sunrise:25-May-1975:M|ContactInfo|null
Facebook user 3:|User|Ravindra|UserInfo|Name:DOB:Gender:sunrise:25-May-1975:M|ContactInfo|email:mobile(H):mobile(W):xxx@xyz.com:1111111111:2222222222

说明:

  • FaceBookUser是一个复杂的对象,其下面的属性使用组合:

    1
    2
    3
    String userName;
    UserBasicInfo userInfo;
    ContactInfo contactInfo;
  • FaceBookUserBuilder是一个静态构建器类,包含并构建FaceBookUser

  • userName只是构建FaceBookUser的必需参数

  • FaceBookUserBuilder通过设置可选参数UserBasicInfoContactInfo来构建FaceBookUser

  • 此示例说明了使用Builder构建的具有不同属性的三个不同FaceBookUsers

  • fbUser1仅作为FaceBookUser构建,仅具有userName属性
  • fbUser2使用userName和UserBasicInfo构建为FaceBookUser
  • fbUser3使用userName,UserBasicInfo和ContactInfo构建为FaceBookUser
  • 在此示例中,使用了合成而不是在Builder类中复制FaceBookUser的所有属性。

    编辑:

    将所有相关属性分组为逻辑类。在FaceBookUser中定义所有这些类。不是在Builder中再次添加所有这些成员变量,而是在Builder类中包含FaceBookUser

    为简单起见,我添加了两个类:UserBasicInfo和ContactInfo。现在使用其他属性来爆炸这个FaceBookUser类

    1
    2
    3
    4
    5
    6
    7
    8
    NewsFeed
    Messages
    Friends
    Albums
    Events
    Games
    Pages
    Ads

    等等

    如果在BuilderFaceBookUser中复制所有这些属性,代码将变得难以管理。相反,通过在FaceBookUserBuilder本身中使用FaceBookUser的组合,您可以简单地构建过程。

    添加上述属性后,您将像往常一样逐步构建FaceBookUser

    它会是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    FaceBookUser fbUser3 = new FaceBookUser.FaceBookUserBuilder("Ravindra").
                                            setUserBasicInfo(info).
                                            setNewsFeed(newsFeed).
                                            setMessages(messages).
                                            setFriends(friends).
                                            setAlbums(albums).
                                            setEvents(events).
                                            setGames(games).
                                            setAds(ads).build();