【问题标题】:The builder pattern and a large number of mandatory parametersbuilder模式和大量强制参数
【发布时间】:2021-09-29 14:24:42
【问题描述】:

迄今为止,我使用构建器模式的following 实现(与here 描述的实现相反):

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() 方法中对所需参数执行验证。

什么是最佳实践,有没有我没有考虑过的更好的方法?

【问题讨论】:

标签: java parameters design-patterns builder


【解决方案1】:

如果您有许多强制参数,您可以使用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();

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

【讨论】:

    【解决方案2】:

    但是,我最近一直在努力理解当您的所有参数都是强制性的(或至少绝大多数都是强制性的)时,该模式有什么好处。

    流利的构建器模式仍然是有益的:

    1. 它更具可读性 - 它有效地允许命名参数,因此调用不仅仅是一长串未命名参数

    2. 它是无序的 - 这使您可以将参数组合到逻辑组中,或者作为单个构建器设置器调用的一部分,或者只是让您使用自然顺序来调用最有意义的构建器设置器方法特定的实例化。


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

    【讨论】:

    • 谢谢,伯特。我认为这最好地总结了我在了解更多关于这种特定模式时一直在努力解决的问题。我认为主要是我希望实现的灵活性(和不变性)。
    【解决方案3】:

    我最近一直在努力了解任何模式的模式 当您的所有参数都是强制性的时会受益

    该模式简化了不可变类的创建并提高了代码的可读性。考虑下面的 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();
    

    解决此问题的一种方法是将 参数被传递到自己的类中

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

    【讨论】:

    • 我绝对同意构建对象的流畅性使事情变得更容易。我认为至少对于我的用例来说,让这些属性中的大多数具有自己的 add 方法,然后在构建对象时进行验证是最有意义的。感谢您的回复。
    • 至少对于 p1,如果需要头发颜色,编译器会告诉您它已损坏。对于 p2,这是一个运行时错误。
    【解决方案4】:

    我很少(如果有的话)看到推广的构建器模式的一个优点是,它也可以用于有条件地构造对象,例如,只有在所有必需参数都正确或其他必需资源可用时。在这方面,它们提供与static factory method 类似的好处。

    【讨论】:

      【解决方案5】:

      我认为这在你有很大的强制值的情况下是合适的,虽然接口的数量会增加但代码会很干净

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

      查看此参考: Builder Pattern with Twist

      【讨论】:

        【解决方案6】:

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

        这假设您的构建器是不可变的:构建器设置器不会设置属性并返回 this,而是返回自己的新副本以及更改。正如您在上面指出的,参数对象是绕过重复参数样板的另一种方法。在那里,您甚至不需要构建器模式:只需将参数对象传递给您的实现构造函数。

        【讨论】:

          【解决方案7】:

          我的匿名类解决方案。这里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();
            }
          }
          

          【讨论】:

            【解决方案8】:

            我刚刚发布了一个免费的 Intellij 插件来解决这类问题。本质上,您为构建器中的每个参数定义一个方法,您可以注释哪些是强制性的,哪些不是,IntelliJ 补全将突出显示哪些是强制性和可选参数。随意尝试一下:

            https://github.com/banterly91/Java-Builder-Guided-Completion-Intellij-Plugin

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-04-29
              • 1970-01-01
              • 1970-01-01
              • 2010-10-28
              • 1970-01-01
              • 2012-10-18
              相关资源
              最近更新 更多