我的团队如何在重构后避免频繁的错误?


20

为您提供一些背景知识:我在一家大约有十二名Ruby on Rails开发人员(+/-实习生)的公司工作。远程工作很普遍。我们的产品由两部分组成:一个相当肥大的核心,然后精简到以此为基础的大客户项目。客户项目通常会扩展核心。不会覆盖关键功能。我可能还会补充说,核心中有一些非常糟糕的部分,这些部分迫切需要重构。有规格,但主要针对客户项目。核心的最差部分未经测试(不是应该的……)。

开发人员分为两个团队,每个sprint使用一个或两个PO。通常,一个客户项目严格与团队和PO之一相关联。

现在我们的问题是:我们经常破坏彼此的东西。A团队的某人扩展或重构了核心功能Y,从而为B团队的一个客户项目造成了意外错误。通常,更改不会在团队中宣布,因此这些错误几乎总是无法预料的。包括PO在内的B团队认为功能Y是稳定的,并且在发布之前未对其进行测试,并且没有意识到更改。

如何摆脱那些问题?您可以推荐我什么样的“公告技术”?


34
显而易见的答案是TDD
mouviciel 2014年

1
您怎么说“不会覆盖关键功能”,然后您的问题是确实发生了?您是否在团队中将“核心”和“关键功能”区分开来,如何做到这一点?只是想了解情况...
logc

4
@mouvciel,并且不要使用动态类型,但是在这种情况下,特别的建议来得太晚了。
Doval 2014年

3
使用像OCaml这样的强类型语言。
Gaius 2014年

@logc可能我不清楚,对不起。我们不会覆盖过滤器库本身之类的核心功能,而是向我们在客户项目中使用的类添加新的过滤器。一种常见的情况是,过滤器库中的更改会破坏客户项目中添加的那些过滤器。
SDD64 2014年

Answers:


24

我建议阅读Michael C. Feathers撰写的《有效使用旧版代码》。它说明您确实需要自动测试,如何轻松添加它们,如果还没有自动测试,以及哪种“代码味道”以何种方式重构。

除此之外,您遇到的另一个核心问题似乎是两个团队之间缺乏沟通。这些团队有多大?他们在处理不同的积压工作吗?

根据您的体系结构拆分团队几乎总是​​一种不好的做法。例如核心团队和非核心团队。相反,我将在功能域(但跨组件)上创建团队。


我在“神话中的一个月”中已经读到,代码结构通常遵循团队/组织结构。因此,这并不是真正的“坏习惯”,而只是事情的正常进行。
Marcel 2014年

我认为在“ 软件开发动力学 ”中,Visual C ++的管理者建议生动地建立功能团队。我还没有读过@Marcel的“神话般的月”,但AFAIK列出了行业中的不良做法……
logc 2014年

马塞尔(Marcel),这的确是通常的做法,但是越来越多的团队(例如功能团队)的做法有所不同。具有基于组件的团队会导致在处理跨组件功能时缺乏沟通。紧接着,它几乎总是导致架构讨论不是基于良好架构的目的,而是人们试图将责任推给其他团队/组件。因此,您将得到该问题作者所描述的情况。另请参阅mountaingoatsoftware.com/blog/the-benefits-of-feature-teams
Tohnmeister 2014年

好吧,据我对OP的理解,他说团队没有分成核心团队和非核心团队。团队是按“每个客户”划分的,本质上是“按功能域”。这就是问题的一部分:由于允许所有团队更改公共核心,因此一个团队的更改会影响另一个团队。
布朗

@DocBrown你是对的。每个团队都可以改变核心。当然,这些更改应该对每个项目都是有益的。但是,它们处理不同的积压。我们为每个客户提供一个,为核心提供一个。
SDD64 2014年

41

我们最核心的部分未经测试(应该如此)。

这就是问题。高效的重构在很大程度上取决于自动化测试套件。如果没有这些,您所描述的问题就会开始出现。如果您使用像Ruby这样的动态语言,这尤其重要,因为该语言没有编译器来捕获与将参数传递给方法有关的基本错误。


10
并在婴儿步骤中进行重构并非常频繁地进行操作。
Stefan Billiet 2014年

1
可能有大量的建议可以在这里添加建议,但是到现在为止。无论OP如何“开玩笑”地表明他们知道这本身就是问题,脚本化测试对重构的影响是巨大的:如果通过失败,那么重构就不会起作用。如果所有通行证仍然是通行证,那么重构可能就可以了(移动不通行证显然是一个加分,但是将所有通行证保持为通行证甚至比获得净收益还重要;改变一个测试并修复五个测试的更改可能是改善,而不是重构)
乔恩·汉娜

我给您“ +1”,但我认为“自动化测试”不是解决此问题的唯一方法。更好的手动但有系统的质量检查(也许由单独的质量检查小组负责)也可以解决质量问题(同时进行自动手动测试可能很有意义)。
布朗

很好,但是如果核心和客户项目是单独的模块(而且使用像Ruby这样的动态语言),则核心可以更改测试及其关联的实现,并在不使自己的测试失败的情况下中断从属模块。
logc

正如其他人所评论。TDD。您可能已经认识到应该对尽可能多的代码进行单元测试。虽然仅出于单元测试的目的而编写单元测试是浪费资源,但是当您开始重构任何组件时,应该在接触核心代码之前先进行大量的测试编写。
jb510 2014年

5

先前的指向您指出更好的单元测试的答案是好的,但是我认为可能还有更多基本问题需要解决。您需要清晰的界面来访问客户项目代码中的核心代码。这样,如果您重构核心代码而不改变通过接口观察到的行为,那么其他团队的代码将不会中断。这将使您更容易知道可以“安全”重构的内容以及需要重新设计(可能是接口中断)的内容。


发现。更加自动化的测试只会带来好处,而且是完全值得做的,但是它无法解决核心问题,即无法传达核心变更。通过围绕重要特征包装接口来实现解耦将是一个巨大的进步。
Bob Tway 2014年

5

其他答案也强调了重点(更多的单元测试,功能团队,与核心组件的简洁接口),但是我发现有一点缺失,那就是版本控制。

如果您通过执行发行版1来冻结核心的行为,并将该发行版放入私有工件管理系统2中,则任何客户项目都可以声明其对核心版本X的依赖关系,并且下一发行版X不会破坏该依赖项。+1

然后,“公告政策”只是简化为在每个发行版中都包含一个CHANGES文件,或者召开团队会议来宣布每个新核心发行版的所有功能。

另外,我认为您需要更好地定义什么是“核心”,以及哪些是“关键”。您似乎(正确地)避免对“关键组件”进行许多更改,但允许对“核心”进行频繁更改。为了依靠某些东西,您必须保持它稳定。如果某些东西不稳定,请不要将其称为核心。也许我建议将其称为“帮助”组件?

编辑:如果您遵循语义版本控制系统中的约定,则内核API中任何不兼容的更改都必须由主要版本更改来标记。也就是说,当您更改以前存在的内核的行为或删除某些内容时,不仅要添加新内容。按照这种约定,开发人员知道从版本'1.1'升级到'1.2'是安全的,但是从'1.X'升级到'2.0'是冒险的,必须仔细检查。

1:在Ruby的世界中,我认为这被称为宝石
2:等效于Java中的Nexus或Python中的PyPI


“版本控制”确实很重要,但是当人们试图通过在发布之前冻结内核来解决上述问题时,您很容易最终需要复杂的分支和合并。原因是,在团队A的“发布发布”阶段中,A可能不得不更改核心(至少是为了修正错误),但不会接受其他团队对核心的更改-因此您最终只能获得一个分支每个团队的核心,被“合并”,这是技术债务的一种形式。有时可以,但通常只是将描述的问题推迟到以后的某个时间点。
布朗

@DocBrown:我同意你的观点,但是我在假设所有开发人员都是合作且成长的前提下写的。这并不是说我没有看到您的描述。但是,使系统可靠的关键部分在于努力实现稳定性。此外,如果团队A需要在核心中更改X,而团队B需要在核心中更改X,则X可能不属于核心;我认为这是我的另一点。:)
logc 2014年

@DocBrown是的,我们学会了为每个客户项目使用核心的一个分支。这引起了其他一些问题。例如,我们不喜欢“接触”已经部署的客户系统。因此,在每次部署之后,他们可能会遇到几个使用过的内核的次要版本跳转。
SDD64 2014年

@ SDD64:这就是我的意思-从长远来看,不立即将更改集成到一个公共内核也不是解决方案。您需要的是为核心提供更好的测试策略-以及自动和手动测试。
Doc Brown

1
出于记录的考虑,我并不是在为每个团队主张一个单独的核心,也不是否认需要进行测试-但是核心测试及其实现可以同时更改,正如我之前评论过的那样。在其基础上构建的项目只能依靠由发布字符串或提交标签标记的冻结核心(不包括错误修复,并且在制定版本控制政策的前提下)。
logc 2014年

3

就像其他人所说的那样,一套好的单元测试套件并不能解决您的问题:即使每个团队测试套件通过了,您在合并变更时也会遇到问题。

与TDD相同。我不知道它如何解决这个问题。

您的解决方案不是技术性的。您需要明确定义“核心”边界,并为某人(无论是首席开发人员还是架构师)分配“看门狗”角色。对内核的任何更改都必须通过此看门狗。他负责确保所有团队的每个输出都可以合并,而不会造成过多的附带损害。


因为他写了大部分核心内容,所以我们有了“看门狗”。可悲的是,他还负责了大多数未经测试的部分。他是YAGNI的模仿者,半年前已被另外两个人取代。我们仍然在努力重构那些“黑暗部分”。
SDD64 2014年

2
这个想法是有一个单元测试套件为核心,这是核心的一部分,与所有的球队,而不是单独的测试套件为每个团队的贡献。
布朗

2
@ SDD64:您似乎将“您还不需要(现在)”和“您不需要清理代码(现在)”混为一谈-这是一个极端的坏习惯,与恕我直言相反。
布朗

看门狗解决方案实际上是次优的,恕我直言。这就像在系统中构建单个故障点,在此之上非常缓慢,因为它涉及到人和政治。否则,TDD当然可以解决这个问题:每个核心测试都是客户项目开发人员应如何使用当前核心的一个示例。但是我认为您是真诚地给出了答案...
logc 2014年

@DocBrown:好的,也许我们的理解有所不同。他编写的核心功能过于复杂,无法满足最奇怪的可能性。他们中的大多数,我们从未遇到过。另一方面,复杂性使我们无法重构它们。
SDD64 2014年

2

作为更长期的解决方案,您还需要团队之间更好,更及时的沟通。每个将利用例如核心功能Y的团队都需要参与构建该功能的计划测试用例。该计划本身将突出显示两个团队在功能Y中固有的不同用例。一旦确定了功能的工作方式,并且测试用例已实现并达成共识,就需要对实现方案进行其他更改。发布该功能的团队需要运行测试用例,而不是将要使用它的团队。可能引起碰撞的任务是从两个团队中的一个添加新的测试用例。当团队成员想到未经测试的功能的新方面时,他们应该可以自由地添加已验证通过自己沙盒的测试用例。这样,将发生的唯一碰撞将是在意图级别上,并且应该在将重构功能发布到野外之前将其确定下来。


2

尽管每个系统都需要有效的测试套件(除其他外,这意味着自动化),并且这些测试(如果得到有效使用)将比现在更快地抓住这些冲突,但这并不能解决潜在的问题。

该问题至少揭示了两个基本问题:修改“核心”以满足个人客户需求的实践,以及团队无法沟通和协调他们进行变更的意图。这些都不是根本原因,因此您需要先了解为什么这样做,然后再进行修复。

首先要确定的事情之一是开发人员和管理人员是否都意识到这里存在问题。如果至少有人这样做,那么您需要找出为什么他们要么认为自己对此无能为力,要么选择不这样做。对于那些没有这样做的人,您可以尝试提高他们的能力,以预测他们当前的行为可能会如何造成未来的问题,或者由有能力的人来代替。除非您的员工知道问题的根源,否则您不太可能解决问题(至少在短期内,甚至不可能)。

至少从一开始就很难用抽象的术语来分析问题,因此请专注于导致问题的特定事件,并尝试确定问题的发生方式。由于涉案人员可能会采取防御措施,因此您需要警惕自我服务和事后证明,以查明实际情况。

我不愿提及一种可能性,因为它不太可能发生:客户的需求是如此分散,以至于通用性不足以证明共享的核心代码是合理的。如果是这样,则实际上您有多个单独的产品,应按此类进行管理,而不要在它们之间创建人为的耦合。


在我们将产品从Java迁移到RoR之前,我们实际上确实像您建议的那样做。我们有一个适用于所有客户的Java内核,但他们的要求有一天“中断”了,我们不得不将其拆分。在这种情况下,我们遇到了以下问题:'老兄,客户Y确实具有如此出色的核心功能。可惜我们不能将其移植到客户Z,因为他们的核心不兼容。有了Rails,我们严格地希望采取“人人享有一个核心”的政策。如果必须的话,我们仍然会提供巨大的更改,但是那些更改会使客户无法进行任何进一步的更新。
SDD64 2014年

对我来说,仅致电TDD似乎还不够。因此,除了最核心的建议外,我最喜欢您的回答。可悲的是,该内核尚未经过完美的测试,但这不能解决我们所有的问题。为一个客户添加新的核心功能似乎很不错,甚至可以为他们提供绿色的构建,因为客户之间仅共享核心规格。人们可能不会注意到,每一个可能的客户都会发生什么。因此,我喜欢您的建议,以找出问题所在并讨论造成这些问题的原因。
SDD64 2014年

1

我们都知道单元测试是必经之路。但是我们也知道,将它们现实地改造成一个核心是困难的。

扩展功能时可能对您有用的一项特定技术是尝试临时和本地验证现有功能是否已更改。可以这样完成:

原始伪代码:

def someFunction
   do original stuff
   return result
end

临时就地测试代码:

def someFunctionNew
   new do stuff
   return result
end

def someFunctionOld
   do original stuff
   return result
end

def someFunction
   oldResult = someFunctionOld
   newResult = someFunctionNew
   check oldResult = newResult
   return newResult
end

通过现有的任何系统级别测试运行此版本。如果一切正常,则说明您还没有损坏任何东西,然后可以继续删除旧代码。请注意,当您检查新旧结果匹配,则还可能会添加代码,分析差异,捕捉的情况下,你知道应该因为预期的变化是不同的,如bug修复。


1

“大多数情况下,变更不会在团队中宣布,因此错误几乎总是无法预料的”

通讯问题有人吗?为了确保进行适当的沟通(除了其他人已经指出的,您应该进行严格的测试)呢?人们已经意识到他们正在编写的界面将在下一个版本中更改,这些更改将是什么?
并在开发过程中尽快让他们至少访问一个虚拟接口(具有空实现),以便他们可以开始编写自己的代码。

没有所有这些,单元测试除了在最后阶段指出系统各部分之间存在某些不足之处外,不会做很多事情。您想知道这一点,但您想及早,很早就知道,并且让团队彼此交谈,协调努力,并且实际上可以经常访问另一个团队正在做的工作(因此定期提交,而不是一次大量的提交)交付数周或数月(交货前1-2天)后提交)。
您的错误不在代码中,当然也不在其他团队的代码中,这些团队不知道您正在与他们编写的接口搞混。您的错误是在开发过程中,人与人之间缺乏沟通和协作。仅仅因为您坐在不同的房间并不意味着您应该将自己与其他人隔离。


1

首先,您有一个沟通问题(可能还与团队建设问题有关),所以我认为针对您的案例的解决方案应该侧重于……好吧,沟通,而不是开发技术。

我认为在启动客户项目时不可能冻结或分叉核心模块(否则,您只需要在公司的日程中集成一些与客户无关的项目,以更新核心模块即可)。

因此,剩下的问题就是尝试改善团队之间的沟通。这可以通过两种方式解决:

  • 与人类。这意味着您的公司指定了一个人作为核心模块架构师(或任何对高级管理人员有用的术语),他们将负责代码的质量和可用性。这个人将化身为核心。因此,她将由所有团队共享,并确保他们之间的适当同步。此外,她还应担任致力于核心模块的代码的审阅者,以保持其一致性;
  • 工具和工作流程。通过在内核上进行持续集成,您将使内核代码本身成为通信介质。这首先需要付出一些努力(通过在其上添加自动化测试套件),但是随后的每晚CI报告将是核心模块的总体状态更新。

您可以在此处找到有关CI作为交流过程的更多信息。

最后,您仍然对公司缺乏团队合作精神有疑问。我不是团队建设活动的忠实拥护者,但这似乎是一种有用的案例。您是否定期举行开发人员范围的会议?您可以邀请其他团队的人参加您的项目回顾吗?还是有时在星期五晚上喝啤酒?

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.