如何在 C 语言中进行模块化设计



我想使我的项目更加模块化,以便在删除其中一个模块时没有模块之间的依赖关系。

例如,如果我将进程中的代码划分为多个目录,例如 X、Y 和 Z,以便 X 中的数据结构不应由 Y 和 Z 中的数据结构直接访问,反之亦然,那么我需要 X、Y 和 Z 之间的一些内部通信机制。

由于我用 C 编码,任何人都可以建议一个示例项目或设计注意事项吗?

这通常归结为 API 设计。我觉得有帮助的几件事:

  • 请记住,您的 API 位于头文件中。实现在 C 文件中。
  • 避免全局变量 - 必要时使用访问器方法
  • 尽可能避免结构共享
  • 使用回调函数减少耦合

libfoo.h

int (*libfoo_callback)(void *arg, const char *name, int id);
/**
 * Iterate over all known foobars in the system.
 */
int libfoo_iterate_foobars(libfoo_callback cb, void *arg);

libfoo.c

#include "libfoo.h"
/* Private to libfoo.c */
struct foobar {
    struct foobar *next;
    const char *name;
    int id;
};
/* Don't make this globally visible */
static struct foobar *m_foobars;
int libfoo_iterate_foobars(libfoo_callback cb, void *arg)
{
    struct foobar *f;
    for (f = m_foobars; f != NULL; f = f->next) {
        int rc = cb(f->name, f->id);
        if (rc <= 0)
            return rc;   /* Stop iterating */
    }
    return 0;
}

some_consumer.c

#include <stdio.h>
#include "libfoo.h"
struct cbinfo {
    int count;
};
static int test_callback(void *arg, const char* name, int id)
{
    struct cbinfo *info = arg;
    printf("    foobar %d: id=%d name=%sn", info->count++, id, name);
    return 1;   /* keep iterating */
}
void test(void)
{
    struct cbinfo info = { 0 };
    printf("All foobars in the system:n");
    libfoo_iterate_foobars(test_callback, &info);
    printf("Total: %dn", info.count);
}

在这里,我展示了一个跟踪一些foobars的libfoo。在这个例子中,我们有一个消费者,他只想显示所有foobars的列表。此设计的优点:

  • 没有全局可见的变量:除了libfoo之外,没有人可以直接修改 foobar 列表。他们只能以公共 API 允许的方式使用 libfoo。

  • 通过使用回调迭代器方法,我使消费者不必知道foobar是如何被跟踪的。今天是struct foobar列表,也许明天是SQLite数据库。通过隐藏结构定义,消费者只需要知道 foobar 有nameid


实现真正的模块化,您将需要两件大事:

  1. 一组 API,用于定义模块如何生成和使用数据
  2. 一种在运行时实际加载模块的方法

具体细节将根据您的目标平台、模块化需求、预算等而有很大差异。

对于#1,您通常有一个模块注册系统,其中某些组件跟踪加载模块的列表,以及有关生产和消耗的元信息。

如果模块可以调用其他模块提供的代码,则需要一种方法来使其可见。这也将发挥作用 2.以 Linux 内核为例 - 它支持可加载的内核模块,以便在内核中添加新功能、驱动程序等,而无需将其全部编译成一个大型二进制文件。模块可以使用EXPORT_SYMBOL来指示特定符号(即函数)可供其他模块调用。 内核跟踪加载了哪些模块、它们导出了哪些函数以及位于哪些地址。

对于#2,您可以利用操作系统的共享库支持。在Linux和其他Unices上,这些动态库是ELF(.so文件),由动态加载程序加载到进程的地址空间中。 在 Windows 上,这些是 DLL。 通常,此加载会在进程启动时自动为您处理。但是,应用程序可以利用动态加载程序显式加载其选择的其他模块。在POSIX上,你会调用dlopen(),而在Windows上你会使用LoadLibrary()。任一函数都会向您返回某种句柄,这将允许您对模块进行进一步的查询或请求。

然后,可能需要模块(根据设计)导出codingfreak_init函数,该函数在首次加载模块时由应用程序调用。然后,此函数将对框架进行其他调用,或返回数据以指示它需要和提供哪些设施。

这些都是非常笼统的信息,应该让你的车轮转动。

我会在

你开始编码之前设置一个"公共"API。然后,仅使用每个模块外部的该 API 进行编码。不要作弊;仅使用公共 API(尽管 API 可以根据需要发展)。它可以帮助尽可能多地将数据结构视为面向对象语言中的对象,并将公共 API 视为对象的方法。尽可能避免直接在模块外部使用内部数据结构字段;尽管如果它们是 API 的一部分,则返回定义良好的数据结构是可以的。只是不要直接在它们来自的模块之外修改它们。如果你花大量的时间预先设计接口,你可以做一个非常可维护的项目。

最新更新