如何处理11000行C++源文件?

Mar*_* Ba 228 c++ version-control maintenance anti-patterns

所以我们在我们的项目中有这个巨大的(11000行巨大的?)mainmodule.cpp源文件,每次我都要触摸它时我都会感到畏缩.

由于这个文件是如此中心和大,它不断累积越来越多的代码,我想不出一个让它真正开始缩小的好方法.

该文件在我们产品的几个(> 10)维护版本中使用并主动更改,因此很难重构它.如果我"简单地"把它分开,比如说开始,分成3个文件,那么合并来自维护版本的变化将成为一场噩梦.而且,如果你拆分了一个历史悠久且历史悠久的文件,跟踪和检查历史中的旧变化SCC突然变得更加困难.

该文件基本上包含我们程序的"主类"(主要内部工作调度和协调),因此每次添加一个功能时,它也会影响该文件,并且每次增长时都会影响该文件.:-(

在这个情况下,你会怎么做?有关如何将新功能移动到单独的源文件而不会弄乱SCC工作流程的任何想法?

(关于工具的注意事项:我们使用C++ Visual Studio;我们使用AccuRevas SCC但我认为这里的类型SCC并不重要;我们Araxis Merge用来做实际比较和合并文件)

Kir*_*sky 129

合并将不会是一个如此大的噩梦,因为将来你将获得30000个LOC文件.所以:

  1. 停止向该文件添加更多代码.
  2. 拆分它.

如果你不能只停留在重构过程中的编码,你可以离开这个大文件作为是一会儿至少不增加更多的代码是:因为它包含一个"主类",你可以从它继承并保留继承的类( es)在几个新的小型和精心设计的文件中重载功能.

  • @Martin,你有几个GOF模式可以做到这一点,一个**Facade**映射mainmodule.cpp的功能,或者(我在下面推荐)创建一套**Command**每个映射到mainmodule.app的函数/功能的类.(我在答案上对此进行了扩展.) (9认同)
  • 有10个维护版本和许多活跃的开发人员,文件不可能被冻结足够长的时间. (3认同)
  • 是的,完全同意,在某些时候你必须停止添加代码或最终它将是30k,40k,50k,kaboom mainmodule只是seg故障.:-) (2认同)

Ste*_*sop 85

  1. 在文件中找到一些相对稳定的代码(不会快速变化,并且在分支之间变化不大)并且可以作为独立单元.将其移动到自己的文件中,并将其移动到自己的类中,在所有分支中.因为它是稳定的,所以当您将更改从一个分支合并到另一个分支时,这不会导致(许多)"笨拙"合并,这些合并必须应用于与最初创建的文件不同的文件.重复.

  2. 在文件中找到一些基本上只适用于少数分支的代码,并且可以独立存在.无论是否快速变化都无关紧要,因为分支数量很少.将其移动到自己的类和文件中.重复.

因此,我们已经摆脱了无处不在的代码,以及特定于某些分支的代码.

这会让你有一个管理不善的代码的核心 - 它在任何地方都需要,但它在每个分支中都是不同的(和/或它不断变化,以便某些分支在其他分支后面运行),然而它只是在一个文件中,你是尝试在分支之间合并失败.别那样做.永久分支文件,可能通过在每个分支中重命名它.它不再是"主要",它是"主要用于配置X".好的,所以你失去了通过合并将相同的更改应用于多个分支的能力,但这无论如何都是代码的核心,其中合并不能很好地工作.如果您不得不手动管理合并以处理冲突,那么在每个分支上手动应用它们并不是一种损失.

我认为你说这种SCC并不重要是错的,因为例如git的合并能力可能比你正在使用的合并工具更好.因此,对于不同的SCC,在不同时间发生核心问题"合并困难".但是,您不太可能能够更改SCC,因此问题可能无关紧要.


Bri*_*sen 67

听起来像你在这里遇到了许多代码味道.首先,主要类似乎违反了开放/封闭原则.这听起来像处理太多的责任.由于这个原因,我认为代码比它需要的更脆弱.

虽然我可以理解您在重构后对可追溯性的担忧,但我希望这个类很难维护和增强,并且您所做的任何更改都可能导致副作用.我认为这些成本超过了重构课程的成本.

在任何情况下,由于代码气味只会随着时间的推移而变差,至少在某些时候,这些代价会超过重构成本.从你的描述中我会假设你已经超过了引爆点.

重构这一点应该在很短的步骤内完成.如果可能,重构任何内容之前添加自动化测试以验证当前行为.然后选择小区域的隔离功能并将其作为类型提取,以便委派责任.

无论如何,这听起来像是一个重大项目,祝你好运:)

  • 它闻起来很多:它闻起来像Blob反模式就在房子里...... http://en.wikipedia.org/wiki/God_object.他最喜欢的一餐是意大利面条代码:http://en.wikipedia.org/wiki/Spaghetti_code :-) (18认同)
  • +1 @Brian在重构之前进行自动验证. (4认同)

Ben*_*oît 49

我遇到过这种问题的唯一解决方案如下.所描述的方法的实际增益是演变的渐进性.这里没有革命,否则你会很快遇到麻烦.

在原始主类上面插入一个新的cpp类.现在,它基本上将所有调用重定向到当前主类,但旨在使这个新类的API尽可能清晰和简洁.

完成此操作后,您就可以在新类中添加新功能.

至于现有的功能,你必须逐步将它们移动到新的类中,因为它们足够稳定.对于这段代码,您将失去SCC帮助,但是没有太多可以做的.选择合适的时机.

我知道这并不完美,但我希望它可以提供帮助,而且这个过程必须适应您的需求!

附加信息

请注意,Git是一个SCC,可以跟踪从一个文件到另一个文件的代码片段.我听说过有关它的好东西,所以当你逐步转移工作时它会有所帮助.

Git是围绕blob的概念构建的,如果我理解正确的话,代表了一些代码文件.将这些部分移动到不同的文件中,即使您修改它们,Git也会找到它们.除了下面评论中提到的Linus Torvalds的视频外,我还未能找到明确的内容.

  • @Martin,看看这个问题:http://stackoverflow.com/questions/1728922/how-does-git-track-source-code-moved-between-files (6认同)
  • @Martin:Git自动完成 - 因为它不跟踪文件,它跟踪内容.在git中实际上更难以"获取一个文件的历史记录". (4认同)

小智 30

孔子说:"走出洞的第一步就是停止挖洞."


Ian*_*Ian 25

让我猜一下:十个拥有不同功能集的客户和一位促进"定制"的销售经理?我之前曾经做过类似的产品.我们基本上有同样的问题.

您认识到拥有一个巨大的文件是一个麻烦,但更麻烦的是十个版本,你必须保持"当前".这是多重维护.SCC可以使这更容易,但它无法使其正确.

在尝试将文件分成多个部分之前,需要将十个分支重新同步,以便您可以一次查看和整形所有代码.您可以一次执行一个分支,针对同一主代码文件测试两个分支.要强制执行自定义行为,您可以使用#ifdef和friends,但是尽可能使用普通的if/else来定义常量.这样,您的编译器将验证所有类型,并且最有可能消除"死"对象代码.(不过,您可能希望关闭有关死代码的警告.)

一旦只有一个版本的文件被所有分支隐式共享,那么开始传统的重构方法就相当容易了.

#ifdefs主要适用于受影响的代码仅在其他每个分支自定义的上下文中有意义的部分.有人可能会争辩说,这些也为同一个分支合并计划提供了机会,但不要疯狂.请一次一个巨大的项目.

在短期内,文件似乎会增长.还行吧.你正在做的是把需要在一起的东西放在一起.之后,无论版本如何,您都会看到明显相同的区域; 这些可以单独留下或随意重构.其他区域明显不同,具体取决于版本.在这种情况下,您有许多选项.一种方法是将差异委派给每个版本的策略对象.另一种方法是从公共抽象类派生客户端版本.但只要你在不同的分支中有十个发展的"提示",这些转变都不可能.

  • 我同意目标应该是拥有一个版本的软件,但使用配置文件(运行时)并不是编译时间保留不是更好 (2认同)

Rob*_*bin 22

我不知道这是否能解决您的问题,但我想您想要做的是将文件内容迁移到彼此独立的较小文件(总结).我还得到的是,你有大约10种不同版本的软件,你需要支持它们而不会搞砸.

首先,没有办法让这很容易,并且会在几分钟的头脑风暴中自行解决.文件中链接的功能对您的应用程序至关重要,只需将其删除并将其迁移到其他文件将无法解决您的问题.

我想你只有这些选择:

  1. 不要迁移并保持你拥有的东西.可能会退出你的工作并开始研究具有良好设计的严肃软件.如果您正在进行长期项目并且有足够的资金来应对一两次崩溃,那么极限编程并不总是最佳解决方案.

  2. 制定一个布局,说明一旦分割出来,你会喜欢你的文件.创建必要的文件并将它们集成到您的应用程序中.重命名函数或重载它们以获取一个额外的参数(也许只是一个简单的布尔值?).一旦必须处理代码,将需要处理的函数迁移到新文件,并将旧函数的函数调用映射到新函数.您仍然应该以这种方式拥有主文件,并且仍然能够看到对其进行的更改,一旦涉及到您在外包时确切知道的特定功能等等.

  3. 尝试说服你的同事一些好的蛋糕,工作流程被高估了,你需要重写应用程序的某些部分才能开展认真的业务.


Pat*_*ick 19

确切地说,这个问题在"有效使用遗留代码"(http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052)一书的章节中处理.

  • 第20章是10,000行,但是作者正在研究如何将它分成易消化的块... 8) (17认同)
  • @Martin,这本书非常出色,但它确实非常依赖测试,重构,测试周期,这可能是你现在所处的困难.我一直处于类似的情况,这本书是我发现的最有用的一本.对于你遇到的丑陋问题,这是一个很好的建议.但如果你无法在图片中找到某种类型的测试工具,那么世界上所有的重构建议都无济于事. (2认同)

oco*_*odo 14

我认为你最好创建一组映射到mainmodule.cpp的API点的命令类.

一旦它们到位,您将需要重构现有的代码库以通过命令类访问这些API点,一旦完成,您可以自由地将每个命令的实现重构为新的类结构.

当然,对于单个类的11个KLOC,其中的代码可能是高度耦合和脆弱的,但创建单独的命令类将比任何其他代理/外观策略更有帮助.

我并不羡慕这项任务,但随着时间的推移,这个问题只会在没有解决的情况下变得更糟.

更新

我建议Command模式比Facade更好.

在(相对)单片Facade上维护/组织许多不同的Command类是可取的.将单个Facade映射到11 KLOC文件可能需要将其分解为几个不同的组.

为什么要费心去弄清楚这些门面组呢?使用Command模式,您将能够有机地对这些小类进行分组和组织,从而使您具有更大的灵活性.

当然,这两个选项都比单个11 KLOC和增长文件更好.


Mic*_*tum 13

一个重要的建议:不要混合重构和错误修正.你想要的是你的程序版本与以前的版本相同,只是源代码不同.

一种方法是开始将最小的函数/部分拆分成它自己的文件,然后用一个头包含(从而将main.cpp转换成#includes列表,这听起来本身就是代码味道*我不是虽然是C++ Guru),但至少它现在被分成了文件).

然后,您可以尝试将所有维护版本切换到"new"main.cpp或任何结构.再说一遍:没有其他变化或错误修正,因为追踪那些令人困惑的地狱.

另一件事:尽管你可能想要一次性重构整个事情,但你可能会咀嚼比你能咀嚼更多的东西.也许只需选择一个或两个"部分",将它们放入所有版本中,然后为您的客户添加更多价值(毕竟,重构不会增加直接价值,因此这是一个必须合理的成本)然后选择另一个一两个部分.

显然,团队需要一些规则来实际使用拆分文件而不是一直向main.cpp添加新东西,但是再次尝试做一个大型重构可能不是最好的行动方案.


zig*_*tar 10

罗夫,这让我想起了我以前的工作.看来,在我加入之前,一切都在一个巨大的文件(也是C++)中.然后他们将它(使用包含在完全随机的点)分成大约三个(仍然是巨大的文件).正如您所料,该软件的质量非常糟糕.该项目总计约4万吨LOC.(几乎没有评论,但有很多重复的代码)

最后,我完成了对项目的重写.我开始从零开始重做项目中最糟糕的部分.当然,我想到了这个新部件和其他部件之间可能的(小)接口.然后我确实将这部分插入到旧项目中.我没有重构旧代码来创建必要的接口,但只是替换它.然后我从那里做了一小步,重写旧代码.

我不得不说这花了大约半年时间,并且在那段时间里没有开发旧的代码库和错误修正.


编辑:

尺寸保持在约40k LOC,但新应用程序包含更多功能,并且初始版本可能比8年前的软件更少.重写的一个原因还在于我们需要新功能并将它们引入旧代码中几乎是不可能的.

该软件用于嵌入式系统,即标签打印机.

我要补充的另一点是理论上该项目是C++.但它根本不是OO,可能是C.新版本是面向对象的.

  • 每当我在关于重构的话题中"从头开始"听到我杀了一只小猫! (9认同)

And*_*rsK 8

好吧,我理解你的痛苦:)我也参与了一些这样的项目并且它并不漂亮.对此没有简单的答案.

可能对您有用的一种方法是开始在所有函数中添加安全防护,即检查参数,方法中的前/后条件,然后最终添加单元测试以捕获源的当前功能.一旦你有了这个,你就可以更好地重新考虑代码,因为如果你忘记了什么,你会发出断言和错误提醒你.

有时虽然有时候重构可能会带来更多痛苦而不是利益.然后最好离开原始项目并处于伪维护状态,然后从头开始,然后逐步添加来自野兽的功能.


小智 8

好的,因此大多数情况下重写生产代码的API作为一个开始是一个坏主意.有两件事需要发生.

其一,您需要让您的团队决定对此文件的当前生产版本执行代码冻结.

第二,你需要采用这个生产版本并创建一个分支来管理构建,使用预处理指令来分割大文件.使用JUST预处理程序指令(#ifdefs,#include,#endifs)拆分编译比重新编码API更容易.这对您的SLA和持续支持来说肯定更容易.

在这里,您可以简单地删除与类中特定子系统相关的函数,并将它们放在一个文件中,例如mainloop_foostuff.cpp,并将其包含在mainloop.cpp中的正确位置.

要么

更耗时但更健壮的方法是设计一个内部依赖关系结构,其中包含事物的双重间接方式.这将允许您拆分并仍然处理共同依赖.请注意,此方法需要位置编码,因此应与适当的注释相结合.

此方法将包括根据您正在编译的变体使用的组件.

基本结构是你的mainclass.cpp将包含一个名为MainClassComponents.cpp的新文件,如下所示:

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"
Run Code Online (Sandbox Code Playgroud)

MainClassComponents.cpp文件的主要结构将用于解决子组件中的依赖关系,如下所示:

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */
Run Code Online (Sandbox Code Playgroud)

现在,为每个组件创建一个component_xx.cpp文件.

当然我正在使用数字,但你应该根据你的代码使用更合理的东西.

使用预处理器可以让您分解,而不必担心API变化,这是生产中的噩梦.

一旦您完成了生产,您就可以实际进行重新设计.