【问题标题】:How do I Modular Design in C?如何在 C 中进行模块化设计?
【发布时间】:2015-05-14 03:58:18
【问题描述】:

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

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

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

【问题讨论】:

    标签: c abstraction modularity


    【解决方案1】:

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

    【讨论】:

    • 感谢您的回答。但是,如果有某些信息必须共享?例如假设我已经将我的整体流程划分为多个子流程,其中流程 1 必须处理用户提供的数据,流程 2 必须处理用户提供的最终数据并生成最终输出,供流程使用3.在这种情况下,如果新的数据在 process-1 和 process-2 之间共享,则由用户提供。有时 process-1 必须从 process-2 获取数据的副本来验证用户提供的数据。
    【解决方案2】:

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

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

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

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


    真正模块化,你需要做两件大事:

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

    具体情况会因您的目标平台、模块化需求、预算等而有很大差异。

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

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

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

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

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

    【讨论】:

    • 感谢@Jonathon 的详细回复。甚至我也在考虑一种 API 方法,其中我们在 X、Y、Z 模块上有一个抽象层,并且模块之间的通信仅通过 API 发生。在这种情况下,维持模块间通信所需的元信息至关重要。
    • 对于回调方法,我有点想使用静态全局变量将状态或常量绑定到回调。这是不好的做法吗?
    • @user3528438 是的,这是一个坏主意,因为它会使您的代码不可重入。解决方案是通过调用回调的函数传递一个 void* 。我将更新答案以显示这一点。为了简单起见,我把它省略了。
    • @user3528438 我已经编辑了我的问题以显示将“上下文”传递给回调。
    猜你喜欢
    • 2014-01-26
    • 2016-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 1970-01-01
    相关资源
    最近更新 更多