我想使我的项目更加模块化,以便在删除其中一个模块时没有模块之间的依赖关系。
例如,如果我将进程中的代码划分为多个目录,例如 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 有name
和id
。
要实现真正的模块化,您将需要两件大事:
- 一组 API,用于定义模块如何生成和使用数据
- 一种在运行时实际加载模块的方法
具体细节将根据您的目标平台、模块化需求、预算等而有很大差异。
对于#1,您通常有一个模块注册系统,其中某些组件跟踪加载模块的列表,以及有关生产和消耗的元信息。
如果模块可以调用其他模块提供的代码,则需要一种方法来使其可见。这也将发挥作用 2.以 Linux 内核为例 - 它支持可加载的内核模块,以便在内核中添加新功能、驱动程序等,而无需将其全部编译成一个大型二进制文件。模块可以使用EXPORT_SYMBOL
来指示特定符号(即函数)可供其他模块调用。 内核跟踪加载了哪些模块、它们导出了哪些函数以及位于哪些地址。
对于#2,您可以利用操作系统的共享库支持。在Linux和其他Unices上,这些动态库是ELF(.so
文件),由动态加载程序加载到进程的地址空间中。 在 Windows 上,这些是 DLL。 通常,此加载会在进程启动时自动为您处理。但是,应用程序可以利用动态加载程序显式加载其选择的其他模块。在POSIX上,你会调用dlopen()
,而在Windows上你会使用LoadLibrary()
。任一函数都会向您返回某种句柄,这将允许您对模块进行进一步的查询或请求。
然后,可能需要模块(根据设计)导出codingfreak_init
函数,该函数在首次加载模块时由应用程序调用。然后,此函数将对框架进行其他调用,或返回数据以指示它需要和提供哪些设施。
这些都是非常笼统的信息,应该让你的车轮转动。
你开始编码之前设置一个"公共"API。然后,仅使用每个模块外部的该 API 进行编码。不要作弊;仅使用公共 API(尽管 API 可以根据需要发展)。它可以帮助尽可能多地将数据结构视为面向对象语言中的对象,并将公共 API 视为对象的方法。尽可能避免直接在模块外部使用内部数据结构字段;尽管如果它们是 API 的一部分,则返回定义良好的数据结构是可以的。只是不要直接在它们来自的模块之外修改它们。如果你花大量的时间预先设计接口,你可以做一个非常可维护的项目。