将遗留的C代码(与广泛使用全局变量紧密结合)重构到共享库中



我正在开发一个用C编写的遗留系统。我正在将代码从几个大型模块重构为几个更小、逻辑独立的共享库。

问题是,现有的代码使这种划分变得困难,因为大型模块试图做太多。其他额外的挑战是:

  • 现有的代码是紧密耦合的:

    例如,一个应该实现集合结构(实际上是一个动态数组)的模块也有从文件、数据库、读取缓存数据等检索数据(以填充结构)的例程。

  • 代码大量使用全局变量,我不知道当我将代码划分到单独的共享库中时,这将如何工作(如果?)。

现有的标头看起来像这样:

/* DYN_ARRAY header */
DYN_ARRAY* DYN_ARRAY_Alloc(); 
void       DYN_ARRAY_Free(DYN_ARRAY *ptr); 
int        DYN_ARRAY_LoadFile(DYN_ARRAY *ptr, cont char* filename, FILE_STRUCT_INFO *info);
/* obvious dependency on database functionality */
int        DYN_ARRAY_LoadQueryResults(DYN_ARRAY *ptr, const char* sql);
/* This innocuous looking function calls a function which introduces
   a dependencies on another logically separate module

*/int DYN_ARAY_GetIdKeyValue(const DYN_ARARY*ptr,const int key_id);

我正在考虑将现有的DYN_ARAY模块划分为三个共享库,如下所示:

  1. dyn_array_core(取决于:无)
  2. dyn_array_db(取决于:dyn_array_core、db_utils…)
  3. dyn_array_misc(取决于:dyn_array_core、misc_utils…)

我的问题是:

  1. 这是一种明智的方法吗(或者有更好的方法来划分代码吗?)

  2. 分区后的代码会像以前一样工作吗(考虑到代码使用全局变量的事实,即每个dll都有自己的全局var副本吗?[如果是,那么显然这不是我想要的]-在这种情况下,我如何重构代码以像以前一样工作?)

如果您想进行更改以实现有用的东西,那么将代码移动到单独的共享对象中并没有多大作用。在我看来,更好的方法是首先重写代码,尽可能多地消除这些全局变量——构建一个或多个结构来保存上下文,并修改函数签名以包含上下文指针。一旦您到达那里,将代码分离为更清晰的模块(也许还有多个共享库)将为您提供更有用的输出。

首先,您可能需要创建一个新的头,其目的是定义前全局变量被重新定位到的结构。首先要注意的是,我们希望在不将代码移动到共享库的情况下进行所有重构——只有在验证初始重构成功后才能进行。(也就是说,不要一次引入超过必要数量的潜在故障点。)

#ifndef _FORMER_GLOBALS_H_
#define _FORMER_GLOBALS_H_
typedef struct GlobalContext {
} GlobalContext;
GlobalContext *CreateGlobalContext(); // a convenience function
#endif /* _FORMER_GLOBALS_H_ */

下一步是将要消除的所有全局变量移到此结构中。如果你知道所有全局变量都是在哪里定义的,那么任务就很容易了。。。只需将它们从源位置中删除,注意任何非零初始值,并将变量定义移动到结构中(无需初始化)。如果全局变量的当前位置没有很好地定义(它们散布在各处),那么这一步会更困难,但编译器工具可能能够帮助您在当前代码中找到全局变量。

接下来考虑CreateGlobalContext()函数。这个函数将分配一个上下文结构并对其进行初始化;如果没有非零初始化,则可以完全消除该函数。

GlobalContext *CreateGlobalContext()
{
    GlobalContext *context = malloc(sizeof(*context));
    memset(context, 0, sizeof(*context)); // initialize it to all zeros
    // if needed, initialize individual non-zero elements
    // context->some_non_zero = 1;
    return context;
}

既然你没有更多的全局变量(在你计划移动的代码模块中),并且有一种方法可以通过正确的初始化为它们创建存储,那么你现在有一堆无法编译的代码——你应该有未解析的引用或所有前一个全局变量的未知标识符。

任何访问已移动全局的函数都应该添加一个新参数

extern int global_a;
void FunctionToBeMoved(int a)
{
    global_a = a;
}
// the above old function becomes:
void FunctionToBeMoved(GlobalContext *context, int a)
{
    context->global_a = a;
}

完成这些转换后,您将需要修改每个更改后的函数的调用方式——它们只需传递当前正在接收的上下文(或者,如果由于缺乏直接的全局使用而将其忽略,则现在需要接收)。

这不是一项小工作量的工作,但将写得不好的代码转换成可读/可维护的代码通常是很大的。

最新更新