今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。

我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。

层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。

其次,就是越层的模块联系。当模块 A 是模块 B 的上层,而模块 B 又是模块 C 的上层,这个时候,让模块 C 对模块 A 可见,在模块 A 中有对 C 导出接口的直接调用,对于清晰的设计是很忌讳的一件事。虽然,我们很难完全避免这个问题,去让 A 对 C 的调用完全通过 B 。但通常应尽力为之。(注:以后写书的话,我争取补充一些实际的例子来说明)不过,对语言不原生支持的数据类型,以及基础设施,但却有必要创造出来给系统用的。可以有些例外。比如内存管理,log 管理,字符串(C 语言用原始库函数管理比较麻烦)等等,我们可能以基础模块的形式提供。但却可能被不同层次的模块直接使用。但,上到一定层次后,还是需要去隐藏它们的。

下面来一点更实际的分析。

以 C 语言为例,由于 C 语言缺乏 namespace 的原生支持,我们通常给 api 加上统一前缀来区分。这倒也不麻烦。

那么模块 A 看起来就是一堆 'A_xxxxx' 为名字的方法。我个人主张单个模块不宜过大,在实现时适合放在同一个 .c 文件里即可。通常,一个模块会围绕一类对象处理。这些对象可以用整数 handle 来表示,也可以用一个特定类型的对象指针。两种方案各有千秋。先来谈对象指针的方案。

一个模块 A 的接口描述文件很可以是这样的(希望以后能补上更现实的代码):

 #ifndef _A_h
#define _A_h struct A;
struct B; struct A* A_create(void);
void A_release(struct A *self);
void A_bind(struct A *self , struct B *b);
void A_commit(struct A *self);
void A_update(void); int A_init(void); #endif

这里,我们定义了 A 这种数据类型。我个人反对用 typedef 或宏来减少代码输入。除非有特别的理由,都写上 struct 前缀,而不是定义出新类型。尤其是在较底层的模块设计时更是如此。在接口描述时,struct A 的细节是绝对不应该暴露出来的,它的数据结构应该仅存在于实现的文件 a.c 中。

关于 A 的接口通常分两类,一类是对 struct A* 做一些处理的,那么就让第一个参数传入 self 指针。这相当于 C++ 的 this 指针。比如上例中的 A_commit ;另一类接近于 C++ 类的静态成员函数,通常用于对这一类对象全部做一个处理,如 A_update 。

注:我无意用 C 去模拟 C++ ,但基于一类数据类型做一些处理的方法,对于 C ,这样的写法也是一个常规的范式而已。至于面向对象等在构建复杂系统时常用到的方法,以后我会谈谈我自己常用的另一些范式。或许像 C++ ,也可以不像。怎么写更好,是个见任见智的问题。不用过于拘泥。

这里的例子中,我们还提到了另一个数据类型 B 。显然,它是放在 B 模块中的。

我们通常不会在 a.h 中去 include b.h ,而只是声明一下 struct B 。(对于 C 语言来说,这并不必要,但写上是个好习惯)。这是因为,如果 B 是位于 A 之下的模块,既在 A 模块的实现中,会用到 B 的方法,我们通常不会让用到 A 模块的人,可以看见 B 的接口。包含 a.h 的同时隐式包含 b.h 就是不必要的了。

从范例代码中,我们可以猜想,struct A 是对 struct B 的某种封装,可以通过对 A 的操作,间接操作到其中的 B 类型。在 A 的模块初始化 A_init 中一定就会初始化 B 了。如果是这样,B 的层次就位于 A 之下。

往往 struct B 中还会保留一个 struct A 类型的引用。首先,我们应该尽力避免这种情况。即:位于下层的 B 应该对上层的 A 一无所知是最好的。如果在 B 模块中必须出现 struct A,那么我们应该至少保证,仅仅是 struct A * ,一个引用,而绝对不能出现任何对 A 模块内接口的调用。不要认为使用巧妙的方法,绕过循环依赖初始化问题就够了。这应该是一个设计原则,不要去违反。

btw, 草率的接口设计往往是日后系统脆弱的根源。图一时之快,随意暴露一些接口,或是自以为聪明的用一些“巧妙”的方法,甚至是语法糖来绕过设计原则,都是很危险的。

一个常见的难处理的问题是:如果 struct A 和 struct B 相互有双向引用。怎样建立这个引用关系?这个建立的过程,到底是 A 的方法,还是 B 的方法?我的答案是,谁在上层,就是谁的方法。

但是 A 和 B 相互都看不见内部数据布局的细节,让 B 的内部对 A 类型做一个引用,比如也需要从 B 模块中暴露一个接口出来。这个接口,可能仅供 A 使用。在这个例子里,就是仅供 A_bind 这个方法去使用。

如果是 C++ ,我们或许会采用 friend 。也可能使用其它一些技巧。反正 C++ 里可以挖掘的语法太多了。但 C 怎么办?下面给个我自己的方案。

原本,我们在 B 中导出的 api 是这样的:

void B_set_A(struct B *self,struct A * a);

现在写成:

struct i_A;

void B_set_A(struct B *self,struct i_A *a);

在 b.c 的实现中,加一个函数用于 struct i_A * 到 struct A * 的转换。

static inline struct A * A(struct i_A *a) { return (struct A *)a; }

然后在 a.c 的实现中,加一个类似函数用于转换 struct A * 到 struct i_A * 。

这样,在 a.c 之外,其它模块因为不能得到任何 struct i_A 类型,而不会错误的使用 B_set_A 这个接口了。

原文链接:http://blog.codingnow.com/2010/01/modularization_in_c_1.html

浅谈 C 语言中模块化设计的范式的更多相关文章

  1. 浅谈C语言中的强符号、弱符号、强引用和弱引用

    摘自http://www.jb51.net/article/56924.htm 浅谈C语言中的强符号.弱符号.强引用和弱引用 投稿:hebedich 字体:[增加 减小] 类型:转载 时间:2014- ...

  2. 浅谈Abp vNext的模块化设计

    abp的模块化给我留下深刻的印象,模块化不是什么新概念,大家都习以为常,但是为什么要模块化,模块化的意义或者说目的是什么?也许我们思考得并不深入.难得的是abp不仅完美的阐述了模块化概念,而且把模块化 ...

  3. 浅谈c语言中的堆

    操作系统堆管理器管理: 堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配. 大块内存: 堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放. 程序手动申请&释放 ...

  4. 浅谈C语言中的联合体

    联合体union 当多个数据须要共享内存或者多个数据每次仅仅取其一时.能够利用联合体(union).在C Programming Language 一书中对于联合体是这么描写叙述的: 1)联合体是一个 ...

  5. 浅谈zygote服务中的设计思路

    zygote服务是Android启动和服务APK的核心服务,每个APK都是通过zygote启动,今日阅读它的源码学习到一个不错的设计思路. 首先看看一个APK通过zygote的启动流程: 按照一般的设 ...

  6. 浅谈C#语言中的各种数据类型,与数据类型之间的转换

    什么是数据类型? 数据类型,百度百科是这样解释的:数据类型在数据结构中的定义是一个值的集合以及定义在这个值集上的一组操作.这样的解释对于一个初学者来说未必太过于深奥. 简单点说,数据类型就是不同长度的 ...

  7. 浅谈C语言中的强符号、弱符号、强引用和弱引用【转】

    转自:http://www.jb51.net/article/56924.htm 首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引 ...

  8. 浅谈C语言中结构体的初始化

    转自:http://www.jb51.net/article/37246.htm <代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始 ...

  9. 浅谈关于QT中Webkit内核浏览器

    关于QT中Webkit内核浏览器是本文要介绍的内容,主要是来学习QT中webkit中浏览器的使用.提起WebKit,大家自然而然地想到浏览器.作为浏览器内部的主要构件,WebKit的主要工作是渲染.给 ...

随机推荐

  1. 【转】Geometry cannot have Z values

    http://blog.csdn.net/tweeenty/article/details/44246407 在对矢量要素类添加要素,进行赋几何信息时(FeatureBuffer.Shape = IG ...

  2. HTTP Analyzer(实时分析HTTP/HTTPS数据流)

    简述 HTTP Analyzer是一款实时分析HTTP/HTTPS数据流的工具.它可以实时捕捉HTTP/HTTPS协议数据,可以显示许多信息(包括:文件头.内容.Cookie.查询字符窜.提交的数据. ...

  3. lower_bound与upper_bound

    昨天一道题目用了lower_bound,大致了解了lower_bound指的是第一个>=x的位置.但是之前对于upper_bound有误解,其实upper_bound指的是第一个>x的位置 ...

  4. Sql_Server中怎样推断表中某列是否存在

    /*推断表AA中是否存在AA_ID这一列.假设不存在,则新增*/ IF NOT EXISTS (SELECT 1 FROM syscolumns INNER JOIN sysobjects ON sy ...

  5. Dynamics CRM2013 Form利用window.location.reload()进行全局刷新带来的问题及解决的方法

    CRM2013以后.表单的保存后变成了局部刷新而非全局刷新,但非常多情况下我们须要刷新整个页面.通过刷新页面来使脚本运行或者业务规则运行来实现某些业务效果,一般我们会使用window.location ...

  6. Import Example Dataset

    Overview The examples in this guide use the restaurants collection in the test database. The followi ...

  7. Format operator

    The argument of write has to be a string, so if we want to put other values in a file, we have to co ...

  8. jquery批量绑定click事件

    jquery批量绑定click事件: var selects = $(".public_select dd ul li"); debugger; /*$(".public ...

  9. AS3 常见问题

    SharedObject 不起作用(exe, air中) var so:SharedObject = SharedObject.getLocal("aa", "/&quo ...

  10. TabHost的自定义

    使用自定义的TabHost可以不用继承TabActicity,但是要注意的是如果使用Activity作为Content的话,有两处代码是一定要加的.不然就会出现RuntimeError,还有在XML布 ...