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

所以我们有这个巨大的(是11000行巨大?)mainmodule.cpp源文件在我们的项目中,每当我碰它时我都畏缩。

由于这个文件如此重要,它不断累积越来越多的代码,我想不出一个好办法让它真正开始缩小。

该文件在我们的产品的几个(> 10)维护版本中被使用和主动更改,因此很难对其进行重构。 如果我只是简单地把它分解成3个文件,那么从维护版本合并更改将成为一个噩梦。 而且,如果你将这个历史悠久而丰富的文件分割开来,跟踪和检查SCC历史上的旧的变化突然变得更加困难。

该文件基本上包含了我们程序的“主要类”(主要内部工作调度和协调),所以每次添加一个function时,它也影响到这个文件,每次增长。 🙁

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

(关于工具的注意事项:我们在Visual Studio使用C ++;我们使用AccuRev作为SCC但是我认为SCC的types在这里并不重要;我们使用Araxis Merge来进行文件的实际比较和合并)

  1. 在文件中find一些相对稳定的代码(不能快速变化,分支之间变化不大),可以作为一个独立的单元。 将其转移到自己的档案中,并将其归入自己的class级,在所有分支中。 因为它是稳定的,所以当从一个分支到另一个分支合并时,这不会导致(很多)“尴尬的”合并,这个合并必须应用到与它们原来的文件不同的文件上。 重复。

  2. 在文件中find一些基本上只适用于less数分支的代码,可以独立使用。 由于分支机构数量较less,因此它是否快速变化并不重要。 把它移到它自己的类和文件中。 重复。

所以,我们摆脱了到处都是一样的代码,以及特定于某些分支的代码。

这给你留下了一个pipe理不好的代码的核心 – 它到处都是需要的,但是每个分支都不一样(或者它不断变化,以至于一些分支在别人的后面运行),但是它是在一个单独的文件中试图在分支之间合并失败。 别那样做。 永久分支文件,也许在每个分支中重命名。 它不再是“主”,它是“configurationX的主要”。 好的,所以你失去了通过合并将相同的改变应用于多个分支的能力,但是这无论如何是合并不能很好地工作的代码的核心。 如果您不得不手动pipe理合并来处理冲突,那么在每个分支上手动应用它们都是没有任何损失的。

我认为你说这种SCC无关紧要,因为例如git的合并能力可能比你使用的合并工具要好。 所以不同的SCC在不同的时间出现“合并困难”的核心问题。 但是,你不可能改变SCC,所以这个问题可能是无关紧要的。

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

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

如果你不能在重构过程中停止编码,那么至less不用再添加更多的代码就可以保留这个大文件:因为它包含了一个“主类”,所以你可以inheritance它,inheritance类( es)与重载function在几个新的小而精心devise的文件。

这听起来像你在这里面临的一些代码气味。 首先,主类似乎违反了开放/封闭的原则 。 这听起来好像是在处理太多的责任 。 由于这个原因,我认为代码要比它需要的更脆。

虽然我可以理解你在重构后对可追溯性的关注,但是我期望这个类很难维护和增强,你所做的任何更改都可能会导致副作用。 我会认为这些成本超过重构类的成本。

无论如何,由于代码的味道只会随着时间的推移而变得更糟,至less在某些时候,代码的成本将超过重构的代价。 从你的描述我会认为你已经过了临界点。

重构这应该在一小步完成。 如果可能, 重构任何东西之前添加自动化testing来validation当前行为。 然后挑选出一小部分独立的function,并将其作为types进行提取,以分配责任。

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

我曾经想过解决这个问题的唯一解决scheme如下。 所描述的方法的实际收益是进化的先进性。 这里没有革命,否则你会很快陷入困境。

在原始主类上方插入一个新的cpp类。 现在,基本上所有调用都会redirect到当前的主类,但是目的是使这个新类的API尽可能地简洁明了。

一旦完成,你就有可能在新的类中添加新的function。

至于现有的function,你必须逐步将它们移动到新的类中,以使它们变得足够稳定。 您将失去SCC对这段代码的帮助,但是对此没有太多可以做的事情。 只要select正确的时间。

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

附加信息

请注意,Git是一个SCC,可以从一个文件到另一个文件。 我已经听到了很多关于它的好消息,所以在你逐渐改变你的工作的时候,这可能会有所帮助。

Git是围绕blob的概念构build的,如果我理解正确的话,代表一些代码文件。 移动这些碎片在不同的文件中,即使你修改它们,Git也会find它们。 除了下面评论中提到的Linus Torvalds的video ,我还没有find明确的说明。

孔子说:“走出去的第一步就是停止挖洞”。

让我猜猜:十个具有不同function的客户和一个促销“定制”的销售经理? 我以前曾经在这样的产品上工作过。 我们有基本相同的问题。

你知道有一个巨大的文件是麻烦,但更麻烦的是十个版本,你必须保持“当前”。 这是多重维护。 SCC可以使这一点更容易,但它不能做到正确。

在尝试将文件分解为多个部分之前,需要将这十个分支重新同步,以便一次查看并形成所有代码。 您可以一次执行一个分支,对同一主代码文件testing两个分支。 为了执行自定义行为,可以使用#ifdef和friends,但是尽可能使用普通的if / else来定义常量。 这样,你的编译器将会validation所有的types,而且很可能消除“死”的对象代码。 (不过,您可能要closures关于死代码的警告。)

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

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

在短期内,该文件似乎会增长。 还行吧。 你正在做的是把需要在一起的东西联系起来。 之后,无论版本如何,您都会看到明显相同的区域; 这些可以单独留下,也可以随意重构。 其他地区将根据版本明显不同。 在这种情况下你有很多select。 一种方法是将差异委托给每个版本的策略对象。 另一个是从一个共同的抽象类派生客户端版本。 但是,只要你在不同的分支有十个发展的“技巧”,这些转变都是不可能的。

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

首先,这是不容易的,并且会在几分钟的头脑风暴中解决。 链接到你的文件中的函数对于你的应用程序来说都是至关重要的,只要简单地将它们剪切并将它们移植到其他文件中就不会节省你的问题。

我想你只有这些select:

  1. 不要迁移,并保持你拥有的东西。 可能退出你的工作,并开始在devise良好的严重软件上工作。 如果你在一个有足够资金的长期项目中工作,那么极限编程并不总是最好的解决scheme。

  2. 制定一个你喜欢你的文件,看看它分裂后的样子。 创build必要的文件并将其集成到您的应用程序中。 重命名函数或重载它们以获取额外的参数(也许只是一个简单的布尔?)。 一旦你必须处理你的代码,把你需要处理的函数移植到新文件中,并将旧函数的函数调用映射到新函数。 你仍然应该有这样的主文件,并且一旦涉及到一个特定的函数,你就知道它是什么时候被外包的,等等。

  3. 尝试说服你的同事用一些很好的蛋糕,工作stream程被高估,你需要重写应用程序的一些部分,以便做生意。

这个问题确实在“使用遗留代码有效地工作”一书的其中一章中得到解决( http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052 )。

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

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

当然,对于单一types的KLOC,这里的代码可能是高度耦合和脆弱的,但是创build单独的命令类将比其他代理/外观策略更有帮助。

我不羡慕这个任务,但随着时间的推移,这个问题只会变得更糟,如果没有解决。

更新

我build议Command模式比Facade更可取。

维护/组织很多不同的Command类,而不是在一个(相对)单一的Facade上是比较好的。 将单个Facade映射到11 KLOC文件可能需要分解成几个不同的组。

为什么还要努力弄清楚这些门面小组? 使用命令模式,您可以将这些小类有机地进行分组和组织,因此您拥有更多的灵活性。

当然,这两种select都比单一的KLOC更好,而且越来越多。

一个重要的build议:不要混用重构和错误修正。 你想要的是你的程序的一个版本与以前的版本是一样的,只是源代码是不同的。

一种方法可能是开始将最小的大函数/部分分解到它自己的文件中,然后包含一个头文件(从而将main.cpp转换成#include包的列表,这听起来是一种代码味道*我不是一个C ++大师),但至less它现在被分割成文件)。

然后,您可以尝试将所有维护版本切换到“新”main.cpp或任何您的结构。 再次:没有其他更改或错误修复,因为跟踪这些混乱如地狱。

另外一件事情:就像你想要一次性重构整个事物一样,你可能会咬掉更多的东西。 也许只是select一个或两个“零件”,让他们进入所有的版本,然后为您的客户添加更多的价值(毕竟,重构不增加直接价值,所以这是一个成本必须被certificate),然后select另一个一个或两个部分。

显然,在团队中需要一定的纪律来实际使用分割文件,而不是一直向main.cpp中添加新的东西,但是再次尝试执行一个大规模的重构可能不是最好的行为。

罗夫尔,这让我想起了我以前的工作。 看来,在我join之前,一切都在一个巨大的文件(也是C ++)中。 然后他们把它分成三份(还是很大的文件)。 正如你所期望的,这个软件的质量是可怕的。 该项目总计大约40k LOC。 (几乎没有评论,但重复的代码LOTS)

最后我做了一个完整的项目重写。 我从头开始重做这个项目最糟糕的部分。 当然,我想到了这个新的部分和其他部分之间可能的(小的)接口。 然后我把这部分插入到旧项目中。 我没有重构旧的代码来创build必要的接口,而是将其replace。 然后,我从那里做了一小步,重写旧的代码。

我不得不说,这大概需要半年的时间,而且在那段时间的错误修正之前,还没有开发旧的代码库。


编辑:

大小停留在大约40k LOC,但新的应用程序包含更多的function,并且在初始版本中比8年前的软件大大减less了错误。 重写的一个原因是我们需要新的特性,在旧的代码中引入它们几乎是不可能的。

该软件是用于embedded式系统,标签打印机。

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

那么大多数情况下重写生产代码的API是一个坏主意。 有两件事情需要发生。

一,你需要真正让你的团队决定在当前生产版本的文件上进行代码冻结。

二,你需要采取这个生产版本,并创build一个分支,pipe理构build使用预处理指令拆分大文件。 使用JUST预处理器指令(#ifdefs,#include,#endifs)拆分编译比重新编码API更容易。 对于您的SLA和持续的支持肯定更容易。

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

要么

一个更耗时但更可靠的方法是devise一个内部的依赖关系结构,以双重间接的方式来包含事物。 这将允许你分裂的东西,仍然照顾共同依赖。 请注意,这种方法需要位置编码,因此应该配合适当的评论。

这种方法将包括根据您正在编译的变体使用的组件。

基本的结构是你的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" 

MainClassComponents.cpp文件的主要结构可以在这里find这样的子组件的依赖关系:

 #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 */ 

现在为每个组件创build一个component_xx.cpp文件。

当然,我使用的是数字,但是您应该根据您的代码使用更多的逻辑。

使用预处理器可以让你分割东西,而不必担心API的变化,这是生产中的噩梦。

一旦你解决了生产问题,你实际上可以重新devise。

那么我明白你的痛苦:)我已经在一些这样的项目,以及它不漂亮。 对此没有简单的答案。

一种可能适用于你的方法是开始在所有函数中添加安全卫士,即检查参数,方法中的前/后条件,然后最后添加unit testing,以捕获源的当前function。 一旦你有了这个,你就可以更好地重新设置代码,因为如果你忘记了某些东西,你就会popup提示和错误提醒你。

有时虽然有时候重构只会带来更多的痛苦,而不会带来好处。 那么离开原来的项目并进入伪维护状态,从头开始,然后逐步增加野兽的function可能会更好。

你不应该关心缩小文件大小,而应该减小文件大小。 这几乎是相同的,但是让你从另一个angular度来看待问题(就像@Brian Rasmussen 所说 ,你的class级似乎有很多责任)。

你所拥有的是一个经典的例子,称为blob的已知devise反模式。 花一些时间阅读我在这里指出的文章,也许你可能会发现一些有用的东西。 另外,如果这个项目看上去很大,那么你应该考虑一些devise,以防止增长到你无法控制的代码。

这不是一个大问题的答案,而是一个特定的理论解决scheme:

  • 找出你想把大文件分割成子文件的地方。 在每一点上以特殊的格式发表意见。

  • 写一个相当简单的脚本,将这些文件拆分成子文件。 (也许特别的评论有embedded文件名,脚本可以使用作为如何拆分它的说明)。它应该保留评论作为分裂的一部分。

  • 运行脚本。 删除原始文件。

  • 当你需要从一个分支合并时,首先通过连接这些分块重新创build这个大文件,进行合并,然后重新分割。

另外,如果你想保存SCC文件的历史logging,我希望最好的办法是告诉你的源代码控制系统,单个文件是原始文件的副本。 然后它将保存该文件中保存的部分的历史logging,当然它也将logging大部分被“删除”。

分解它的一种方法没有太多的危险,就是要对所有的线路变化进行历史性的考察。 是否有某些function比其他function更稳定? 如果你愿意的话,热点的变化。

如果在几年内没有更改过某行,则可以将其移动到另一个文件,而不必担心。 我会看看最后一个修订版本注释的源代码,看看是否有任何function可以退出。

Wow, sounds great. I think explaining to your boss, that you need a lot of time to refactor the beast is worth a try. If he doesn't agree, quitting is an option.

Anyway, what I suggest is basically throwing out all the implementation and regrouping it into new modules, let's call those "global services". The "main module" would only forward to those services and ANY new code you write will use them instead of the "main module". This should be feasible in a reasonable amount of time (because it's mostly copy and paste), you don't break existing code and you can do it one maintenance version at a time. And if you still have any time left, you can spend it refactoring all old depending modules to also use the global services.

My sympathies – in my previous job I encountered a similar situation with a file that was several times larger than the one you have to deal with. Solution was:

  1. Write code to exhaustively test the function in the program in question. Sounds like you won't already have this in hand…
  2. Identify some code that can be abstracted out into a helper/utilities class. Need not be big, just something that is not truly part of your 'main' class.
  3. Refactor the code identified in 2. into a separate class.
  4. Rerun your tests to ensure nothing got broken.
  5. When you have time, goto 2. and repeat as required to make the code manageable.

The classes you build in step 3. iterations will likely grow to absorb more code that is appropriate to their newly-clear function.

I could also add:

0: buy Michael Feathers' book on working with legacy code

Unfortunately this type of work is all too common, but my experience is that there is great value in being able to make working but horrid code incrementally less horrid while keeping it working.

Consider ways to rewrite the entire application in a more sensible way. Maybe rewrite a small section of it as a prototype to see if your idea is feasible.

If you've identified a workable solution, refactor the application accordingly.

If all attempts to produce a more rational architecture fail, then at least you know the solution is probably in redefining the program's functionality.

My 0.05 eurocents:

Re-design the whole mess, split it into subsystems taking into account the technical and business requirements (=many parallel maintenance tracks with potentially different codebase for each, there is obviously a need for high modifiability, etc.).

When splitting into subsystems, analyze the places which have most changed and separate those from the unchanging parts. This should show you the trouble-spots. Separate the most changing parts to their own modules (eg dll) in such a way that the module API can be kept intact and you don't need to break BC all the time. This way you can deploy different versions of the module for different maintenance branches, if needed, while having the core unchanged.

The redesign will likely need to be a separate project, trying to do it to a moving target will not work.

As for the source code history, my opinion: forget it for the new code. But keep the history somewhere so you can check it, if needed. I bet you won't need it that much after the beginning.

You most likely need to get management buy-in for this project. You can argue perhaps with faster development time, less bugs, easier maintaining and less overall chaos. Something along the lines of "Proactively enable the future-proofness and maintenance viability of our critical software assets" 🙂

This is how I'd start to tackle the problem at least.

Start by adding comments to it. With reference to where functions are called and if you can move things around. This can get things moving. You really need to assess how brittle the code base it. Then move common bits of functionality together. Small changes at a time.

Another book you may find interesting/helpful is Refactoring .

Something I find useful to do (and I'm doing it now although not at the scale you face), is to extract methods as classes (method object refactoring). The methods that differ across your different versions will become different classes which can be injected into a common base to provide the different behaviour you need.

I found this sentence to be the most interesting part of your post:

> The file is used and actively changed in several (> 10) maintenance versions of our product and so it is really hard to refactor it

First, I would recommend that you use a source control system for developing these 10 + maintenance versions that supports branching.

Second, I would create ten branches (one for each of your maintenance versions).

I can feel you cringing already! But either your source control isn't working for your situation because of a lack of features, or it's not being used correctly.

Now to the branch you work on – refactor it as you see fit, safe in the knowledge that you'll not upset the other nine branches of your product.

I would be a bit concerned that you have so much in your main() function.

In any projects I write, I would use main() only perform initialization of core objects – like a simulation or application object – these classes is where the real work should go on.

I would also initialize an application logging object in main for use globally throughout the program.

Finally, in main I also add leak detection code in preprocessor blocks that ensure it's only enabled in DEBUG builds. This is all I would add to main(). Main() should be short!

You say that

> The file basically contains the "main class" (main internal work dispatching and coordination) of our program

It sounds like these two tasks could be split into two separate objects – a co-ordinator and a work dispatcher.

When you split these up, you may mess up your "SCC workflow", but it sounds like adhering stringently to your SCC workflow is causing software maintenance problems. Ditch it, now and don't look back, because as soon as you fix it, you'll begin to sleep easy.

If you're not able to make the decision, fight tooth and nail with your manager for it – your application needs to be refactored – and badly by the sounds of it! Don't take no for an answer!

As you've described it, the main issue is diffing pre-split vs post-split, merging in bug fixes etc.. Tool around it. It won't take that long to hardcode a script in Perl, Ruby, etc. to rip out most of the noise from diffing pre-split against a concatenation of post-split. Do whatever's easiest in terms of handling noise:

  • remove certain lines pre/during concatenation (eg include guards)
  • remove other stuff from the diff output if necessary

You could even make it so whenever there's a checkin, the concatenation runs and you've got something prepared to diff against the single-file versions.

  1. Do not ever touch this file and the code again!
  2. Treat is like something you are stuck with. Start writing adapters for the functionality encoded there.
  3. Write new code in different units and talk only to adapters which encapsulate the functionality of the monster.
  4. … if only one of the above is not possible, quit the job and get you a new one.

"The file basically contains the "main class" (main internal work dispatching and coordination) of our program, so every time a feature is added, it also affects this file and every time it grows."

If that big SWITCH (which I think there is) becomes the main maintenance problem, you could refactor it to use dictionary and the Command pattern and remove all switch logic from the existing code to the loader, which populates that map, ie:

  // declaration std::map<ID, ICommand*> dispatchTable; ... // populating using some loader dispatchTable[id] = concreteCommand; ... // using dispatchTable[id]->Execute(); 

I think the easiest way to track the history of source when splitting a file would be something like this:

  1. Make copies of the original source code, using whatever history-preserving copy commands your SCM system provides. You'll probably need to submit at this point, but there's no need yet to tell your build system about the new files, so that should be ok.
  2. Delete code from these copies. That should not break the history for the lines you keep.

I think what I would do in this situation is bit the bullet and:

  1. Figure out how I wanted to split the file up (based on the current development version)
  2. Put an administrative lock on the file ("Nobody touch mainmodule.cpp after 5pm Friday!!!"
  3. Spend your long weekend applying that change to the >10 maintenance versions (from oldest to newest), up to and including the current version.
  4. Delete mainmodule.cpp from all supported versions of the software. It's a new Age – there is no more mainmodule.cpp.
  5. Convince Management that you shouldn't be supporting more than one maintenance version of the software (at least without a big $$$ support contract). If each of your customers have their own unique version…. yeeeeeshhhh. I'd be adding compiler directives rather than trying to maintain 10+ forks.

Tracking old changes to the file is simply solved by your first check-in comment saying something like "split from mainmodule.cpp". If you need to go back to something recent, most people will remember the change, if it's 2 year from now, the comment will tell them where to look. Of course, how valuable will it be to go back more than 2 years to look at who changed the code and why?