如何避免级联重构?


52

我有一个项目。在此项目中,我希望对其进行重构以添加功能,并且对项目进行重构以添加功能。

问题是,当我完成后,原来我需要做一个小的接口更改以适应它。所以我做了改变。然后,就新类而言,无法使用其当前接口来实现消费类,因此它也需要一个新接口。现在已经三个月了,我不得不解决了几乎不相关的无数问题,而且我正在寻找解决从现在开始经过一年规划的问题,或者只是因为困难而列为无法解决的问题,然后再进行编译再次。

将来如何避免这种级联重构?这仅仅是我以前的课程互相依赖的一种征兆吗?

简要编辑:在这种情况下,重构就是功能,因为重构增加了特定代码的可扩展性并减少了一些耦合。这意味着外部开发人员可以做更多的事情,而这正是我想要提供的功能。因此,原始重构本身不应该是功能上的更改。

我五天前承诺的更大修改:

在开始重构之前,我有一个带有接口的系统,但是在实现中,我只是简单地dynamic_cast通过了所有可能的实现。显然,这意味着您不能一方面从接口继承,其次,对于没有实现访问权即可实现此接口的任何人,这都是不可能的。因此,我决定要解决此问题并开放供公众使用的界面,以便任何人都可以实施,而实施该界面是整个合同所必需的-显然是一项改进。

当我发现并用火杀死所有地方时,我发现一个地方被证明是一个特殊的问题。它取决于所有各种派生类的实现细节以及已经实现但在其他地方更好的重复功能。它可以代替公共接口来实现,而可以重新使用该功能的现有实现。我发现它需要特定的上下文才能正常运行。粗略地说,调用先前的实现看起来像

for(auto&& a : as) {
     f(a);
}

但是,要获得此上下文,我需要将其更改为更类似的内容

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

这意味着对于以前曾经是其中一部分的所有操作f,其中一些操作需要成为g无需上下文的新功能的一部分,而其中某些操作则需要由现在延迟的一部分组成f。但是,并非所有方法都f调用需要或需要此上下文-其中一些需要通过单独的方式获得的独特上下文。因此,对于f最终调用的所有内容(大致来说,几乎所有内容),我必须确定他们需要的上下文(如果有的话),应该从哪里获取以及如何将它们从旧f分为新f与新g

这就是我最终所处位置的方式。我一直坚持下去的唯一原因是因为无论如何我都需要这种重构。


67
当您说“重构项目以添加功能”时,您的确切意思是什么?根据定义,重构不会改变程序的行为,这会使该语句变得混乱。
Jules

5
@Jules:严格来说,该功能是允许其他开发人员添加特定类型的扩展,因此该功能是重构,这使得类结构更加开放。
DeadMG

5
我认为在谈论重构的每本书籍和文章中都讨论了这一点?源代码控制可以拯救;如果您发现要执行步骤A,则必须先执行步骤B,然后报废A,然后首先执行B。
rwong 2015年

4
@DeadMG:这是我最初想在第一句话中引用的书“游戏“拾起棒”是Mikado方法的一个很好的比喻。您消除了“技术债务”,这几乎是每个软件中都存在的固有问题。系统—遵循一组易于实现的规则。您仔细提取每个相互依存的依赖项,直到暴露出中心问题为止,而不会折叠项目。”
rwong 2015年

2
可以,您能否弄清楚我们在谈论哪种编程语言?阅读完您的所有评论后,我得出的结论是,您正在手动完成此操作,而不是使用IDE来帮助您。因此,我想知道是否可以给您一些实用的建议。
thepacker 2015年

Answers:


69

上一次我尝试以无法预料的后果开始重构,而一天后又无法稳定构建和/或测​​试 ,我放弃了并将代码库恢复到重构前的状态。

然后,我开始分析出了什么问题,并制定了一个更好的计划,如何以较小的步骤进行重构。因此,我对避免级联重构的建议是:知道何时停止,不要让事情失去控制!

有时,您不得不忍耐不住,放弃一整天的工作-绝对比放弃三个月的工作容易。你失去了这一天是不是完全白费,至少你已经学会了如何来解决这个问题。以我的经验,总是有可能在重构中采取更小的步骤。

旁注:您似乎处在一种情况下,您必须决定是否愿意牺牲整个三个月的工作,然后重新制定新的(可能更成功的)重构计划。我可以想象这不是一个容易的决定,但是问自己,您需要再三个月的风险有多高,这不仅是要稳定构建,还要修复在过去三个月中您可能在重写期间引入的所有无法预见的错误?我写了“ rewrite”,因为我想那是您真正所做的,而不是“重构”。通过上次编译项目并重新开始真正的重构(与“重写”相反),您可以更快地解决当前问题。


53

这仅仅是我以前的课程互相依赖的一种征兆吗?

当然。导致无数其他变化的一个变化几乎就是耦合的定义。

如何避免级联重构?

在最糟糕的代码库中,单个更改将继续级联,最终使您(几乎)更改所有内容。存在广泛耦合的任何重构的一部分是隔离您正在处理的部分。您不仅需要重构新功能触及该代码的位置,还需要重构其他所有触及该代码的位置。

通常,这意味着制作一些适配器来帮助旧代码使用外观和行为类似于旧代码但使用新实现/接口的东西。毕竟,如果您要做的只是更改接口/实现,但是离开耦合,您将一无所获。是猪的口红。


33
+1重构的需求越严重,重构的范围就越大。这是事物的本质。
Paul Draper 2015年

4
但是,如果您真正进行重构,则其他代码不必马上考虑更改。(当然,您最终将需要清理其他部分……但这不是立即需要的。)通过应用程序其余部分“层叠”进行的更改要比重构大,在这一点上基本上是重新设计或重写。
cHao 2015年

+1适配器正是隔离您首先要更改的代码的方法。
winkbrace

17

听起来您的重构过于雄心勃勃。重构应该分几个小步骤进行,每个步骤都可以(例如)在30分钟内完成-在最坏的情况下,最多一天-可以使项目可构建,并且所有测试仍可以通过。

如果您将每个变更保持在最小限度,那么重构就不可能长时间中断您的构建。最坏的情况可能是将参数更改为广泛使用的界面中的方法,例如添加新参数。但是随之而来的变化是机械的:在每个实现中添加(并忽略)参数,并在每个调用中添加默认值。即使有数百个引用,执行这样的重构也不需要花一天的时间。


4
我不知道这种情况如何发生。为了对方法接口进行任何合理的重构,必须传递一组易于确定的新参数,以使调用的行为与更改前的行为相同。
Jules

3
我从来没有遇到过要执行这样的重构的情况,但是我不得不说这对我来说听起来很不寻常。您是说您从界面中删除了功能吗?如果是这样,去哪儿了?进入另一个界面?或者别的地方?
Jules

5
然后,方法是在重构之前删除要删除的功能的所有用法,而不是之后。这样,您就可以在进行代码构建时保持代码构建。
Jules

11
@DeadMG:听起来很奇怪:正如您所说,您正在删除一项不再需要的功能。但是另一方面,您写的是“项目变得完全无法运行”,这实际上听起来是绝对需要的功能。请澄清。
布朗

26
@DeadMG在这种情况下,您通常将开发新功能,添加测试以确保其正常工作,将现有代码转换为使用新接口,然后删除(现在)多余的旧功能。这样一来,就不会有事情破裂的地步。
sapi 2015年

12

将来如何避免这种级联重构?

一厢情愿的思维设计

目标是针对新功能进行出色的OO设计和实现。避免重构也是一个目标。

从头开始,为您希望拥有的新功能进行设计。花时间做好它。

但是请注意,这里的关键是“添加功能”。新事物往往使我们在很大程度上忽略了代码库的当前结构。我们的如意算盘设计是独立的。但是,我们还需要两件事:

  • 仅充分重构,以形成必要的接缝,以注入/实现新功能的代码。
    • 重构的阻力不应驱动新设计。
  • 用API编写面向客户的类,该类使新功能和现有的编解码器彼此之间完全幸福。
    • 它进行音译以获取来回的对象,数据和结果。最少知识原则是该死的。我们不会做比现有代码已经做的更糟的事情。

启发式,经验教训等。

重构就像将默认参数添加到现有方法调用一样简单。或单个调用静态类方法。

现有类上的扩展方法可以帮助以最低的绝对风险保持新设计的质量。

“结构”就是一切。结构是单一责任原则的实现;有助于功能的设计。在类层次结构中,代码将始终保持简短。在测试,返工和避免对旧代码丛林的黑客攻击期间,要花大量时间进行新设计。

一厢情愿的思维班专注于手头的任务。通常,无需扩展现有的类-您只是在再次引发重构级联,而不得不处理“较重”类的开销。

从现有代码中清除此新功能的所有残余。在这里,完整且封装良好的新功能比避免重构更重要。


9

摘自迈克尔·费瑟斯(Michael Feathers)的(精彩的)著作《有效地使用传统代码》

当您破坏传统代码中的依赖关系时,通常必须稍微暂停一下审美意识。有些依赖关系会彻底中断;从设计的角度来看,其他人最终看起来并不理想。它们就像是手术中的切开点:工作后,代码中可能会留下疤痕,但是它下面的所有东西都会变得更好。

如果以后可以覆盖破坏依赖点的地方覆盖代码,那么您也可以修复这种伤痕。


6

听起来(尤其是在评论中的讨论中),您已经采用了自我强加的规则,这意味着“较小”的更改与完全重写软件的工作量相同。

解决方案必须是“然后不要这样做”。这就是实际项目中发生的情况。结果,许多旧的API都有难看的接口或废弃的参数(总是为null),或者名为DoThisThing2()的函数与具有完全不同的参数列表的DoThisThing()相同。其他常见的技巧包括将信息存储在全局变量或带标记的指针中,以便将其走私到很大的框架中。(例如,我有一个项目,其中一半的音频缓冲区仅包含一个4字节的魔术值,因为它比更改库调用其音频编解码器的方式容易得多。)

没有特定的代码很难给出具体的建议。


3

自动化测试。您无需成为TDD狂热者,也不需要100%覆盖,但是自动化测试使您可以放心地进行更改。另外,听起来您的设计具有很高的耦合度。您应该阅读SOLID原则,这些原则是专门为解决软件设计中的此类问题而制定的。

我也会推荐这些书。

  • 有效地使用旧版规范,羽毛
  • 重构,福勒
  • 在Tests,Freeman和Pryce的指导下,不断发展的面向对象软件
  • 干净的代码,马丁

3
您的问题是:“将来如何避免这种[故障]?” 答案是,即使您当前“拥有”配置项和测试,也无法正确应用它们。几年来,我没有一个持续超过十分钟的编译错误,因为我将编译视为“第一个单元测试”,而当它损坏时,我将其修复,因为我需要能够看到测试通过我正在进一步研究代码。
asthasr 2015年

6
如果我要重构频繁使用的接口,请添加垫片。该填充程序可处理默认设置,以便旧式呼叫继续起作用。我在填充程序后面的接口上工作,然后,当我完成操作后,我开始更改类以再次使用该接口而不是填充程序。
asthasr 2015年

5
尽管构建失败,仍继续进行重构类似于推算。这是不得已的导航技术。在重构中,重构的方向可能是完全错误的,并且您已经看到了明显的迹象(停止编译的那一刻,即没有空速指示器的飞行),但是您决定继续进行。最终飞机从雷达上掉下来。幸运的是,我们不需要黑匣子或调查人员进行重构:我们始终可以“还原到最后一个已知的良好状态”。
rwong 2015年

4
@DeadMG:您写了“就我而言,以前的调用已经不再有意义了”,但是您的问题是“一个较小的接口更改以适应它”。老实说,这两个句子中只有一个是正确的。从问题描述中可以很明显地看出,您的界面更改绝对不是问题。您应该真的,应该更加认真地思考如何使您的更改向后兼容。以我的经验,这总是可能的,但是您必须首先想出一个好的计划。
布朗

3
@DeadMG在这种情况下,我认为您所做的不能合理地称为重构,其基本要点是将设计更改作为一系列非常简单的步骤来应用。
Jules

3

这仅仅是我以前的课程互相依赖的一种征兆吗?

可能是的。尽管当需求发生足够的变化时,您可以通过相当不错且简洁的代码库获得类似的效果

将来如何避免这种级联重构?

除了停止处理遗留代码外,您还可以担心。但是,您可以使用一种方法来避免几天,几周甚至几个月不使用有效代码库的影响。

该方法名为“天皇方法”,其工作方式如下:

  1. 在一张纸上写下您要实现的目标

  2. 进行最简单的更改即可带您朝那个方向发展。

  3. 使用编译器和测试套件检查其是否有效。如果确实存在,请继续执行步骤7。否则,请继续执行步骤4。

  4. 在您的纸上,请注意为了使当前的更改生效而需要更改的内容。从当前任务到新任务绘制箭头。

  5. 恢复您的更改这是重要的步骤。这是一种反直觉的感觉,一开始会对身体造成伤害,但是由于您只是尝试了一件简单的事情,因此实际上并没有那么糟糕。

  6. 选择没有传出错误(没有已知依赖性)的任务之一,然后返回到2。

  7. 提交更改,划掉纸上的任务,选择没有传出错误(没有已知依赖性)的任务,然后返回2。

这样,您将在很短的间隔内获得一个有效的代码库。您还可以在其中合并团队其他成员的更改。您可以直观地了解自己仍然需要做的事情,这有助于确定是否要继续进行这项工作,还是应该停止该工作。


2

重构是一种结构化的学科,与您认为合适的清理代码不同。您需要在开始之前编写单元测试,并且每个步骤都应包含一个特定的转换,您应该知道该转换不会对功能进行任何更改。每次更改后,单元测试都应通过。

当然,在重构过程中,您自然会发现应应用的更改可能会导致损坏。在这种情况下,请尽力为使用新框架的旧接口实现兼容性垫片。从理论上讲,系统应该仍然可以像以前一样工作,并且单元测试应该通过。您可以将兼容性填充程序标记为已弃用的接口,并在更合适的时间对其进行清理。


2

...我重构了项目以添加功能。

正如@Jules所说,重构和添加功能是两件事。

  • 重构是指在不改变程序行为的情况下更改程序的结构。
  • 另一方面,添加功能会增强其行为。

...但是确实,有时您需要更改内部工作方式以添加您的内容,但我宁愿称其为修改而不是重构。

我需要进行一些小的界面更改以适应它

那就是事情变得混乱的地方。接口是作为边界来将实现与使用方式隔离开来。触摸界面后,任何一侧(实现或使用它)的所有内容也都必须更改。这可以传播到您所经历的程度。

则使用新类的消费类无法使用其当前接口来实现,因此它也需要一个新接口。

一个界面需要更改听起来不错...它传播到另一个界面意味着更改甚至进一步传播。听起来某种形式的输入/数据需要沿着链条向下流动。是这样吗


您的演讲非常抽象,因此很难弄清楚。一个例子将非常有帮助。通常,接口应该非常稳定并且彼此独立,这使得可以修改系统的一部分而不会损害其余部分……这要归功于接口。

...实际上,避免级联代码修改的最佳方法就是良好的接口。;)


-1

我认为您通常无法做到,除非您愿意保持现状。但是,当遇到像您这样的情况时,我认为更好的方法是通知团队,让他们知道为什么要进行一些重构以继续更健康的发展。我不会自己去修理。我会在Scrum会议上谈论它(假设你们有),然后与其他开发人员系统地讨论。


1
在先前的9个答案中所提出和解释的观点上,这似乎没有提供任何实质性的建议
gnat 2015年

@gnat:可能不是,但是简化了响应。
塔里克
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.