建造者模式和大量强制参数

85

迄今为止,我使用以下的建造者模式实现(与此处描述的实现相反):

public class Widget {
    public static class Builder {
        public Builder(String name, double price) { ... }
        public Widget build() { ... }
        public Builder manufacturer(String value) { ... }
        public Builder serialNumber(String value) { ... }
        public Builder model(String value) { ... }
    }

    private Widget(Builder builder) { ... }
}

在我遇到的大多数需要构建具有各种必需和可选参数的复杂对象的情况下,这种方法效果很好。然而,最近我一直在努力理解当所有参数都是必需的(或者至少绝大部分都是必需的)时,该模式有何好处。

解决这个问题的一种方法是将传递的参数逻辑分组为它们自己的类,以减少传递给构建器构造函数的参数数量。

例如,不要这样写:

Widget example = new Widget.Builder(req1, req2, req3,req4,req5,req6,req7,req8)
                           .addOptional(opt9)
                           .build();

变成如下分组:

Object1 group1 = new Object1(req1, req2, req3, req4);
Object2 group2 = new Object2(req5, req6);

Widget example2 = new Widget.Builder(group1, group2, req7, req8)
                            .addOptional(opt9)
                            .build();

虽然拥有单独的对象可以简化很多事情,但是如果不熟悉代码的话,这也会让人有些难以理解。我考虑过将所有参数都移动到它们自己的addParam(param)方法中,然后在build()方法中对必填参数进行验证。

什么是最佳实践?或者是否有更好的方法我没有考虑到?


3
比当前任何答案都更好的解决方案在Builder pattern with a twist中描述。使用不同的接口来设置每个必需参数。 - jaco0646
我尝试了@jaco0646提出的建议,但我认为我开发了更好的替代品:https://github.com/banterly91/Java-Builder-Guided-Completion-Intellij-Plugin 欢迎查看。 - dragosb
8个回答

70

如果您有许多必填参数,可以使用步骤生成器(Step Builder)。简而言之:您为每个必填参数定义一个接口,建造者方法返回下一个必填建造者接口或可选方法的建造者本身。建造者仍然是一个实现所有接口的单个类。

interface StepB {
    StepBuilder b(String b);
}

interface StepA {
    StepB a(String a);
}

final class StepBuilder implements StepA, StepB {
    private String a;
    private String b;
    private String c = "";

    private StepBuilder() {
    }

    static StepA with() {
      return new StepBuilder();
    }

    // mandatory, from StepA
    @Override
    StepB a(String a) {
        this.a = a;
        return this;
    }

    // mandatory, from StepB
    @Override
    StepBuilder b(String b) {
        this.b = b;
        return this;
    }

    // optional
    StepBuilder c(String c) {
        this.c = c;
        return this;
    }

    Product build() {
        return new Product(a, b, c);
    }
}

使用方法:

StepBuilder.with().a("hello").b("world").build();

// or with the optional parameter c
StepBuilder.with().a("hello").b("world").c("!").build();

请记住,我只是为了方便解释而将该类命名为StepBuilder。最好给它一个反映所在领域某个方面的名称。例如:

Url.with().host("example.com").port(81).path("some/where").query("status=1").build()

像Kotlin和Scala这样的语言在这里更方便,因为它们提供具有默认值的命名参数。


4
这个步骤构建器实现(就像builder()参数实现一样)唯一的缺点是需要按特定顺序指定参数。不过,要解决这个问题需要每个构建器2^n个类(n是必需参数的数量)。如果构建器是自动生成的,则这不是问题,但如果有超过3个必需参数,那么绝对不是你想手写的东西。 - Rik Schaaf

40
然而,最近我一直在苦苦思考,当所有参数都是强制的(或者至少绝大部分是强制的)时,这种模式究竟有什么好处。

然而流畅构建器模式仍然有益处:

  1. 它更易读 - 它有效地允许命名参数,使调用不只是一长串未命名参数。

  2. 它无序性 - 这让你可以将参数组合成逻辑组,可以作为单个构建器 setter 调用的一部分,也可以通过让你使用自然顺序来调用构建器 setter 方法,使这种特定实例化最有意义。


Widget example = new Widget.Builder(req1, req2, req3,req4,req5,req6,req7,req8)
                               .addOptional(opt9)
                               .build();

变成以下分组:

Object1 group1  = new Object1(req1, req2, req3, req4);
Object2 group2  = new Object2(req5, req6);
Widget example2 = new Widget.Builder(group1, group2, req7, req8)
                            .addOptional(opt9)
                            .build();
虽然将参数分开成单独的对象可以简化事情,但对于不熟悉代码的人来说,这也会使事情变得有些难以理解。我考虑过将所有参数移动到它们自己的addParam(param)方法中,然后在build()方法中对需要的参数进行验证。

我倾向于在适当或自然的情况下采用混合方式。不一定要全部放在构造函数中或每个参数都有自己的addParam方法中。Builder让您具有灵活性,可以选择其中一个,另一个,在中间,或组合使用:

Widget.Builder builder = new Widget.Builder(Widget.BUTTON);

builder.withWidgetBackingService(url, resource, id);
builder.withWidgetStyle(bgColor, lineWidth, fontStyle);
builder.withMouseover("Not required");

Widget example = builder.build();

1
谢谢,Bert。我认为这最好地概括了我在学习这个特定模式时所苦苦挣扎的内容。我认为我主要是在寻求灵活性(和不可变性)。 - speedRS

4
最近我一直在努力理解当所有参数都是必需时,这种模式有什么好处。
该模式简化了不可变类的创建并促进了可读性强的代码。考虑下面的Person类(具有传统构造函数和构建器)。
public static class Person {

    private static final class Builder {
        private int height, weight, age, income, rank;
        public Builder setHeight(final int height) { this.height = height; return this; }
        public Builder setWeight(final int weight) { this.weight = weight; return this; }
        public Builder setAge(final int age) { this.age = age; return this; }
        public Builder setIncome(final int income) {    this.income = income; return this; }
        public Builder setRank(final int rank) { this.rank = rank; return this; }
        public Person build() { return new Person(this); }
    }

    private final int height;
    private final int weight;
    private final int age;
    private final int income;
    private final int rank;

    public Person(final int height, final int weight, final int age, final int income, final int rank) {
        this.height = height; this.weight = weight; this.age = age; this.income = income; this.rank = rank;
    }

    private Person(final Builder builder) {
        height = builder.height; weight = builder.weight; age = builder.age; income = builder.income; rank = builder.rank;
        // Perform validation
    }

    public int getHeight() { return height; }
    public int getWeight() { return weight; }
    public int getAge() { return age; }
    public int getIncome() { return income; }
    public int getRank() {  return rank; }

}

哪种建造方法更容易理解?
final Person p1 = new Person(163, 184, 48, 15000, 23);
final Person p2 = new Person.Builder().setHeight(163).setWeight(184).setAge(48).
    setIncome(15000).setRank(23).build();

一种解决这个问题的方法是将传递的参数逻辑分组到它们自己的类中

当然,这就是内聚性的原则,无论对象构造语义如何都应该采用。


2
我完全同意构建对象的流畅性会让事情变得更容易。在我的使用场景中,我认为大多数属性都应该有自己的“add”方法,然后在构建对象时进行验证,这样是最合理的。谢谢你的回复。 - speedRS
8
使用 p1,如果需要指定头发颜色,则编译器会告诉您代码有问题。使用 p2,则会在运行时出现错误。 - EJ Campbell

3
建造者模式的一个优点是很少(如果有的话)被宣传的,即它也可以用于有条件地构造对象,例如仅在所有必需参数正确或其他必需资源可用时才构造对象。在这方面,它们提供了类似于静态工厂方法的好处。

2
我认为,如果您有很大的强制值,使用这种方法可能是合适的,虽然接口数量会增加,但代码会更加清晰。
public class PersonBuilder implements NamePersonBuilder, LastNamePersonBuilder, 
                                  BirthDatePersonBuilder, FinalPersonBuilder {

private String name;
private String lastName;
private Date birthDate;
private String phoneNumber;

/**
 * Private constructor to force the use of the factroy method
 */
private PersonBuilder() {
}

/**
 * Creates a new person builder
 */
public static NamePersonBuilder aPerson() {
    return new PersonBuilder();
}

public LastNamePersonBuilder withName(String aName) {
    name = aName;
    return this;
}

public BirthDatePersonBuilder withLastName(String aLastName) {
    lastName = aLastName;
    return this;
}

public FinalPersonBuilder withBirthDate(Date aBirthDate) {
    birthDate = aBirthDate;
    return this;
}

public FinalPersonBuilder andPhoneNumber(String aPhoneNumber) {
    phoneNumber = aPhoneNumber;
    return this;
}

public Person build() {
    // The constructor and setters for Person has default scope
    // and is located in the same package as the builder
    Person p = new Person();
    p.setName(name);
    p.setLastName(lastName);
    p.setBirthDate(birthDate);
    p.setPhoneNumber(phoneNumber);
    return p;
}

interface NamePersonBuilder {
    LastNamePersonBuilder withName(String aName);
}

interface LastNamePersonBuilder {
    BirthDatePersonBuilder withLastName(String aLastName);
}

interface BirthDatePersonBuilder {
    FinalPersonBuilder withBirthDate(Date aBirthDate);
}

interface FinalPersonBuilder {
    FinalPersonBuilder andPhoneNumber(String aPhoneNumber);
    Person build();
}}

这将强制用户设置所有必填值,并强制按照特定顺序进行设置。因此,构建一个人的代码如下:
PersonBuilder.aPerson()
    .withName("Name")
    .withLastName("LastName")
    .withBirthDate(new Date())
    .build();

请查看这个参考链接: 带有变化的建造者模式


0

建造者/工厂仍然可以让您将接口与实现类型解耦(或让您插入适配器等),假设Widget成为一个接口,并且您有一种方法来注入或隐藏new Widget.Builder

如果您不关心解耦,并且您的实现是一次性的,则您是正确的:建造者模式并没有比普通构造函数更有用(它仍然使用属性-每个构建器方法样式标记其参数)。

如果您反复创建具有参数变化很小的对象,则仍然可能会有所帮助。您可以传递、缓存等中间生成器,在插入多个属性后获取:

Widget.Builder base = new Widget.Builder(name, price).model("foo").manufacturer("baz");

// ...

Widget w1 = base.serialNumber("bar").build();
Widget w2 = base.serialNumber("baz").build();
Widget w3 = base.serialNumber("quux").build();

这假设你的构建器是不可变的:构建器的setter不会设置属性并返回this,而是返回一个带有更改的新副本。正如你上面指出的那样,参数对象是避免重复参数模板代码的另一种方法。在那里,你甚至不需要使用构建器模式:只需将参数对象传递给你的实现构造函数即可。


0

我的解决方案使用匿名类。这里familyName是必需参数,而givenName是可选的。这个解决方案的主要目标是强制创建Person的程序员设置必需的参数(如果他不这样做,Java将无法编译)。

new Person(
    Person.parametersObject(new Person.RequiredParameters() {
      @Override
      public void setFamilyName() {
        this.familyName = "Jonson";
      }
    })
    .setGivenName("John")
);

实际上,目标并没有完全达成:因为我无法强制程序员编写this.familyName = familyName;,但他必须实现setFamilyName。如果程序员不是白痴,他知道在这个方法中该做什么,但由于疲劳可能会忘记。
实现:
public class Person {

  private String familyName;
  private String givenName;


  public Person(ParametersObject parametersObject) {
    parametersObject.initializeSpecifiedFields(this);
  }

  public static ParametersObject parametersObject(Person.RequiredParameters requiredParameters) {
    return new Person.ParametersObject(requiredParameters);
  }


  public String getFamilyName() {
    return familyName;
  }
  public Person setFamilyName(String familyName) {
    this.familyName = familyName;
    return this;
  }

  public String getGivenName() {
    return givenName;
  }
  public Person setGivenName(String givenName) {
    this.givenName = givenName;
    return this;
  }


  public static class ParametersObject {

    private String familyName;
    private String givenName;

    public ParametersObject(Person.RequiredParameters requiredParameters) {
      this.familyName = requiredParameters.familyName;
    }

    public void initializeSpecifiedFields(Person person) {
      person.familyName = this.familyName;
      person.givenName = this.givenName;
    }

    public ParametersObject setGivenName(String givenName) {
      this.givenName = givenName;
      return this;
    }
  }

  public static abstract class RequiredParameters {
    public String familyName;
    public abstract void setFamilyName();
  }
}

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接