建设者模式:什么时候失败?


45

在实施“构建器模式”时,我经常感到困惑于何时让构建失败,甚至每隔几天我就此采取不同的立场。

首先来一些解释:

  • 随着早期的失败我的意思是建立一个对象应为无效参数传入,一旦失败,所以里面的SomeObjectBuilder
  • 有了失败的晚期我的意思是,只有建立一个对象可以在失败build()的是隐含调用对象的构造函数来建立呼叫。

然后是一些参数:

  • 支持延迟失败:构建器类不应只不过是仅包含值的类。而且,它导致更少的代码重复。
  • 支持尽早失败:软件编程中的一种通用方法是您希望尽早发现问题,因此,最合乎逻辑的检查位置是在构建器类的“构造器”,“设置器”中,最后是在build方法中。

关于此的普遍共识是什么?


8
我认为迟到没有任何好处。有人说构建器类“应该”优先于良好的设计,并且尽早发现bug总是比晚发现bug更好。
2014年

3
另一种看待此问题的方法是,构建器可能不知道什么是有效数据。在这种情况下,早期失败更多是关于一旦知道错误就失败。 早失败,将是制造商返回null的时候出现了一个问题对象build()
克里斯

如果您不添加发出警告的方法,也不提供在构建器中进行修复的方法,那么失败就没有意义了。
2014年

Answers:


34

让我们看一下可以在其中放置验证代码的选项:

  1. 在建设者的二传手里面。
  2. 里面的build()方法。
  3. 在构造的实体内部:创建实体时将在build()方法中调用它。

选项1使我们能够更早地发现问题,但是在某些复杂情况下,我们只能验证具有完整上下文的输入,因此,至少要在build()方法中进行部分验证。因此,选择选项1将导致代码不一致,一部分验证在一个地方完成,而另一部分则在另一地方完成。

选项2并不比选项1差很多,因为通常在builder之前build()(特别是在流畅的接口中)调用builder中的setter 。因此,在大多数情况下,仍然有可能及早发现问题。但是,如果生成器不是创建对象的唯一方法,则它将导致验证代码重复,因为您需要在创建对象的所有地方都使用它。在这种情况下,最合乎逻辑的解决方案是使验证尽可能靠近所创建的对象,即在其内部。这是选项3

从SOLID的角度来看,将验证放入构建器中也违反了SRP:构建器类已经负责聚合数据以构造对象。验证是根据自己的内部状态建立合同,这是检查另一个对象状态的新职责。

因此,从我的角度来看,从设计的角度来看,不仅晚失败更好,而且在构造的实体内部而不是在构建器本身中失败也更好。

UPD:当在构建器内进行验证(选项1或2)有意义时,此评论使我想起了另一种可能性。如果构建器在要创建的对象上拥有自己的合同,这确实很有意义。例如,假设我们有一个构建器,该构建器构造具有特定内容(例如,数字范围列表)的字符串1-2,3-4,5-6。该构建器可能具有类似的方法addRange(int min, int max)。结果字符串对这些数字一无所知,也不必知道。构建器本身定义字符串的格式和数字的约束。因此,该方法addRange(int,int)必须验证输入数字并在max小于min时引发异常。

就是说,一般规则是仅验证由建造者本身定义的合同。


我认为值得注意的是,虽然选项1可能导致“不一致”的检查时间,但如果一切都“尽早”进行,则仍可以将其视为一致的。如果使用构建器的变体StepBuilder代替,则使“尽早”变得更加容易一点。
约书亚·泰勒

如果如果传递了空字符串,则URI构建器将引发异常,这是否违反SOLID?垃圾
Gusdor 2014年

@Gusdor是的,如果它本身引发异常。但是,从用户的角度来看,所有选项似乎都由构建器引发异常。
伊万·加梅尔

那么,为什么不具有build()调用的validate()?这样,几乎没有重复,一致性,也没有违反SRP。它还使无需尝试构建即可验证数据成为可能,并且验证已接近创建。
StellarVortex 2014年

在这种情况下,@ StellarVortex将被验证两次-在builder.build()中进行一次验证,并且,如果数据有效并且我们继续进行对象的构造函数,则在该构造函数中进行验证。
Ivan Gammel 2014年

34

假定您使用Java,请考虑Joshua Bloch在“ 创建和销毁Java对象 ”一文中提供的权威且详细的指导(以下引号中的粗体是我的):

像构造器一样,构造器可以对其参数施加不变性。构建方法可以检查这些不变量。将参数从构建器复制到对象之后,必须检查它们,并在对象字段而不是构建器字段(项目39)上检查它们,这一点至关重要。如果违反了任何不变式,则build方法应抛出一个IllegalStateException(项目60)。异常的detail方法应指出违反了哪个不变式(项目63)。

施加涉及多个参数的不变式的另一种方法是让setter方法采用某些不变式必须保持的整组参数。如果不满足不变量,则setter方法将引发IllegalArgumentException。这样做的优点是,一旦传递了无效参数,便会立即检测出不变的故障,而不是等待构建被调用。

请注意,根据本文编辑者的解释,以上引用中的“项目”指的是“ 有效Java,第二版 ”中提供的规则。

本文并没有深入解释为什么建议这样做,但是,如果您考虑一下,原因是显而易见的。在文章的正确解释中,提供了有关理解这一点的一般性技巧,解释了如何将构建器概念与构造函数概念联系在一起,并且应该在构造函数中检查类不变式,而不是在可能之前/准备对其进行调用的任何其他代码中进行检查。

要更具体地了解为什么在调用构建之前检查不变式会出错,请考虑使用CarBuilder的一个流行示例。生成器方法可以按任意顺序调用,结果,直到构建之前,人们才真正知道特定参数是否有效。

考虑到跑车不能有超过2个座位,一个人怎么知道setSeats(4)还可以吗?只有在构建时,才能确定是否setSportsCar()调用了,也就是是否抛出TooManySeatsException


3
+1用于建议抛出什么异常类型,正是我要寻找的。
Xantix

不确定我是否可以选择。似乎纯粹是在讨论何时只能在组中验证不变性。构建器在不涉及任何其他属性的情况下接受单个属性,并且仅在组具有不变性时才接受属性组。在这种情况下,单个属性应该在构建之前引发异常吗?
Didier A.

19

在我看来,应立即知道由于无法容忍而无效的无效值。换句话说,如果您仅接受正数,并且传入了负数,则无需等待build()被调用。我不会考虑这些您将“期望”发生的问题的类型,因为这是调用方法开头的先决条件。换句话说,您不太可能依赖于设置某些参数的失败。您更有可能假设参数正确,或者您自己进行一些检查。

但是,对于不太容易验证的更复杂的问题,最好在致电时予以告知build()。一个很好的例子就是使用您提供的连接信息建立与数据库的连接。在这种情况下,虽然从技术上讲您可以检查这种情况,但它不再直观,只会使您的代码复杂化。如我所见,这些也是实际可能发生的问题的类型,您必须尝试一下才能真正想到。区别在于将字符串与正则表达式匹配以查看是否可以将其解析为int,然后只是尝试对其进行解析,从而处理可能发生的任何潜在异常之间的区别。

我通常不喜欢在设置参数时抛出异常,因为这意味着必须捕获抛出的所有异常,因此我倾向于使用中的验证build()。因此,由于这个原因,我更喜欢使用RuntimeException,因为同样,传递的参数中的错误通常不应发生。

但是,这比任何事情都是最佳实践。我希望能回答您的问题。


11

据我所知,一般的做法(不确定是否达成共识)是尽早发现可能的错误而失败。这也使得无意间滥用您的API变得更加困难。

如果它是可以在输入中检查的琐碎属性,例如容量或长度应为非负数,则最好立即失败。推迟错误会增加错误和反馈之间的距离,这使得更难找到问题的根源。

如果您不幸处于某个属性的有效性取决于其他属性的情况下,那么您有两种选择:

  • 要求同时提供两个(或多个)属性(即,单个方法调用)。
  • 当您知道没有更多更改要传入时(build()即何时调用),请测试有效性。

与大多数情况一样,这是在上下文中做出的决定。如果上下文使过早失败变得笨拙或复杂,则可以进行权衡以将检查推迟到以后,但是快速失败应该是默认设置。


因此,总而言之,您是说要尽早验证对象/原始类型中可能包含的所有内容是否合理?像unsigned@NonNull等等
skiwi

2
@skiwi差不多,是的。域检查,空检查等。我不主张在其中做更多的事情:构建器通常是简单的东西。
JvR 2014年

1
可能值得注意的是,如果一个参数的有效性取决于另一个参数的值,则只有当一个参数知道另一个参数是“真正”建立的,一个参数才可以合法地拒绝该参数值。如果它是允许的一个参数值多次设定[并优先的最后设置],然后在某些情况下建立一个物体的最自然的方式可以是给定参数X的值,该值是无效的给定的当前值Y,但在调用之前,请先将build()set设置YX有效的值。
2014年

例如,如果一个人正在构建一个Shape,并且构建器具有WithLeftWithRight属性,并且希望调整一个构建器以在另一个位置构建一个对象,则要求WithRight在向右移动对象以及WithLeft向左移动对象时首先调用它,这会增加不必要的复杂性与允许WithLeft将左边缘设置为旧右边缘的右侧相比,前提是WithRightbuild调用之前固定了右边缘。
supercat

0

基本规则是“早期失效”。

稍微高级一点的规则是“尽早失败”。

如果属性本质上是无效的...

CarBuilder.numberOfWheels( -1 ). ...  

...然后您立即拒绝。

其他情况可能需要组合检查值,并且最好将其放置在build()方法中:

CarBuilder.numberOfWheels( 0 ).type( 'Hovercraft' ). ...  
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.