有关C语言模块化实现的探讨

开发 架构 后端
怎样才是好的设计?模块化是我们经常谈论的一个方向,它将复杂的问题分解成小问题,实现高内聚,低耦合。不过,模块之间的联系需要遵循的原则,坚持起来是很难的。本文探讨了C语言实现模块化的一些问题。

本文节选自云风的博客上近日的两篇文章:《好的设计》和《C 语言对模块化支持的欠缺》。

由于最近几年用的主要开发语言是 C 和 lua 。那么也打算以此为基础写。假定读者至少有不错的 C 语言基础了。我真正想谈的是,如何把一个软件很好的构建起来。到底需要做些什么。(从实现层面看)怎样才是好的软件。

那么有一个重点问题,也是老问题,怎样才是好的设计。

好的设计,必然是容易实现的。它可以很精巧,但不能难以理解。

太阳底下无新鲜事。软件行业已经发展了这么多年,你想到的东西,肯定有人都想到过了。

每个软件也都有它的生命期,我们只要在它的生命期内完成它的使命就行了。软件往往需要尽快的投入使用,然后在使用中演化。这个演化最大可能并不依靠你一个人的力量去推动。随着参与的人增加,人和人(指开发人员)的共性就会减少。每个人都看得懂可以充分接受,软件才不容易向坏的方面演化。

我们常常谈模块化,谈高内聚,低耦合。

本质上,就是如何管理复杂度。如何把一件很难的事情(开发一个软件),分解成小问题,分而治之。

这些小问题之间的千丝万缕的联系,是设计人员面临的最大难题。

有些原则听起来不错,但是坚持起来很难。

比如,让模块的输入输出没有副作用。你能让你的模块每个输入对对应着唯一输出吗?

又比如,让模块层次化。如果 A 模块依赖 B 模块,B 模块依赖 C 模块。一旦出现这个状态,你能保证 A 模块绝对和 C 模块隔绝吗?更有甚者,让三个模块循环依赖这种更糟糕的事情也并不鲜见。

抽象是个好东西。但借助不断的抽象,问题不断的包起来,演化成新的巨无霸,显然会让事情更糟。虽然最终可能真的能像搭积木一样去组装软件了。或是雇佣更多的程序员填表单一样的工作,相互不需要对方在做什么。但是,软件性能却下降到了不可以忍受的地步。bug 也隐藏的更久,更不可收拾。

好的设计,必须对问题有足够清晰的理解。有如庖丁解牛一般,把整个问题划开,在最薄弱的地方分离。其实,做到这点,也就够了。

解决这些问题,其实跟语言无关。语言之争是没有多大意义的。如开头所说,把设计做好,模块之间的关系,用足够简单的方式就能描述清楚了,大部分流行的开发语言都能做到。

用 C 来实作,而没有用它的近亲 C++ ,也是为了避免狭隘的争议:我们该用这个特性吗?该用那个特性吗?这个形式做是不是好点?那样会不会有更好的性能?

所谓开发效率,对于个人来说,语言之不同,是会有很大差异。但是那是实现层面的差异。对于完成设计,这个过程,效率和所用语言无关。

实现的阶段,程序员可不可以开心的放心的去完成那些接口,这就是衡量设计好不好的指标了。这个时候,一个高开发效率的语言有优势(更少的代码量),一个容易掌握的语言也有优势(可以让更多的人参于而少犯错误)。

#t#对于我的团队,我会更乐于采用一种让实现人员更轻松的方式。不用理会太多的语言细节,不用在投入开发前学习更多的概念(尤其是这个项目独有的),不用特别严格的 code review 也可以允许大家提交新的代码,切不至于轻易的引入 bug 。

我相信,软件做到后面,设计人员不需要亲自写太多代码。虽然我现在每天还是大量的写,也并不觉得枯燥。

事必恭亲是不好,但并不是说,你给实现人员足够信任就可以放手的。真正让你放手的只能是,你做出了好的设计,无论是谁,他也写不坏它。这时,是你乐意自己写,还是多找几个同学帮忙写,已经不重要了。

#p#

那么我们来讨论一下怎样构建一个(稍具规模的)软件。我选择用 C 为实现工具来做这件事情。就不得不谈语言还没有提供给我们的东西。

模块化是最高原则之一(在 《Unix 编程艺术》一书中, Unix 哲学第一条即:模块原则),我们就当考虑如何简洁明快的使用 C 语言实现模块化。

除开 C/C++ ,在其它现在流行的开发语言中,缺少标准化的模块管理机制是很难想象的。但这也是 C 语言本身的设计哲学决定的:把尽可能多的可能性留给程序员。根据实际的系统,实际的需要去定制自己需要的东西。

对于巨型的系统(比如 Windows 这样的操作系统),一般会考虑使用一种二进制级的模块化方案。由模块自己提供元信息,或是使用统一的管理方案(比如注册表)。稍小一点的系统(我们通常开发接触到的),则会考虑轻量一些的源码级方案。

首先要考虑的往往是模块的依赖关系和初始化过程。

依赖关系可以放由链接器或加载器来解决。尤其在使用 C 语言时,简单的静态库或动态库,都不太会引起大的麻烦。

C++ 则不然,C++ 的某些特性(比如模板类静态成员的构造)必须对早期只供 C 语言使用的链接器做一些增强。即使是精心编写的 C++ 库,也有可能出现一些意外的 bug 。这些 bug 往往需要对编译,链接,加载过程很深刻的理解,才能查出来。注:我并不想以此来反对使用 C++ 做开发。

我们需要着重管理的,是模块的初始化过程。

对于打包在一起的一个库(例如 glibc ,或是 msvcrt ),会在加载时有初始化入口,以及卸载时有结束代码。我想说的不是这个,而是我们自己内部拆分的更小的模块的相互依赖关系。

谁先初始化,谁后初始化,这是一个问题。

在 C++ 的语言级解决方案中,使用的是单件模块。要么由链接器决定以怎样的次序来生成初始化代码,这,通常会因为依赖关系和实际构造次序不同而导致 bug (注:我在某几本 C++ 书中都见过,待核实。自己好久不写 C++ 也没有实际的错误例子);要么使用惰性初始化方案。这个惰性初始化也不是万能的,并且有些额外的开销。(多线程环境中尤其需要注意)

我使用 C 语言做初期设计的时候,采用的是一种足够简单的方法。就是,以编码规范来规定,每个模块必须存在一个初始化函数,有规范的名字。比如 foo 模块的初始化入口叫

  1. int foo_init()  
  2.  

#t#规定:凡使用特定模块,必须调用模块初始化函数。

为了避免模块重复初始化,初始化函数并不直接调用,而是间接的。类似这样: mod_using(foo_init);

mod_using 负责调用初始化函数,并保证不重复调用,也可以检查循环依赖。

在这里,我们还约定了初始化成功于否的返回值。(在我们的系统中,返回 0 表示正确,1 表示失败)然后定义了一个宏来做这个使用。

  1. #define USING(m) if (mod_using(m##_init,#m)) { return 1; }  
  2.  

注:我个人反对滥用宏。也尽可能的避免它。这里使用宏,经过了慎重的考虑。我希望可以有一个代码扫描器去判断我是否漏掉了模块初始化(可能我使用了一个模块,但忘记初始化它)。宏可以帮助代码扫描分析器更容易实现。而且,使用宏更像是对语言做的轻微且必要的扩展。

这样,我的系统中模块模块的实现代码最后,都有一个 init 函数,里面只是简单的调用了 USING 来引用别的模块。例如:

  1. #include "module.h"  
  2.  
  3. /*  
  4.   我个人偏爱把 module.h 的引入放在源文件最后,初始化入口之前。  
  5.   它里面之定义了 USING 宏,以及相关管理函数。  
  6.   这样做是为了避免在代码的其它地方去引入别的模块。  
  7. */ 
  8.  
  9. int 
  10. foo_init()  
  11. {  
  12.   USING(memory);  // 引用内存管理模块  
  13.   USING(log);  // 引用 log 模块  
  14.  
  15.   return 0;  
  16. }  
  17.  

至于模块的卸载,大部分需求下是不需要的。今天在这里就不论证这一点了。

责任编辑:yangsai 来源: 云风的Blog
相关推荐

2011-05-13 15:54:50

C模块化

2011-05-13 15:46:49

C模块化

2022-09-21 11:51:26

模块化应用

2016-12-14 14:50:26

CSS预处理语言模块化实践

2019-08-28 16:18:39

JavaScriptJS前端

2020-09-17 10:30:21

前端模块化组件

2020-09-18 09:02:32

前端模块化

2010-01-21 09:27:30

模块化的优点NetBeans

2021-04-06 10:19:36

Go语言基础技术

2009-08-17 10:11:12

C# Windows

2010-03-11 17:24:27

Python编程语言

2022-09-05 09:01:13

前端模块化

2016-10-09 11:03:41

Javascript模块化Web

2023-12-25 22:24:36

C++模块Module

2013-08-20 15:31:18

前端模块化

2017-05-18 10:23:55

模块化开发RequireJsJavascript

2015-10-10 11:29:45

Java模块化系统初探

2022-03-11 13:01:27

前端模块

2010-05-28 10:31:28

模块化IT

2023-05-24 10:35:11

Node.jsES模块
点赞
收藏

51CTO技术栈公众号