【发布时间】:2015-05-14 03:58:18
【问题描述】:
我想让我的项目更加模块化,以便在删除其中一个模块时不存在模块间依赖关系。
例如如果我将进程中的代码分成多个目录,例如 X、Y 和 Z,这样 X 中的数据结构不应该被 Y 和 Z 中的数据结构直接访问,反之亦然,那么我需要一些 X 之间的内部通信机制, Y 和 Z。
由于我使用 C 编码,任何人都可以建议一个示例项目或相同的设计注意事项吗?
【问题讨论】:
标签: c abstraction modularity
我想让我的项目更加模块化,以便在删除其中一个模块时不存在模块间依赖关系。
例如如果我将进程中的代码分成多个目录,例如 X、Y 和 Z,这样 X 中的数据结构不应该被 Y 和 Z 中的数据结构直接访问,反之亦然,那么我需要一些 X 之间的内部通信机制, Y 和 Z。
由于我使用 C 编码,任何人都可以建议一个示例项目或相同的设计注意事项吗?
【问题讨论】:
标签: c abstraction modularity
我会在您开始编码之前设置一个“公共”API。然后,从每个模块外部仅使用该 API 编写代码。不要作弊;仅使用公共 API(尽管 API 可以根据需要发展)。它可以帮助将数据结构视为面向对象语言中的对象,并将公共 API 视为对象的方法,尽可能多地。尽可能避免在模块外部直接使用内部数据结构字段;尽管返回定义良好的数据结构是可以的,如果它们是 API 的一部分。只是不要直接在它们来自的模块之外修改它们。如果您花大量时间预先设计界面,您可以制作一个非常易于维护的项目。
【讨论】:
这通常归结为 API 设计。我觉得有帮助的几件事:
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=%s\n", 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: %d\n", info.count);
}
这里我展示了一个跟踪一些 foobar 的 libfoo。在这个例子中,我们有一个消费者,他只想显示所有 foobar 的列表。这种设计的好处:
没有全局可见的变量:除了libfoo 之外没有人可以直接修改 foobars 列表。他们只能以公共 API 允许的方式使用 libfoo。
通过使用回调迭代器方法,我让消费者不必知道如何跟踪 foobar。今天它是struct foobar 的列表,也许明天它是一个 SQLite 数据库。通过隐藏结构定义,消费者只需要知道 foobar 有一个name 和一个id。
要真正模块化,你需要做两件大事:
具体情况会因您的目标平台、模块化需求、预算等而有很大差异。
对于#1,您通常会有一个模块注册系统,其中一些组件会跟踪已加载模块的列表,以及有关生产和消费内容的元信息。
如果模块可以调用其他模块提供的代码,您需要一种方法使其可见。这也将影响 2 的实现。以 Linux 内核为例 - 它支持loadable kernel modules 以便将新功能、驱动程序等添加到内核中,而无需将其全部编译成一个大的二进制文件。模块可以使用EXPORT_SYMBOL 表示特定符号(即函数)可供其他模块调用。内核会跟踪加载了哪些模块、它们导出了哪些函数以及位于哪些地址。
对于 #2,您可以利用操作系统的共享库支持。在 Linux 和其他 Unices 上,这些 dynamic libraries 是 ELF(.so 文件),它们由动态加载程序加载到进程的地址空间中。在 Windows 上,这些是 DLL。通常,当您的流程开始时,此加载会自动为您处理。但是,应用程序可以利用动态加载器显式加载其选择的其他模块。在 POSIX 上,您将调用 dlopen(),而在 Windows 上,您将使用 LoadLibrary()。任一函数都会向您返回某种类型的句柄,这将允许您对模块进行进一步的查询或请求。
然后您的模块可能需要(根据您的设计)导出 codingfreak_init 函数,该函数在模块首次加载时由您的应用程序调用。然后,此函数将对您的框架进行额外调用,或返回数据以指示它需要和提供的工具。
这些都是非常笼统的信息,应该会让你的车轮转动。
【讨论】: