如何改进构建器模式?

发布于 2024-08-09 09:29:26 字数 2076 浏览 6 评论 0原文

动机

最近我寻找一种无需向构造函数传递大量参数即可初始化复杂对象的方法。我尝试使用构建器模式,但我不喜欢这样的事实:我无法在编译时检查我是否确实设置了所有需要的值。

传统构建器模式

当我使用构建器模式创建我的 Complex 对象时,创建更加“类型安全”,因为更容易看出参数的用途:

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();

但现在我遇到了问题,即我很容易错过一个重要的参数。我可以在 build() 方法中检查它,但这只是在运行时。在编译时,如果我错过了什么,没有任何东西会警告我。

增强的构建器模式

现在我的想法是创建一个构建器,如果我错过了所需的参数,它会“提醒”我。我的第一次尝试如下所示:

public class Complex {
    private String m_first;
    private String m_second;
    private String m_third;

    private Complex() {}

    public static class ComplexBuilder {
        private Complex m_complex;

        public ComplexBuilder() {
            m_complex = new Complex();
        }

        public Builder2 setFirst( String first ) {
            m_complex.m_first = first;
            return new Builder2();
        }

        public class Builder2 {
            private Builder2() {}
            Builder3 setSecond( String second ) {
                m_complex.m_second = second;
                return new Builder3();
            }
        }

        public class Builder3 {
            private Builder3() {}
            Builder4 setThird( String third ) {
                m_complex.m_third = third;
                return new Builder4();
            }
        }

        public class Builder4 {
            private Builder4() {}
            Complex build() {
                return m_complex;
            }
        }
    }
}

如您所见,构建器类的每个设置器都会返回不同的内部构建器类。每个内部构建器类只提供一个 setter 方法,最后一个仅提供一个 build() 方法。

现在对象的构造再次看起来像这样:

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();

...但是没有办法忘记所需的参数。编译器不会接受它。

可选参数

如果我有可选参数,我将使用最后一个内部构建器类 Builder4 来设置它们,就像“传统”构建器一样,返回自身。

问题

  • 这是一个众所周知的模式吗?它有一个特殊的名字吗?
  • 你看到任何陷阱吗?
  • 您是否有任何改进实施的想法 - 从减少代码行的意义上来说?

Motivation

Recently I searched for a way to initialize a complex object without passing a lot of parameter to the constructor. I tried it with the builder pattern, but I don't like the fact, that I'm not able to check at compile time if I really set all needed values.

Traditional builder pattern

When I use the builder pattern to create my Complex object, the creation is more "typesafe", because it's easier to see what an argument is used for:

new ComplexBuilder()
        .setFirst( "first" )
        .setSecond( "second" )
        .setThird( "third" )
        ...
        .build();

But now I have the problem, that I can easily miss an important parameter. I can check for it inside the build() method, but that is only at runtime. At compile time there is nothing that warns me, if I missed something.

Enhanced builder pattern

Now my idea was to create a builder, that "reminds" me if I missed a needed parameter. My first try looks like this:

public class Complex {
    private String m_first;
    private String m_second;
    private String m_third;

    private Complex() {}

    public static class ComplexBuilder {
        private Complex m_complex;

        public ComplexBuilder() {
            m_complex = new Complex();
        }

        public Builder2 setFirst( String first ) {
            m_complex.m_first = first;
            return new Builder2();
        }

        public class Builder2 {
            private Builder2() {}
            Builder3 setSecond( String second ) {
                m_complex.m_second = second;
                return new Builder3();
            }
        }

        public class Builder3 {
            private Builder3() {}
            Builder4 setThird( String third ) {
                m_complex.m_third = third;
                return new Builder4();
            }
        }

        public class Builder4 {
            private Builder4() {}
            Complex build() {
                return m_complex;
            }
        }
    }
}

As you can see, each setter of the builder class returns a different internal builder class. Each internal builder class provides exactly one setter method and the last one provides only a build() method.

Now the construction of an object again looks like this:

new ComplexBuilder()
    .setFirst( "first" )
    .setSecond( "second" )
    .setThird( "third" )
    .build();

...but there is no way to forget a needed parameter. The compiler wouldn't accept it.

Optional parameters

If I had optional parameters, I would use the last internal builder class Builder4 to set them like a "traditional" builder does, returning itself.

Questions

  • Is this a well known pattern? Does it have a special name?
  • Do you see any pitfalls?
  • Do you have any ideas to improve the implementation - in the sense of fewer lines of code?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

猫瑾少女 2024-08-16 09:29:26

传统的构建器模式已经处理了这个问题:只需在构造函数中获取强制参数即可。当然,没有什么可以阻止调用者传递 null,但您的方法也不会。

我发现您的方法的一个大问题是,您要么具有大量强制参数的类组合爆炸,要么强制用户在一个特定的序列中设置参数,这很烦人。

此外,还有很多额外的工作。

The traditional builder pattern already handles this: simply take the mandatory parameters in the constructor. Of course, nothing prevents a caller from passing null, but neither does your method.

The big problem I see with your method is that you either have a combinatorical explosion of classes with the number of mandatory parameters, or force the user to set the parameters in one particular sqeuence, which is annoying.

Also, it is a lot of additional work.

心头的小情儿 2024-08-16 09:29:26
public class Complex {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public Complex(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}
public class Complex {
    private final String first;
    private final String second;
    private final String third;

    public static class False {}
    public static class True {}

    public static class Builder<Has1,Has2,Has3> {
        private String first;
        private String second;
        private String third;

        private Builder() {}

        public static Builder<False,False,False> create() {
            return new Builder<>();
        }

        public Builder<True,Has2,Has3> setFirst(String first) {
            this.first = first;
            return (Builder<True,Has2,Has3>)this;
        }

        public Builder<Has1,True,Has3> setSecond(String second) {
            this.second = second;
            return (Builder<Has1,True,Has3>)this;
        }

        public Builder<Has1,Has2,True> setThird(String third) {
            this.third = third;
            return (Builder<Has1,Has2,True>)this;
        }
    }

    public Complex(Builder<True,True,True> builder) {
        first = builder.first;
        second = builder.second;
        third = builder.third;
    }

    public static void test() {
        // Compile Error!
        Complex c1 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2"));

        // Compile Error!
        Complex c2 = new Complex(Complex.Builder.create().setFirst("1").setThird("3"));

        // Works!, all params supplied.
        Complex c3 = new Complex(Complex.Builder.create().setFirst("1").setSecond("2").setThird("3"));
    }
}
幼儿园老大 2024-08-16 09:29:26

不,这不是什么新鲜事。您实际上正在做的是通过扩展标准构建器来创建某种 DSL模式来支持分支,这是确保构建器不会为实际对象生成一组冲突设置的绝佳方法。

就我个人而言,我认为这是构建器模式的一个很好的扩展,你可以用它做各种有趣的事情,例如在工作中,我们有 DSL 构建器用于一些数据完整性测试,这允许我们做诸如 assertMachine( ).usesElectricity().and().makesGrindingNoises().whenTurnedOn();。好吧,也许不是最好的例子,但我想你明白了。

No, it's not new. What you're actually doing there is creating a sort of a DSL by extending the standard builder pattern to support branches which is among other things an excellent way to make sure the builder doesn't produce a set of conflicting settings to the actual object.

Personally I think this is a great extension to builder pattern and you can do all sorts of interesting things with it, for example at work we have DSL builders for some of our data integrity tests which allow us to do things like assertMachine().usesElectricity().and().makesGrindingNoises().whenTurnedOn();. OK, maybe not the best possible example but I think you get the point.

梦里兽 2024-08-16 09:29:26

为什么不在构建器构造函数中放入“所需”参数?

public class Complex
{
....
  public static class ComplexBuilder
  {
     // Required parameters
     private final int required;

     // Optional parameters
     private int optional = 0;

     public ComplexBuilder( int required )
     {
        this.required = required;
     } 

     public Builder setOptional(int optional)
     {
        this.optional = optional;
     }
  }
...
}

Effective Java 中概述了此模式。

Why don't you put "needed" parameters in the builders constructor?

public class Complex
{
....
  public static class ComplexBuilder
  {
     // Required parameters
     private final int required;

     // Optional parameters
     private int optional = 0;

     public ComplexBuilder( int required )
     {
        this.required = required;
     } 

     public Builder setOptional(int optional)
     {
        this.optional = optional;
     }
  }
...
}

This pattern is outlined in Effective Java.

淑女气质 2024-08-16 09:29:26

我不会使用多个类,而是只使用一个类和多个接口。它会强制执行您的语法,而无需输入太多内容。它还允许您紧密地查看所有相关代码,从而更容易在更大的级别上理解代码的情况。

Instead of using multiple classes I would just use one class and multiple interfaces. It enforces your syntax without requiring as much typing. It also allows you to see all related code close together which makes it easier to understand what is going on with your code at a larger level.

2024-08-16 09:29:26

恕我直言,这看起来很臃肿。如果您必须拥有所有参数,请将它们传递到构造函数中。

IMHO, this seems bloated. If you have to have all the parameters, pass them in the constructor.

芸娘子的小脾气 2024-08-16 09:29:26

我已经看到/使用过这个:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();

然后将它们传递给需要它们的对象。

I've seen/used this:

new ComplexBuilder(requiredvarA, requiedVarB).optional(foo).optional(bar).build();

Then pass these to your object that requires them.

烟沫凡尘 2024-08-16 09:29:26

当您有很多可选参数时,通常会使用构建器模式。如果您发现需要许多必需参数,请首先考虑以下选项:

  • 您的类可能做得太多了。仔细检查它是否违反单一责任原则。问问自己为什么需要一个包含如此多必需实例变量的类。
  • 您的构造函数可能做得太多 。构造函数的工作就是构造。 (他们在命名时并没有很有创意;D)就像类一样,方法也有单一职责原则。如果您的构造函数不仅仅执行字段赋值,那么您需要一个充分的理由来证明这一点。您可能会发现您需要工厂方法而不是构建器。
  • 您的参数可能做得太少。问问自己,您的参数是否可以分组为一个小结构(或 Java 中的类似结构的对象)。不要害怕进行小班授课。如果您确实发现需要创建一个结构体或小型类,请不要忘记 重构 out 功能 属于结构而不是更大的类。

The Builder Pattern is generally used when you have a lot of optional parameters. If you find you need many required parameters, consider these options first:

  • Your class might be doing too much. Double check that it doesn't violate Single Responsibility Principle. Ask yourself why you need a class with so many required instance variables.
  • You constructor might be doing too much. The job of a constructor is to construct. (They didn't get very creative when they named it ;D ) Just like classes, methods have a Single Responsibility Principle. If your constructor is doing more than just field assignment, you need a good reason to justify that. You might find you need a Factory Method rather than a Builder.
  • Your parameters might be doing too little. Ask yourself if your parameters can be grouped into a small struct (or struct-like object in the case of Java). Don't be afraid to make small classes. If you do find you need to make a struct or small class, don't forget to refactor out functionality that belongs in the struct rather than your larger class.
£烟消云散 2024-08-16 09:29:26

有关何时使用构建器模式及其优点的更多信息,您应该查看我的帖子以了解另一个类似的问题这里

For more information on when to use the Builder Pattern and its advantages you should check out my post for another similar question here

任性一次 2024-08-16 09:29:26

问题1:关于模式的名称,我喜欢“Step Builder”这个名字:

问题2/3:关于陷阱和建议,感觉已经结束了对于大多数情况来说都很复杂。

  • 您在使用构建器时强制执行顺序,这在我的经验中是不寻常的。我知道这在某些情况下很重要,但我从来不需要它。例如,我认为没有必要在这里强制执行序列:

    Person.builder().firstName("John").lastName("Doe").build()
    Person.builder().lastName("Doe").firstName("John").build()

  • 但是,很多时候构建器需要强制执行一些约束以防止伪造对象建造的。也许您想确保提供所有必填字段或字段组合有效。我猜这就是您想要在建筑物中引入排序的真正原因。

    在这种情况下,我喜欢 Joshua Bloch 的建议,在 build() 方法中进行验证。这有助于跨领域验证,因为此时一切都可用。请参阅此答案:https://softwareengineering.stackexchange.com/a/241320

总之,我不会添加任何代码变得复杂只是因为您担心“错过”对构建器方法的调用。在实践中,通过测试用例很容易发现这一点。也许从普通的构建器开始,然后如果您一直被缺少方法调用所困扰,则可以引入它。

Question 1: Regarding the name of the pattern, I like the name "Step Builder":

Question 2/3: Regarding pitfalls and recommendations, this feels over complicated for most situations.

  • You are enforcing a sequence in how you use your builder which is unusual in my experience. I could see how this would be important in some cases but I've never needed it. For example, I don't see the need to force a sequence here:

    Person.builder().firstName("John").lastName("Doe").build()
    Person.builder().lastName("Doe").firstName("John").build()

  • However, many times the builder needed to enforce some constraints to prevent bogus objects from being built. Maybe you want to ensure that all required fields are provided or that combinations of fields are valid. I'm guessing this is the real reason you want to introduce sequencing into the building.

    In this case, I like recommendation of Joshua Bloch to do the validation in the build() method. This helps with cross field validation because everything is available at this point. See this answer: https://softwareengineering.stackexchange.com/a/241320

In summary, I wouldn't add any complication to the code just because you are worried about "missing" a call to a builder method. In practice, this is easily caught with a test case. Maybe start with a vanilla Builder and then introduce this if you keep getting bitten by missing method calls.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文