我如何在C模块化设计?

时间:2015-03-12 05:07:22

标签: c abstraction modularity

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

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

由于我使用C编码,是否有人可以建议样本项目或设计注意事项?

2 个答案:

答案 0 :(得分:4)

这通常归结为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=%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);
}

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

  • 没有全局可见的变量:libfoo以外的任何人都无法直接修改foobar列表。它们只能以公共API允许的方式使用libfoo。

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


要成为真正的模块化,你需要两件大事:

  1. 一组定义模块如何生成和使用数据的API
  2. 在运行时实际加载模块的方法
  3. 具体情况因您的目标平台,模块化需求,预算等而异。

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

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

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

    然后可能需要您的模块(按照您的设计)导出codingfreak_init函数,该函数在首次加载模块时由您的应用程序调用。然后,此函数将对您的框架进行额外调用,或返回数据以指示它需要和提供的工具。

    这是非常一般的信息,应该让你的车轮转动。

答案 1 :(得分:0)

在您开始编码之前,我会设置一个“公共”API。然后,代码仅使用每个模块外部的API。不要作弊;仅使用公共API(尽管API可以根据需要发展)。它可以帮助尽可能多地处理面向对象语言中的对象之类的数据结构,以及像对象方法那样的公共API。尽可能避免在模块外直接使用内部数据结构字段;虽然如果它们是API的一部分,返回定义良好的数据结构是可以的。只是不要直接在它们来自的模块之外修改它们。如果您花费大量时间预先设计接口,则可以创建一个非常易于维护的项目。