为什么将类型与生成器结合在一起?


20

我最近在Code Review上删除了我的一个答案,它的开始是这样的:

private Person(PersonBuilder builder) {

停止。红色标志。一个PersonBuilder将建立一个Person;它知道一个人。Person类应该对PersonBuilder一无所知-这只是一个不可变的类型。您已经在此处创建了圆形耦合,其中A取决于B,而B取决于A。

该人员应仅获取其参数;愿意创建一个人而不创建它的客户应该能够做到这一点。

我被选票打了耳光,并告诉我(引用)红旗,为什么?这里的实现与Joshua Bloch在其“ Effective Java”书(项目2)中演示的形状相同。

因此,看来在Java 中实现构建器模式的一种正确方法是使构建器成为嵌套类型(尽管这不是这个问题),然后制造产品(正在构建的对象的类) )对构建器的依赖,如下所示:

private StreetMap(Builder builder) {
    // Required parameters
    origin      = builder.origin;
    destination = builder.destination;

    // Optional parameters
    waterColor         = builder.waterColor;
    landColor          = builder.landColor;
    highTrafficColor   = builder.highTrafficColor;
    mediumTrafficColor = builder.mediumTrafficColor;
    lowTrafficColor    = builder.lowTrafficColor;
}

https://zh.wikipedia.org/wiki/Builder_pattern#Java_example

对于相同的Builder模式,相同的Wikipedia页面对于C#具有非常不同的实现(并且更加灵活):

//Represents a product created by the builder
public class Car
{
    public Car()
    {
    }

    public int Wheels { get; set; }

    public string Colour { get; set; }
}

如您所见,此处的产品Builder类一无所知,并且它关心的所有事情都可以通过直接的构造函数调用,抽象工厂或构建器实例化。据我所知,产品创建模式的永远需要知道什么是创造任何东西。

我一直在接受反对论证(显然,在Bloch的书中明确辩护),可以使用构建器模式来返工一种类型,该类型会使构造函数with肿有许多可选参数。因此,与其坚持我的想法,不如我知道我在该站点上进行了一些研究,发现我怀疑,这种说法没有道理

那怎么办?为什么要针对一开始就不应该存在的问题提出过度设计的解决方案?如果我们让约书亚·布洛赫(Joshua Bloch)离开他的基座一分钟,我们能否提出一个单一的,有效的理由来结合两种具体类型并将其称为最佳实践?

这一切对我来说都是崇尚货色的编程


12
而这个“质疑”的表情只是being之以鼻……
菲利普·肯德尔

2
@PhilipKendall可能有点。但是我真的很想了解为什么每个Java构建器实现都有这种紧密的耦合。
Mathieu Guindon '16

19
@PhilipKendall对我的好奇心。
2016年

8
@PhilipKendall它的读起来像是一声,是的,但其核心是一个有效的主题问题。也许可以删除rant了?
Andres F.

我对“构造函数参数过多”的参数没有印象。但是,这两个构建器示例给我留下的印象都更少。也许是因为这些示例太简单了,无法显示非平凡的行为,但是Java示例的读取方式却像复杂的流利接口一样,而C#示例只是实现属性的一种about回方式。这两个示例均未进行任何形式的参数验证。构造函数至少会验证提供的参数的类型和数量,这意味着参数过多的构造函数将获胜。
罗伯特·哈维

Answers:


22

我不同意你的说法,这是一个危险信号。我也不同意您对对方的表示,即表示构建器模式的一种正确方法。

对我来说,有一个问题:生成器是否是类型API的必要部分?这里,PersonBuilder是Person API的必要部分吗?

完全有必要通过其提供的构建器来创建经过有效性检查,不可变和/或紧密封装的Person类(无论该构建器是嵌套的还是相邻的)。这样,“人员”可以保留其所有字段的私有性或“包私有性”以及“ final”,并且如果正在进行中的工作,还可以让该类开放供修改。(它可能最终因修改而关闭,而对于扩展却开放,但是现在这并不重要,对于不可变的数据对象尤其有争议。)

如果确实必须通过提供的PersonBuilder作为单个程序包级API设计的一部分来创建Person,则采用圆形耦合就可以了,此处不作任何保证。值得注意的是,您说这是一个无可争辩的事实,而不是API设计的观点或选择,因此可能导致不良响应。我不会拒绝您的投票,但我不会因此而怪罪他人,我将其余的“什么理由拒绝投票”的讨论留给了Code Review帮助中心meta。投票失败;这不是“巴掌”。

当然,如果您想开放很多方法来构建Person对象,那么PersonBuilder可以成为使用者实用程序不应该直接依赖的Person类。这个选择似乎更灵活-谁不想有更多的对象创建选项?-但是为了保持保证(例如不变性或可序列化性),Person突然不得不公开另一种公共创建技术,例如很长的构造函数。如果Builder是用来表示或替换具有很多可选字段的构造函数或开放修改的构造函数,则该类的long构造函数可能是一个实现细节,最好将其隐藏起来。(还请记住,一个正式且必要的Builder并不能阻止您编写自己的构造实用程序,只是您的构造实用程序可能像在我上面的原始问题中一样,将Builder用作API)。


ps:我注意到您列出的代码审查示例具有不变的Person,但是Wikipedia的反例列出了带有getter和setter的可变Car。如果您使语言和不变式保持一致,则可能更容易看到Car省略了必要的机械。


我想我了解您关于将构造函数视为实现细节的观点。当时看来,似乎不是有效的Java正确传达了这一信息(我没有/没有读过这本书),留下了许多初级(?)Java开发人员,他们假设“这是怎么做的”,而不管其常识如何。 。我同意Wikipedia的示例不适合进行比较。.但是,嘿,它是Wikipedia ..和FWIW,我实际上并不在乎那些低俗的票。无论如何,被删除的答案的净得分都为正(我有机会最终得分[徽章:对等压力])。
Mathieu Guindon '16

1
但是,我似乎无法摆脱这样一个想法,即带有太多构造函数参数的类型是设计问题(打破SRP的气味),即构造器模式迅速推到地毯下。
Mathieu Guindon '16

3
@ Mat'sMug我的上一篇文章认为该类需要那么多构造函数参数。如果我们在谈论任意业务逻辑组件,我同样会将其视为设计气味,但是对于数据对象,具有十个或二十个直接属性的类型不是问题。例如,对象代表日志中单个强类型记录并不违反SRP 。
Jeff Bowman

1
很公平。String(StringBuilder)不过,我仍然没有得到构造函数,它似乎服从于“原则”,在这种情况下,类型依赖于其构造函数是正常的。当我看到构造函数重载时,我感到非常惊讶。它不像String具有42个构造函数参数...
Mathieu Guindon

3
@卢安·奥德 从字面上看,我从未见过它,也不知道它的存在。toString()是普遍的。
克莱里斯(罢工)-16年

7

我理解在辩论中使用“呼吁权威”会带来多大的烦恼。争论应该站在他们自己的IMO上,尽管指出这样一个受人尊敬的人的建议是没有错的,但实际上它本身不能被认为是一个完整的争论,因为我们知道太阳绕着地球行进,因为亚里斯多德说过。

话虽如此,我认为你的论点前提是一样的。我们绝不应该将两种具体类型耦合在一起,并称其为最佳实践,因为……有人这样说?

为了使耦合有问题的论点成立,必须要解决一些特定的问题。如果这种方法不受这些问题的影响,那么您就无法从逻辑上论证这种耦合形式存在问题。因此,如果您想在这里提出自己的观点,我认为您需要说明此方法如何解决这些问题和/或取消构建器的耦合将如何改善设计。

副手,我正在努力查看耦合方法将如何产生问题。当然,这些类是完全耦合的,但是生成器仅用于创建类的实例。另一种看待它的方法是作为构造函数的扩展。


那么...更高的耦合,更低的内聚=>圆满的结局?嗯...我看到了有关构建器“扩展构造函数”的要点,但是举另一个例子,我看不到String构造函数重载StringBuilder参数是做什么的(尽管a是否StringBuilder构建器尚有争议,但这是另一个故事)。
Mathieu Guindon '16

4
@ Mat'sMug需要明确的是,我对这两种方式都不抱有强烈的感觉。我不倾向于使用生成器模式,因为我并没有真正创建具有很多状态输入的类。由于您提到其他地方的原因,我通常会分解此类。我只想说的是,如果您可以证明这种方法是如何导致问题的,那么上帝降下并将建造者的模式交给石碑上的摩西并没有关系。你赢了'。另一方面,如果您不能...
JimmyJames

我在自己的代码库中使用的构建器模式的一种合法用途是模拟VBIDE API。基本上,您提供了代码模块的字符串内容,而构建器为您提供了一个VBE模拟程序,包括窗口,活动代码窗格,项目和带有包含您提供的字符串的模块的组件。没有它,我不知道如何能够在Rubberduck中编写80%的测试,至少每次我看着它们时都不会刺伤自己。
Mathieu Guindon '16

@ Mat'sMug对于将数据解组为不可变对象非常有用。这些对象往往是属性包,即不是真正的OO。问题的整个领域是如此的PITA,无论他们是否遵循良好实践,任何有助于完成工作的东西都会被利用。
JimmyJames

4

要回答为什么将类型与构建器结合使用,有必要了解为什么任何人首先都会使用构建器。特别是:当您具有大量构造函数参数时,Bloch建议使用构建器。这不是使用生成器的唯一原因,但我推测这是最常见的。简而言之,构建器是那些构造器参数的替代品,并且通常在与构建器相同的类中声明该构建器。因此,封闭的类已经了解了构建器,并且将其作为构造器参数传递并不会改变它。这就是为什么将类型与生成器结合在一起的原因。

为什么拥有大量参数意味着您应该使用构建器?嗯,拥有大量参数在现实世界中并不罕见,也不违反单一责任原则。当您有十个String参数并试图记住哪些是哪些以及是否应该为空的第五个或第六个String时,它也很糟糕。使用构建器可以更轻松地管理参数,还可以执行其他一些很酷的事情,您应该阅读本书以了解有关内容。

什么时候使用与它的类型不匹配的构建器?通常,当对象的组件无法立即使用并且具有这些组件的对象不需要了解您要构建的类型时,或者您为无法控制的类添加构建器时, 。


奇怪的是,我唯一需要构建器的时候是当我拥有一个没有确定形状的类型时,例如MockVbeBuilder,它构建了一个IDE API的模型。一个测试可能只需要一个简单的代码模块,另一个测试可能需要两种形式和一个类模块-IMO ,这就是GoF的构建器模式的用途。就是说,我可能会开始用另一个带有疯狂构造函数的类来使用它……但是这种耦合根本感觉不到。
Mathieu Guindon '16

1
@Mat的杯子您提供的类似乎更能代表Builder设计模式(来自Gamma等人的Design Patterns),不一定是简单的builder类。他们俩都造物,但他们不是一回事。另外,您从不“需要”构建器,它只是使其他事情变得更容易。
老发内德

1

创建模式的产品永远不需要知道创建它的内容。

Person类不知道什么是创造它。它只有一个带有参数的构造函数,那又如何呢?参数类型的名称以... Builder结尾,它实际上并没有强制执行任何操作。毕竟这只是一个名字。

构建器是一个出色的1配置对象。 配置对象实际上只是将属于彼此的属性分组在一起。如果它们只是为了传递给类的构造函数而属于彼此,那就这样吧!二者origindestination所述的StreetMap例子是类型的Point。是否可以将每个点的各个坐标传递到地图?当然可以,但是a的属性Point属于同一类,另外,您可以在其上使用各种有助于其构造的方法:

1区别在于,构建器不仅是纯数据对象,而且允许这些链接的setter调用。Builder主要关心如何构建自身。

现在让我们添加一些命令模式,因为如果仔细观察,构建器实际上就是一个命令:

  1. 它封装了一个动作:调用另一个类的方法
  2. 它包含执行该操作所需的信息:方法参数的值

对于构建者而言,特殊的部分是:

  1. 该要调用的方法实际上是一个类的构造函数。现在最好将其称为创造模式
  2. 参数的值是构建器本身

传递给Person构造函数的内容可以构建它,但不一定必须如此。它可能是普通的旧配置对象或构建器。


0

不,他们完全错了,而您绝对正确。该构建器模型只是愚蠢的。构造函数参数的来源与Person无关。构建器只是为用户带来便利,仅此而已。


4
谢谢...我敢肯定,如果这个答案扩大一点,它将得到更多的选票,似乎还没有完成=)
Mathieu Guindon

是的,我想为建造商提供+1的另一种方法,该方法可以提供相同的保障和保证,而无需耦合
暗淡的怪胎

3
为了解决这个问题,请参阅已接受的答案,其中提到PersonBuilder了实际上是PersonAPI 必不可少的部分的情况。就目前的情况而言,这个答案并没有证明它的理由。
Andres F.
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.