浅谈 C 语言中模块化设计的范式
今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。
我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。
层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。
其次,就是越层的模块联系。当模块 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 语言中模块化设计的范式的更多相关文章
- 浅谈C语言中的强符号、弱符号、强引用和弱引用
摘自http://www.jb51.net/article/56924.htm 浅谈C语言中的强符号.弱符号.强引用和弱引用 投稿:hebedich 字体:[增加 减小] 类型:转载 时间:2014- ...
- 浅谈Abp vNext的模块化设计
abp的模块化给我留下深刻的印象,模块化不是什么新概念,大家都习以为常,但是为什么要模块化,模块化的意义或者说目的是什么?也许我们思考得并不深入.难得的是abp不仅完美的阐述了模块化概念,而且把模块化 ...
- 浅谈c语言中的堆
操作系统堆管理器管理: 堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配. 大块内存: 堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放. 程序手动申请&释放 ...
- 浅谈C语言中的联合体
联合体union 当多个数据须要共享内存或者多个数据每次仅仅取其一时.能够利用联合体(union).在C Programming Language 一书中对于联合体是这么描写叙述的: 1)联合体是一个 ...
- 浅谈zygote服务中的设计思路
zygote服务是Android启动和服务APK的核心服务,每个APK都是通过zygote启动,今日阅读它的源码学习到一个不错的设计思路. 首先看看一个APK通过zygote的启动流程: 按照一般的设 ...
- 浅谈C#语言中的各种数据类型,与数据类型之间的转换
什么是数据类型? 数据类型,百度百科是这样解释的:数据类型在数据结构中的定义是一个值的集合以及定义在这个值集上的一组操作.这样的解释对于一个初学者来说未必太过于深奥. 简单点说,数据类型就是不同长度的 ...
- 浅谈C语言中的强符号、弱符号、强引用和弱引用【转】
转自:http://www.jb51.net/article/56924.htm 首先我表示很悲剧,在看<程序员的自我修养--链接.装载与库>之前我竟不知道C有强符号.弱符号.强引用和弱引 ...
- 浅谈C语言中结构体的初始化
转自:http://www.jb51.net/article/37246.htm <代码大全>建议在变量定义的时候进行初始化,但是很多人,特别是新人对结构体或者结构体数组定义是一般不会初始 ...
- 浅谈关于QT中Webkit内核浏览器
关于QT中Webkit内核浏览器是本文要介绍的内容,主要是来学习QT中webkit中浏览器的使用.提起WebKit,大家自然而然地想到浏览器.作为浏览器内部的主要构件,WebKit的主要工作是渲染.给 ...
随机推荐
- [Transducer] Create a Sequence Helper to Transduce Without Changing Collection Types
A frequent use case when transducing is to apply a transformation to items without changing the type ...
- CLion注冊码算法逆向分析实录(纯研究)
声明 CLion程序版权为jetBrains全部.注冊码授权为jetBrains及其付费用户全部,本篇仅仅从兴趣出发,研究其注冊码生成算法. 不会释出不论什么完整的源码. 网上查了下.已有注冊机,所以 ...
- Ubuntu 10.04 右上角网络管理图标消失的解决的方法
那个显示网络状态的那个图标.叫做:network-manager.假设是有线网络连接的话.是上下两个箭头,假设是无线网络的话.是一个发射信号的形状. sudo gedit /etc/Ne ...
- 《从零開始学Swift》学习笔记(Day 51)——扩展构造函数
创文章.欢迎转载.转载请注明:关东升的博客 扩展类型的时候,也能够加入新的构造函数.值类型与引用类型扩展有所差别.值类型包含了除类以外的其它类型.主要是枚举类型和结构体类型. 值类型扩展构造函数 扩展 ...
- cocos2d-iphone 动作
(1)CCMoveTo [CCMoveTo alloc]initWithDuration:<#(ccTime)#> position:<#(CGPoint)#> 參数说明 : ...
- 2014 百度之星 1003 题解 Xor Sum
Xor Sum Problem Description Zeus 和 Prometheus 做了一个游戏,Prometheus 给 Zeus 一个集合,集合中包括了N个正整数,随后 Prometheu ...
- python判断一个单词是否为有效的英文单词?——三种方法
For (much) more power and flexibility, use a dedicated spellchecking library like PyEnchant. There's ...
- ELK到底是什么?那么多公司用!__转载
Sina.饿了么.携程.华为.美团.freewheel.畅捷通 .新浪微博.大讲台.魅族.IBM...... 这些公司都在使用ELK!ELK!ELK! ELK竟然重复了三遍,是个什么? 一.ELK ...
- 我所理解的monad(1):半群(semigroup)与幺半群(monoid)
google到数学里定义的群(group): G为非空集合,如果在G上定义的二元运算 *,满足 (1)封闭性(Closure):对于任意a,b∈G,有a*b∈G (2)结合律(Associativit ...
- spring context对象
在 java 中, 常见的 Context 有很多, 像: ServletContext, ActionContext, ServletActionContext, ApplicationContex ...