开发环境设计是个简单,却又非常不简单的事,当然结果也就是天壤之别。
比如我曾经混过的一个外包项目,竟然需要每个开发人员除了要从SVN下载应用源码外,还要一份Nuclear操作系统副本。编译的时候,自然也就要先编译操作系统了。搞个bug,要忍受跟SVN磨叽一两个钟头,花半个钟将代码加入SourceInsight,修改几行代码后,再跟编译器折腾个把钟,然后再SVN……就这样,日复一日,年复一年。当然,现实没那么夸张,但我给那些开发人员做个了统计,证实每天花不少于2小时在编译、版本管理等。当然,必要的编译、版本管理等工作也是需要时间的,但绝不应该那么多。试想一下,源码缩减到只有几兆,数十兆,或是百来兆,那下载一次代码的时间则能在数分钟内完成;将操作系统,平台做成库文件提供,应用开发人员只编译应用部分并链接库文件,则几分钟就能做完一次版本编译。如此一来,节约下来的时间相对客观。更重要的是,人的心情会变好~~
另一个直接受开发环境影响的,就是代码层次结构了。想一想,看到一个C, C++的源文件的开始是十几,数十行#include,外加几个编译选项宏的时候会不会疯掉。然后呢,Makefile里又是一堆-I路径。我最怕这个了,找个编译错误都能有经历千山万水的成就感。很多时候,得将些头文件,和头文件的内容进行挪移,甚至另起炉灶(这招太损,但既然都这么搞,也只能入乡随俗)。
https://files.cnblogs.com/files/hhao020/cshell_prj.re0.001.rar
编译前:
1. vob/xxx_prj/
1.1............build/
1.2............cmd/
1.3............tools/
1.4............export/
1.5............cshell/
1.5.1.................export/
1.5.2.................inc/
1.6............userapp/
1.6.1.................export/
1.7.2.................inc/
编译后:
1. vob/xxx_prj/
1.0............bin/
1.0.1..............linux.i32
1.0.2..............linux.i64
1.1............build/
1.2............cmd/
1.3............tools/
1.4............export/
1.5............cshell/
1.5.1.................export/
1.5.2.................inc/
1.5.3.................bin/
1.5.3.1.....................linux.i32/
1.5.3.2.....................linux.i64/
1.6............userapp/
1.6.1.................export/
1.6.2.................inc/
1.6.3.................bin/
1.6.3.1.....................linux.i32/
1.6.3.2.....................linux.i64/
相对编译前,编译后的各级源码目录下多了个bin目录,里面包含多个子目标。通常来说,一个项目下会生成针对不同功能板卡的多个可执行文件,因此项目根目录应当包含多个userapp目录,而不同硬件、版本的板块,则在bin目录下使用不同的目标类型存放编译结果。
这里特别的东西,就是export目录,承担着开发环境设计的画龙点睛之重担。几乎肯定的,export极少被开发环境设计者认识。这是个很悲哀的事,一旦在项目开始时擦肩而过,几乎只能永远错过了。而随之带来的,就是项目开发成本和维护成本攀升,软件越做越复杂。我讲这个,时常被人反驳,代码多了,自然越来越复杂。
扯淡!你见过土豪因为钱多了而花钱烦恼么?我们做程序的没钱,代码就是财富,项目开展一段时间后,代码多了只应该使一切更容易,因为此时隐藏问题,目标更清晰,还有了更多可用的,可靠的组件,工具。
子目录export什么来头?它里面存放的,是可以供模块外引用的头文件,如宏,变量和函数的声明。与export对应的,是模块内头文件inc目录,存放的都是些模块内使用的头文件。如此一来,C语言的程序,从某种意义上就有了面向对象的public和private接口特征。外部模块是不能越过编译限制来访问inc的头文件的,这一点可以通过makefile来控制。版本管理者则可以在无需理解源码的前提下,通过编译来发现错误的引用,而他所要做的,就是确保makefile不被未授权的修改。我以前做的比较绝,版本编译前,强行重新生成各模块的makefile(从标准模块库复制)。这样一来,任何想突破规则的改动,都会被编译器抱怨。
开发环境设计,另一个要点是,避免大量的#include<xxx.h>。这个不是那么绝对,但如果项目是新的,就应该避免应用模块用;如果只是个新模块,则应该采取设置边界edger.c来隔绝。要做到避免系统头文件,项目根目录export应该提供至少两个组件:prj_type.h和sal_os.h。在我做到项目里,它们会被命名成zType_Def.h和salOS.h。zType_Def.h包含了一些基础类型的定义,如byte_t, word_t, dword_t, lword_t,平台特定的zMsg_t, zHandle_t,甚至一些方便变成的宏定义MAX,_STR等等。这里只有一个原则,就是一切都必须跟操作系统无关。salOS.h则是一些系统头文件提供内容的封装,比如memset被重新定义成zMemset,time被重新定义成zTime,等等。当然,salOS也是跟操作系统无关的,salOS里可以出现#include<xxx>引用系统头文件,但限定为通用的系统头文件。如ws_sock.h这类特定系统的头文件,则不可以暴露在salOS.h里面,必须在salNet.c等模块里进行封装后,在salOS.h中提供操作系统无关的接口函数。
zType_Def是必须强制被所欲应用程序引用的,salOS则根据应用的情况决定。但不管咋样,这两个文件都是软硬件无关的。换句话说,当我们开发一个给嵌入式目标板使用的程序时,有机会在Linux一类的系统上进行大部分功能的调试工作。当然,大多时候光有type和salOS是不够的,还需要个salHW。salHW是具体目标系统相关的,实现对真实硬件接口的一个横向封装,而在模拟环境下提供一套基本的硬件行为模拟。salOS和salHW所对应的实现,需要存放在不同的子目录。这样,通过makefile可以很容易的控制具体需要编译、链接哪个。
我相当反对应用程序开发者直接使用printf,malloc等一类系统函数,在一个稍微大一点点的项目,它们总是会带来灾难。比如,我刚开始做移动核心网时,出现了无法关闭的一两个打印。这是电信运营商眼里是绝不容许的,幸好,很快等到一个计划中的版本升级,否则那一年的无故障运行时间,就不可能达成5个9了。有了type和salOS等头文件,在应付打印,内存泄漏等问题,就容易多了(这点以后聊)。
说了半天,都是防范滥用头文件问题,但没说过如何防范头文件烂设计问题。确实,很多开发人员偷懒,把所有东西都扔到export目录,inc目录总是空空的。说实话,哥拿这类人真没什么办法,也只有空想者拿刀砍人!