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

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

该文件在我们产品的几个(> 10)维护版本中被使用和积极更改,因此很难重构它。如果我“简单地”将其拆分为3个文件,那么从维护版本合并回更改将成为一场噩梦。而且,如果您拆分具有如此长而丰富历史的文件,跟踪和检查SCC历史中的旧更改突然变得非常困难。

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

在这种情况下你会怎么做?关于如何在不打乱SCC工作流程的情况下将新特性移动到单独的源文件中,您有什么想法吗?

(注意:我们使用c++和Visual Studio;我们使用AccuRev作为SCC,但我认为SCC的类型在这里并不重要;我们使用Araxis Merge来做实际的文件比较和合并)


当前回答

我发现有一件事很有用(我现在正在做,尽管没有达到你所面临的规模),那就是将方法提取为类(方法对象重构)。不同版本中不同的方法将成为不同的类,这些类可以被注入到公共库中,以提供您所需的不同行为。

其他回答

让我猜猜:10个拥有不同功能集的客户和一个提倡“定制化”的销售经理?我以前做过这样的产品。我们遇到了同样的问题。

您认识到拥有一个巨大的文件是很麻烦的,但更麻烦的是您必须保持10个版本的“最新”。这是多重维护。SCC可以使这更容易,但它不能使它正确。

Before you try to break the file into parts, you need to bring the ten branches back in sync with each other so that you can see and shape all the code at once. You can do this one branch at a time, testing both branches against the same main code file. To enforce the custom behavior, you can use #ifdef and friends, but it's better as much as possible to use ordinary if/else against defined constants. This way, your compiler will verify all types and most probably eliminate "dead" object code anyway. (You may want to turn off the warning about dead code, though.)

一旦所有分支隐式地共享了该文件的一个版本,那么就更容易开始使用传统的重构方法。

#ifdefs主要适用于受影响的代码只在其他分支自定义上下文中有意义的部分。有人可能会说,这也为相同的分支合并方案提供了机会,但不要太疯狂。一次只做一个大项目。

In the short run, the file will appear to grow. This is OK. What you're doing is bringing things together that need to be together. Afterwards, you'll begin to see areas that are clearly the same regardless of version; these can be left alone or refactored at will. Other areas will clearly differ depending on the version. You have a number of options in this case. One method is to delegate the differences to per-version strategy objects. Another is to derive client versions from a common abstract class. But none of these transformations are possible as long as you have ten "tips" of development in different branches.

首先向它添加注释。参考函数被调用的位置以及是否可以移动物体。这可以让事情动起来。你真的需要评估它的代码基础有多脆弱。然后将公共功能转移到一起。一次做一些小改变。

所以从一开始重写产品代码的API是一个坏主意。需要做两件事。

首先,您需要让您的团队决定对该文件的当前生产版本进行代码冻结。

第二,您需要使用这个生产版本并创建一个分支,该分支使用预处理指令来管理构建,以分割大文件。使用JUST预处理器指令(#ifdefs, #includes, #endifs)拆分编译比重新编码API更容易。对于您的sla和持续的支持来说,这绝对更容易。

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

OR

一种更耗时但健壮的方法是设计一个内部依赖关系结构,在包含内容的方式上具有双重间接性。这将允许您分割内容,并仍然照顾到共同依赖关系。注意,这种方法需要位置编码,因此应该加上适当的注释。

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

基本结构是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文件的主要结构将在那里计算子组件中的依赖关系,如下所示:

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

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

当然,我使用数字,但你应该使用一些更符合逻辑的基于你的代码。

使用预处理器可以让你把事情分开,而不必担心API的变化,这在生产中是一个噩梦。

一旦你确定了产品,你就可以开始重新设计了。

在我看来,您现在面临的是一堆代码异味。首先,主类似乎违反了开/闭原则。这听起来也像是承担了太多责任。因此,我认为代码比实际需要的更加脆弱。

虽然我可以理解您对重构后可跟踪性的担忧,但我认为该类相当难以维护和增强,而且您所做的任何更改都可能导致副作用。我假设这些的成本超过了重构类的成本。

在任何情况下,由于代码气味只会随着时间的推移而变得更糟,至少在某些时候,这些成本将超过重构的成本。根据你的描述,我认为你已经过了临界点。

重构应该分小步骤进行。如果可能的话,在重构任何东西之前添加自动测试来验证当前行为。然后挑选出独立功能的小区域,并将其提取为类型,以便委派职责。

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

这是一个困难而有趣的重构。

首先,将实现与接口分离。将这个巨大的文件转换为一个只转发调用和参数的空shell。这样,你可以创建责任有限的组件,而不会影响任何调用者(他们仍然调用巨大的文件/模块/类)。

为了做到这一点,您还需要寻找新的潜在组件的创建时间。根据构造函数的发送方式,在拥有所有参数之前,对参数进行堆叠可能非常棘手。

然后,您可以查找调用者并让他们调用您的组件。这是简单的部分。