如何在团队中促进使用Builder模式?


22

我们的代码库是新老程序员,像我自己一样,为了统一起见,他们很快就学会了按照做事的方式来做。考虑到我们必须从某个地方开始,我自己决定重构一个数据持有者类,如下所示:

  • 删除了setter方法,并设置了所有字段final(我final理所当然地认为“ 好”)。事实证明,该二传手仅在构造函数中使用,因此没有副作用。
  • 引入了一个Builder类

Builder类是必需的,因为构造函数(首先是提示重构的原因)跨越大约3行代码。它有很多参数。

幸运的是,我的一个团队的成员正在另一个模块上工作,并且碰巧需要设置者,因为他需要的值在流程的不同点可用。因此,代码如下所示:

public void foo(Bar bar){
    //do stuff
    bar.setA(stuff);
    //do more stuff
    bar.setB(moreStuff);
}

我认为他应该改用构建器,因为摆脱设置器可以使字段保持不变(他们之前听说过关于不变性的内容),并且还因为构建器允许对象创建是事务性的。我草绘了以下伪代码:

public void foo(Bar bar){
    try{
        bar.setA(a);
        //enter exception-throwing stuff
        bar.setB(b);
    }catch(){}
}

如果引发该异常,bar将有损坏的数据,而使用构建器可以避免:

public Bar foo(){
        Builder builder=new Builder();
    try{
        builder.setA(a);
        //dangerous stuff;
        builder.setB(b);
        //more dangerous stuff
        builder.setC(c);
        return builder.build();
    }catch(){}
    return null;
}

我的队友反驳说,有问题的异常永远不会触发,对于特定的代码区域来说这是公平的,但是我相信缺少树的森林。

折衷方案是恢复到旧的解决方案,即使用无参数的构造函数,并根据需要使用setter设置所有内容。理由是该解决方案遵循了我的违反的KISS原则。

我是这家公司的新手(不到6个月),并且完全意识到我失去了这家公司。我的问题是:

  • 是否还有另一个争论来使用Builders而不是“旧方法”?
  • 我提出的变更真的值得吗?

但是真的

  • 在提倡尝试新事物时,您有什么技巧可以更好地表达这些论点?

6
构建器将需要以某种方式设置值。因此,它不能解决基本问题。
艾米·布兰肯希

3
为什么在致电之前无法将(更多/危险/抛出异常)的东西移至何处setA
yatima2975 '16

2
只有 3行构造函数参数?那还不错。
pjc50 '16

7
在任何软件设计中,总是在复杂性和去耦之间进行权衡。尽管我个人认为构建器是一个很好的解决方案,但您的同事还是可以基于KISS进行犹豫。我维护的软件过于复杂,部分原因是因为每位新员工都流氓说:“这很愚蠢,我将以自己的方式来做这件事”,因此我们有5个用于计划的框架后台作业和8种查询数据库的方法。一切都要适度,在这种情况下没有明确的正确答案。
布兰登

3
KISS是一个模糊的概念,可修改的对象是开发人员大大低估的复杂性源,它们使多线程,调试和异常处理比不可变的对象困难得多。该对象是有效的,还是在构造中创建了一个异常(捕获该异常的更好位置?)。
Alessandro Teruzzi '16

Answers:


46

考虑到我们必须从某个地方开始,我自己决定重构一个数据持有者类

这是错误的地方-您对代码库进行了重大的体系结构更改,以使其与预期的变得截然不同。您使您的同事感到惊讶。惊喜只有在参加聚会时才是好的,最少惊喜的原则是黄金(即使涉及聚会,我也会穿干净的内裤!)

现在,如果您认为此更改值得,那么最好草绘出显示该更改的伪代码,并将其呈现给您的同事(或至少是角色最接近架构师的人),然后在做任何事情之前先看看他们的想法。实际上,您很无赖,以为整个代码库都是您认为合适的。团队成员,而不是团队成员!

我可能对您有点苛刻,但我却处在相反的位置:检查一些代码并认为“ WTF发生在这里?!”,才发现新人(几乎总是新人)决定改变根据他认为的“正确方法”应该做的事情。还要与SCM历史保持一致,这是另一个主要问题。


7
“只有在参加聚会的情况下,惊喜才是好事,”实际上并不是每个人都如此!我停止谈论任何所谓的朋友说,给我一个惊奇什么,特别是一个。与同在。啊。
Darkhogg '16

3
因此,如果每个重构和设计模式,或每个决定,如“我在这里使用存取器和mutator还是构建器模式?” 必须获得团队的批准,开发人员将如何感到有能力做正确的事情?如果一切必须由委员会设计,您将如何进行具有前瞻性的更改。我一直在OP任职,在此之前,我不得不维护毫无意义的代码(作为新手),而其他人则害怕更改它,因为它是一致的。一致性固然良好,但并非以改善为代价。
布兰登

11
@Brandon不会,但是会影响他们的每一个广泛的变化都应该。如果您必须修复错误,则只需编写少量代码-没什么大不了的。如果您做出的决定影响到其他所有人,那么至少要有礼貌地告诉他们您在做什么。您可以征得团队的同意,以更改您毫无意义的代码,然后您所做的更改将成为新的一致性。您只需要与您的同事交谈。说“我将改变大家都讨厌的糟糕的访问者模式”并听到您的同事回答“是的,大约有人来解决这个问题”没什么不好
gbjbaanb

13
@布兰登有一句名言:“通向善意的道路通往地狱”。一致性不仅是好的,而且是必要的!想象一下,试图管理一个由20个应用程序共同开发的开发团队,每个应用程序以不同的样式和/或体系结构编写,因为偏好,当前技术,趋势等决定了当时的最佳状态。让团队同意应该改变的事情,可能是新来的人因为他们解决了一个长期存在的问题而被接受,以及因为以您认为最好的方式变得更加勇敢而被解雇。
DrewJordan '16

3
@Brandon-如果您要流氓去解决“每个人都同意是错误的”事情,那么您可能会通过选择“每个人都同意是错误的事情”而不是选择充其量只是具有不同阵营的事情而获得更大的成功。话题;如不变性 好的设计/架构会使不变性的成本很高,而几乎没有回报。因此,除非您的团队因为使用二传手而出现问题,否则您根本不会解决问题。OTOH,如果这是经常发生错误的来源,那么分枝似乎可以证明团队的决定是合理的。
Dunk

21

幸运的是,我的一个团队的成员正在另一个模块上工作,并且碰巧需要设置者,因为他需要的值在流程的不同点可用。

如前所述,我看不出是什么使代码需要设置程序。我可能对用例不太了解,但是为什么不等到实例化对象之前知道所有参数呢?

作为替代方案,如果情况需要使用setter:提供一种冻结代码的方法,如果尝试在对象准备好后修改字段,则setter会引发断言错误(aka程序员错误)。

我认为除去二传手并使用final是做到这一点以及加强不变性的“适当”方法,但是,这真的是您应该花费的时间吗?

一般提示

老=坏,新=好的,我的方式,您的方式...这种讨论不是建设性的。

当前,对象的使用方式遵循一些隐式规则。这是您未编写代码的规范的一部分,您的团队可能认为代码的其他部分滥用它的风险不大。您要做的是通过Builder和编译时检查使规则更加明确。

在某种程度上,代码重构与“ 残破的窗口”理论联系在一起:您认为正确解决任何问题是正确的(或为以后的“质量改进”会议做笔记),以便使代码始终看起来令人愉快,安全干净。但是,您和您的团队对“足够好”有不同的想法。您认为与真诚的团队一起做出贡献,使代码更好的机会可能与现有团队有所不同。

顺便说一句,你的球队不应该被假定为从惯性,不良的生活习惯进行一定的痛苦,缺乏教育,...你可以做的最糟糕的是投射的图像无所不知的一切谁在那里向他们展示如何编码。例如,不变性并不是什么新鲜事物,您的同龄人可能已经读过它,而仅仅是因为这个话题在博客中变得越来越流行,还不足以说服他们花时间更改其代码。

毕竟,有无数种方法来改进现有的代码库,但是却要花费时间和金钱。我可以看一下您的代码,将其称为“旧的”,然后找一些事情进行讨论。例如:没有正式的规范,没有足够的断言,也许没有足够的注释或测试,没有足够的代码覆盖率,算法不理想,内存使用过多,在每种情况下均未使用“最佳”可能的设计模式,等等。ad nauseam。但是对于大多数我想提的要点,您会认为:没关系,我的代码有效,无需花更多时间在上面。

也许您的团队认为您的建议已经超出了收益递减的范围。即使由于示例的原因而发现了一个错误的实际实例(例外),他们也可能只修复了一次并且不希望重构代码。

因此,问题是鉴于他们有限,该如何度过您的时间和精力?软件会不断进行更改,但是通常您的想法会以负数开始,直到您可以为其提供良好的论据为止。即使您对收益是正确的,但这本身也不是一个充分的论据,因为它将在“ 有一天,很高兴…… ”篮子中结束。

在提出论据时,您必须进行一些“理智”检查:您是要出售自己的想法还是自己?如果其他人向团队提出了该怎么办?您会在他们的推理中发现一些缺陷吗?您是要应用一些通用的最佳实践还是要考虑代码的细节?

然后,如果要解决团队过去的“错误”,请确保不会出现。它有助于表明您不是您的主意,但可以合理地获得反馈。您做得越多,您的建议就越有机会被遵循。


你是绝对正确的。一个小异议:这不是“旧方法”与“我的新方法”。我完全知道我可能在胡说八道。我的问题是,通过继续使用公司中每个人都感到恐惧的软件实践,我可能会停滞不前成为开发人员。因此,即使我错了,我也会尝试进行一些更改。(该任务是由重构相关类的请求触发的)
rath 2016年

@rath是否正在开发新的代码库?
biziclop '16

@biziclop在旧的代码库中插入了新模块。我最近做了一个。快乐的时光。
rath 2016年

5
@rath也许您需要一个个人项目,可以在自己的时间从事自己的项目,从而可以促进自己的发展?我也不相信您的“ Builder”模式论点;根据您所提供的信息,getter / setter方法似乎是一个完美的解决方案。请记住,您对雇主和团队负责;您的首要任务是确保您所做的任何工作都对您负责的人有利。
Ben Cottrell

@rath好吧,即使您没有使用“旧的/新的”代码,重要的是接收端如何感知它,尤其是当它们具有比您更大的决策能力时。如果您想为自己的利益而不是为您开发的产品引入更改,那看起来就好像。如果公司中的每个人都觉得这种做法可怕,那么它最终将改变,但这似乎不是优先事项。
coredump

17

如何在团队中促进使用Builder模式?

你不知道

如果您的构造函数具有3行参数,则它太大且太复杂。没有设计模式可以解决此问题。

如果您有一个其数据在不同时间可用的类,则它应该是两个不同的对象,或者您的使用者应聚合这些组件,然后构造该对象。他们不需要建造者来做到这一点。


它是Strings和其他原语的大数据持有人。任何地方都没有逻辑,甚至不检查nulls等。因此,它的大小仅仅是其本身,而不是复杂性。我认为做这样的事情builder.addA(a).addB(b); /*...*/ return builder.addC(c).build();在眼睛上会容易得多。
rath 2016年

8
@rath:它是Strings和其他原语的大数据持有人。=>然后您还有其他问题,希望没人关心电子邮件意外字段是否分配有该人的邮寄地址...
Matthieu

@MatthieuM。我想一次遇到一个问题:)
rath 2016年

@rath-与试图弄清楚如何将构建器模式混入已经存在的代码中相比,合并一个好的验证检查方案似乎更加有用和容易接受。您曾在其他地方说过要改善自己。学习如何以最小的努力和后果获得最大收益绝对是应该发展的属性。
Dunk

1
我有很多构造函数,它们的参数比这更多。IoC模式中必需的。这个答案不好概括。
djechlin '16

16

您似乎更注重做对而不是与他人合作。更糟糕的是,您意识到自己正在做的是一场权力斗争,却没有考虑到经历和权威受到挑战的人们可能会感觉如何。

反过来。

专注于与他人合作。将您的想法整理为建议和想法。在提出问题之前让他们讨论他们的想法,以使他们思考您的想法。避免直接对抗,并外在愿意接受与小组进行艰难的战斗来改变小组相比,继续做小组的事情(目前而言)更具生产力。(尽管把事情带回去。)使与您一起工作变得愉快。

始终如一地这样做,应该减少冲突,并发现更多的想法被他人采纳。我们所有人都遭受这样的幻想:完美的逻辑论证会让别人赞叹并赢得胜利。而且,如果这种方式行不通,我们认为应该

但这与现实直接冲突。在提出第一个逻辑要点之前,大多数参数都会丢失。即使在所谓的逻辑人物中,心理学也胜过理性。说服人们的目的在于克服心理障碍,使其得到正确的聆听,而不是拥有完美的逻辑。


2
Frame your thoughts as suggestions and ideas. Get THEM to talk through THEIR ideas before asking questions to make them think about yours尽管如此,还是+1
拉斯

2
@rath请记住,其他人可能不会从您的书中阅读。您正在讨论的人可能会将其视为对抗,这可能会适得其反。
Iker,2016年

2
@rath:没有对与错。权衡取舍。而且,折衷往往不仅仅涉及代码。
Marjan Venema

1
@Dunk存在的一切都是权衡。当权衡非常明确并且显然在一方面时,我们称它们为对还是错。但是,如果我们专注于从一开始就进行权衡,那么识别何时变得模糊起来就容易了。
btilly '16

2
我知道没有一个编程“最佳实践”,在最佳实践中也没有例外。有时很难找到这些例外,但它们始终存在。
btilly 2016年

11

是否还有另一个争论来使用Builders而不是“旧方法”?

“当您有了新的锤子时,一切都会看起来像钉子。” -这就是我读到您的问题时的想法。

如果我是你的同事,我会问你“为什么不在构造函数中初始化整个对象?” 毕竟这就是功能。为什么要使用构建器(或任何其他设计)模式?

我提出的变更真的值得吗?

从那个小的代码片段很难分辨出来。也许是-您和您的同事需要对其进行评估。

在提倡尝试新事物时,您有什么技巧可以更好地表达这些论点?

是的,在讨论该主题之前,请详细了解该主题。在进行讨论之前,请以良好的论据和理由武装自己。


7

我当时因技能而应聘。当我必须查看代码时,..那就太可怕了。然后尝试清理它时,去过那里的人总是抑制更改,因为他们看不到好处。听起来像您正在描述的东西。最终我辞去了该职位,现在我可以在一家拥有更好实践的公司中发展得更好。

我认为您不应该与团队中的其他人一起发挥作用,因为他们在那里待了更长的时间。如果您看到您的团队成员在您所从事的项目中使用了不良做法,则必须像现在一样指出imo。如果没有,那么您就不是非常宝贵的资源。如果确实要一遍又一遍地指出这些问题,但没有人在听,请向您的管理层要求更换或更换工作。非常重要的一点是,不要让自己适应不良做法。

对实际问题

为什么要使用不可变的对象?因为您知道您要处理的内容。

假设您传递了一个数据库连接,该数据库连接允许您通过设置器更改主机,那么您不知道它是什么连接。由于是一成不变的,所以您知道。

但是,假设您具有一个可以从持久性中检索然后再使用新数据重新持久化的数据结构,则可以认为该结构不是不变的。它是同一实体,但是值可以更改。而是使用不可变值对象作为参数。

但是,您要重点关注的问题是一种构建器模式,以摆脱构造器中的依赖关系。我对此表示不满。实例显然已经很复杂了。不要试图隐藏它,解决它!

l

删除安装员可能是正确的,具体取决于类别。构建器模式不能解决问题,而只是将其隐藏。(即使您看不见,地毯下的污垢仍然存在)


我不知道您的情况的详细信息,但是如果您进来尝试改变一切,而没有先获得人们对您技能的信心,或者不花时间学习“为什么”他们以他们的方式做事,那么您会受到完全的对待因为几乎任何一家公司,即使是非常好的公司,都会受到对待。实际上,尤其是在非常好的情况下。一旦人们对某人的技能有了信心,那么就可以为您的建议提供令人信服的理由。如果您无法提出令人信服的理由,那么您的建议很有可能不是那么好。
Dunk

1
idk如何回应您的假设@Dunk我不是要更改所有内容,而是尝试清理它。并不是人们知道他们在做什么,他们自豪地创建了类和函数,这些类和函数在几百行代码中,用idk表示多少个状态,全局等。所以我支持我的发言。永不退缩,当你发现在一个位置,你将不得不采取以不好的做法,以适应你的自我之中。
超级英雄

@Dunk尝试改变人们作为团队成员的编码方式不是我要做的。但是我从一开始就告诉他们。如果代码确实是一个垃圾,那么“我不会完全重写它而不会在这个项目中工作”。如果不适当的话,那是相互的。这不仅是一种安慰,还在于我需要对自己生产的产品承担责任。如果项目中的周围代码完全混乱,我将无法做到这一点。
超级英雄

您不必“永远”使用垃圾代码或遵循不良做法,而不必仅仅因为这就是这样做的方式。但是,如果您想成功地进行改变,那么最好至少先证明自己的信誉。几乎每个开发人员都认为他们继承的代码是垃圾。如果每个公司每次都只是与新人达成协议,甚至不知道他们的真正技能,那么垃圾就会变成垃圾。另外,您应该花时间了解为什么要按原样进行。有时,“错误”方式是特定情况下的“正确”方式。
Dunk

@Dunk我发现您在这件事上与我有不同的看法。如果从其他答案中读取,您的似乎更受欢迎。我不同意,为什么我对此表示反对。你所说的话并没有改变我的想法。
超级英雄

2

尽管所有其他答案都非常有用(比我的要有用的多),但是还有一个您尚未提到的构建器的属性:通过将对象的创建转换为流程,您可以强制执行复杂的逻辑约束。

例如,您可以要求将某些字段设置在一起或以特定的顺序设置。您甚至可以根据这些决定返回目标对象的不同实现。

// business objects

interface CharRange {
   public char getCharacter();
   public int getFrom();
   public int getTo();
}

class MultiCharRange implements CharRange {
   final char ch;
   final int from;
   final int to;

   public char getCharacter() { return ch; }
   public int getFrom() { return from; }
   public int getTo() { return to; }
}

class SingleCharRange implements CharRange {
   final char ch;
   final int position;

   public char getCharacter() { return ch; }
   public int getFrom() { return position; }
   public int getTo() { return position; }
}

// builders

class Builder1 {
   public Builder2 character(char ch) {
      return new Builder2(ch);
   }
}

class Builder2 {
   final char ch;
   int from;
   int to;

   public Builder2 range(int from, int to) {
      if (from > to) throw new IllegalArgumentException("from > to");
      this.from = from;
      this.to = to;
      return this;
   }

   public Builder2 zeroStartRange(int to) {
      this.from = 0;
      this.to = to;
      return this;
   }

   public CharRange build() {
      if (from == to)
         return new SingleCharRange(ch,from);
      else 
         return new MultiCharRange(ch,from,to);
   }
}

这是一个毫无意义的示例,但它演示了所有三个属性:始终始终设置字符,始终将范围限制设置并验证,并且在构建时选择实现。

很容易看出这如何导致更易读和可靠的用户代码,但是如您所见,它还有一个缺点:很多构建器代码(我什至省去了构造器以缩短代码片段)。因此,您尝试通过询问以下问题来计算权衡:

  • 我们仅从一两个地方实例化对象吗?这有可能改变吗?
  • 也许我们应该通过将原始对象重构为较小对象的组合来实施这些约束?
  • 我们最终会在方法之间传递构建器吗?
  • 我们可以改用静态工厂方法吗?
  • 这是外部API的一部分吗,需要尽可能地做到万无一失?
  • 如果有构建器,大约可以避免我们当前积压的bug数量?
  • 我们将节省多少额外的文档编写工作?

您的同事只是认为权衡是没有好处的。您应该公开和诚实地讨论原因,最好不要诉诸抽象原则,而应专注于此解决方案的实际含义。


1
通过将对象的创建转变为流程,可以强制执行复杂的逻辑约束。 BAM。当将其组织为更小,离散,更简单的步骤时,这意味着复杂性。
radbobob

2

听起来这个类实际上是一个以上的类,并放在同一文件/类定义中。如果它是DTO,则应该可以一次性实例化它并且使其不可变。如果您在任何一项交易中都不使用所有字段/属性,那么几乎可以肯定它会打破“单一责任”原则。可能是将其分解为更小,更紧密,不可变的DTO类可能会有所帮助。如果您还没有读干净代码,请考虑阅读它,以便更好地阐明问题以及进行更改的好处。

我认为您不能按委员会在此级别上设计代码,除非您确实是暴民编程(这听起来不像您属于那种团队),所以我认为可以根据您的要求进行更改最好的判断,如果事实证明您判断不对,就把它放在下巴上。

但是,只要您能够按原样阐明代码的潜在问题,那么即使您的解决方案不是公认的解决方案,您仍然可以争论是否需要解决方案。然后人们可以自由地提供替代方案。

强硬见解,软弱无力 ”是一个好原则-您可以根据自己的知识来自信地前进,但是如果您发现一些与之相反的新信息,则应谦虚而退缩,并就正确的解决方案进行讨论。是。唯一的错误答案是让它变得更糟。


绝对不是委员会设计的,这是代码太糟糕的部分原因。我想太好了。它 DTO,但我不会将其分解成碎片。如果代码库不好,那么数据库就差很多。+1(主要是最后一段)。
rath 2016年
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.