本帖最后由 sedatefire 于 2013-7-28 16:45 编辑
计算机系毕业的人,大概都会有一门专业课,曰为《软件工程》,当年我不仅上了一次中文版的,后来学校搞什么中英教学,又上了一个学期的英文版的。尽管上了两遍,但我回忆起来当年的教学内容。第一个闪念,却是“大锤测试”,纯属是因为名字让人印象深刻而已。
当年的我,学习也算认真的,上了两遍的同一门课程,也是隐隐约约找到感觉了。什么需求分析啦、瀑布开发模型啦、项目管理啦、小组成员激励啦。当时正处大三下学期,正开始憧憬着未来的就业生活。当时那个信心爆棚啊,只想着出社会后,在软件公司即使领着一帮牛鬼蛇神,也能够如何如何叱咤风云来着。却不曾想,毕业后的第一年,只是查bug查得狗跳**飞而已。总而言之,在这样美好的愿景下,我对这门专业课还是比较上心的。
如今工作多年后,独立完成的案子也不少,却发现软件工程里面讲的这些东西,恐怕对于90%的国内公司而言,基本形同虚设。哎呀呀,偏题啦偏题啦。今日此篇,说说软件分层的事儿。这只是软件工程里面一个小小的分支,而它的理念也基本普及到每个软件工程师的心中。但老实说,做得好的人还不多。(哎,这话该不该讲我还犹豫挺久,说不定是自己公司太弱了,没见世面呢,但不管怎样,语不惊人死不休的目的是达到了)。
老外的东西,直译过来,说什么高内聚、低耦合,对于没有一定构建系统经验的大学生来说,无亦于催眠良药。哎,再赞一个加拿大,先工作再上学,再工作再上学,随时可进修的教育制度。
我先用一个比喻来阐述一下软件分层的概念,然后再来讲述常见的错误,看看您躺枪了没?
好吧,直接一点,先说软件模块吧,它就是一个插座,接口就是插孔。
什么是软件模块呢? 说白了,你可以理解成一个独立的xxxx.c档和相应的xxxx.h档。.c档里面的东西是经过精挑细选的,主题明确的几个函数和一些变量等等汇集而成。.h档呢,里面放的也是精挑细选的函数声明,供别人调用。这个别人,软件模块叫做“上层”,它还说如果你是第n层,那么调用者就是第n+1层,你就是它的下层。下层不可依赖上层噢。这些函数,就是传说中的接口。
在这里我急不可耐的要发第一枪: 许多工程师习惯把全局变量extern出来放到.h里面,这里我要很明确的不怕得罪人地表达: 我鄙视这种不成熟的做法。这种感觉如同把插座里面的导线裸露在机壳外一般。也许有人说,为了压缩代码空间,为了提高执行时间。但,我还是表示更加强烈的鄙视:时空效率的提高可不是这么干的。用这种做法压缩时空只是战术上的,小道尔,整个框架的修改调整方为正道。还有些工程师,习惯把只有a.c档会用到的define常量,结构体,IO定义,enum等,放到a.h里面。最郁闷的是,所有的x.c xx.c xxx.c都包含有一个all_include.h文档,而a.h就包含在在这个无所不包的all_include.h里面。这感觉,这感觉,就像所有的隐私都泄露在外,家丑,还有各种卡密码...
回过头来再说插座吧,插孔(接口)许多人还是整理得不错的。可是插座还有一个非常重要的组成部分------插头,如果是一个转换插座的话。更常见的是一个一米五左右的220V连接线,接往另外一个“插座”。在软件上说白了,就是你这个module.c档里面的代码调用了哪些外部函数,所以包含了哪些.h档,没有了它们,你这个module.c就无法编译过了。我们通常关心对上层提供了什么接口,却常常忽略自己依赖了哪些外部接口。这部分,只有在软件移植,特别是重大的平台移植时,才能体现出来它的重要性。
平台的移植,我们无法做到拔营即走,攻城略地。在51时代,我们无可奈何(源于静态栈),只能放任它们。但如今m3的时代来临,我们可以将所有的这些对下一层的依赖打包起来,整一整,对下层的依赖,可以整得像插头一般爽利。
针对插头这件事,我要发一个组合枪,装的是霰弹,您可接好了。
第二枪:你的设备驱动程序移植起来是不是巨艰难,因为它的插头不利索。举例说明如下:
1.裸露的寄存器名到处都是,比如p1^x = 0, tcnt = xxx, 这一部分最好用define打包在一起放在文件头。
2.违法依赖了“上一层”的模块内容。
在设备初始化函数中,你是否会直接访问系统参数,不管是用全局变量也好还是函数调用。以lcd初始化为例。
lcd_init(void)
{
lcd_contrast_set (sys_para.lcd_contrast); /* 我最鄙视的全局变量风 */
or
lcd_contrast_set ( get_sys_para_lcd_contrast() ); /* 我次鄙视的层次不分明的风格 */
}
这个事儿,比较恰当的做法是。
lcd_init (uint_fast8_t contast)
{
/* 接下来你懂的 */
}
3.没有保持设备驱动的纯洁性,这个同样属于层次不分明的做法。以buzzer为例。
/*
input: 0,长哔~~ 1,哔 2,哔哔 3,哔哔哔
*/
void buzzer_ctrl (uint_fast8_t para)
{
if (“应用层”状态 == xxx)
{
return;
}
if (其他不相干的设备状态 == xxx)
{
return;
}
}
这就他妈的坑爹,明明按合约说好了,我传入参数1,就叫一声。你说,你说,为何不叫,为何....
为了获取其他模块的状态,你不得不在buzzer.c包含无关的.h档在里面。这就是污染,玷污了设备的纯洁。
这事儿也简单,就是把if拉到外面判断。你可能要说,那我要写好多的if语句啊,浪费程序空间。哈哈,你就等着再中枪吧,希望你像红军一样可以中枪不倒,继续冲锋,或者发表一大段的临死表白。
存个草稿,未完待续
插孔 ----- 对上层接口函数,放.h档,基本的要有,init, read/recv, write/send,其他的可以尽量的丰富多彩。放心,不调用就不链接(得懂链接器原理和操作方式噢),不浪费空间。
插座座体 ----- 主题明确的软件模块,
插头 ----- 对下层的依赖,常常被忽略的地方,导致移植困难。m3/m0时代的到来,这一切终将改变,但对于函数指针的使用得有一定的造诣。
第一枪: 许多工程师习惯把全局变量extern出来放到.h里面,这里我要很明确的不怕得罪人地表达: 我鄙视这种不成熟的做法。
第二枪: 你的设备驱动程序移植起来是不是巨艰难,因为它的插头不利索。详见上述的举例说明。
我的其他系列观点,顺便来新手园地逛逛吧
写给在路上的新手---研发之声系列汇集
https://bbs.21ic.com/forum.php?mod=viewthread&tid=576478&fromuid=567930
|