“今天早上阳光明媚啊,”老C在教研室门口深呼吸。
“嗯,东花园显得很漂亮啊……”小P应声道,两人走过东花园,来到教研室,准备妥当后,坐在小P桌前。
“好,我们今天先不评论你的代码内容,而是重新来写这个代码。”老C不好意思说小P的代码太烂以至于无法评审,所以决定另起炉灶,“然后我们拿新写的这个版本与原来的版本做比较。”
“也好。”小P同意。
“这样,我们两个来一起写这个代码,这样更快一些。”老C道。
“好啊好啊。”
两人新建了一个名叫AppleGame的工程,然后老C添加了一个main.c的文件,“本来在这里应当使用配置管理工具的,但是因为简单起见,我们就土
法炼钢,采用拷贝的方法记录版本吧。”老C一边说一边在硬盘上建立了一个新目录,起名为AppleGame_V0.01,“名字也随便起了,不用使用
VBD等等复杂的规范,但是你千万注意这只是例子,以后千万不要随便学啊。”老C解释道,“至于什么配置管理和版本命名规范,我们以后再说。”
“槑……”小P有些晕,下意识的回答道。
“我们的做法在一般面试、笔试的时候可千万不要用,完全是大炮打蚊子……在这里出现只是为了演示,然而如果你熟悉了这样的开发过程,再反过来使用更直接的方法应付面试和笔试还是比较容易的,”老C又补充道,“大炮一响,黄金万两……有时打打大炮也是有价值的,起码熟悉了开炮过程,这样以后用大炮打黄金就驾轻就熟了……”
“……”小P决定无视老C的自言自语,心想人上了年纪就是有些罗嗦。
“好,第一个任务,写一个main函数。”
“这个简单。”小P应道。在文件上敲下几行代码。
void main()
{
}
“唉,这样是有问题滴……”老C说到。
“什么问题?”
“规范……”老C回答,“我们应当这样写。”老C开始更改小P的代码。
int main()
{
return 0;
}
int main(int argc, char* argv[])
{
return 0;
}
“我们两个中间选一个,因为这个程序没有命令支持,所以就第一个吧。”老C说到,“这个是C99的新标准,我们还是按照标准来吧。”老C补充道,“你可以
在学校图书馆查查《ISO/IEC 9899 :
1999》这个文档,里面说的很清楚。如果我们还使用原来的格式,可能在代码兼容性上会出问题,我们写出的代码就不能在所有编译器上编译通过……”
“是么?这么复杂……”小P有些疑惑,“好吧,那么就这样吧。”
“那么现在说说你解决这道题的思路吧。”老C问。
“嗯,先设计一个循环队列,每个队列中的元素表示一个小朋友,1表示在对列中,0表示他不在对列中,然后开始按照规则玩游戏,直到剩下最后一个小朋友,然后在队列中找出这个剩下的元素,打印它的下标……就是座位号码啦……”
“嗯,思路还算清晰,”老C评论,“我们姑且不论选择循环队列是否合适,就先按照这个思路来做,等到后面再评审更改吧。”然后他在文件中添加如下的语句。
int main()
{
/* Initialize the queue. */
/* Play the game, until the last one is found. */
/* Search the last one's seat number. */
return 0;
}
“好了,我们的第一版程序完成了。”老C拍拍手。
“完了?”小P有些不敢相信。
“是啊,”老C确定的说,“编程不只是写代码,代码 != 程序!当你开始进行思考的时候,就开始进行程序设计了,代码不过是程序的表达方式。如果人类的语言可以在计算机上执行,你刚才说的话,就是代码。是不是这样?”
“嗯,有些道理……”
“编译!好了,我们的第一版程序没有什么问题!”老C说完,将main.c文件拷贝到AppleGame_V0.01文件夹下面,然后又新建了一个AppleGame_V0.02的文件夹。
“下来我们需要一个调试宏,”老P说到,“本来可以使用IDE为我们准备的debug和release编译选项,但是这里我们先不用,为了明白背后的道理,我们完全自己打造一个先。关于debug和release,我们以后再说。”
“哦……”小P点点头。
老C在main.c的开头写下如下代码。
#include <stdio.h>
#define PRINT_DEBUG_INFO
#if defined(PRINT_DEBUG_INFO)
#define MY_DEBUG(str) printf(str)
#define MY_DEBUG_1(str, par) printf(str, par)
#else
#define MY_DEBUG(str)
#define MY_DEBUG_1(str, par)
#endif // PRINT_DEBUG_INFO
“这几个宏用于在调试的时候输出一些中间信息,”老C解释道,“如果我们想输出调试信息,只需要#define PRINT_DEBUG_INFO就可以了,否则就注释掉这个宏。这只是一些小技巧而已,没有什么神秘的。”
“是吗?嗯,我看看……”小P琢磨着代码。
“下来进行一些实质性的,”老C接着说,“但是之前我们要了解一个规则,用
问题域的词汇去编程,而不是解决域。”
“槑,什么叫问题域?解决域?”小P不解。
“我写你看好了。”老C说道,然后在main.c文件中接下来的部分写下如下内容。
//////////////////////////////////////////////////////////////////////////
//
#define CHILDREN_NUM 20U
typedef int SEAT_NUM;
typedef enum tagEXIST_STATE { ABSENT, EXISTED } EXIST_STATE;
typedef struct tagCHILD
{
SEAT_NUM seatNum_;
EXIST_STATE existState_;
}CHILD;
#define QUEUE_LENGTH CHILDREN_NUM
typedef CHILD QUEUE_CONTENT;
typedef struct tagQUEUE
{
int size_;
int index_;
QUEUE_CONTENT queue_[QUEUE_LENGTH];
}QUEUE;
typedef struct tagAPPLE_GAME
{
int currCountNum_;
int childrenRemained_;
QUEUE childrenQueue_;
}APPLE_GAME;
void InitAppleGame (APPLE_GAME* game);
int IsGameOver (APPLE_GAME* game);
void PlayGame (APPLE_GAME* game);
int LastChildSeatNum (APPLE_GAME* game);
//////////////////////////////////////////////////////////////////////////
//
int main()
{
APPLE_GAME theGame;
int num;
/* Initialize the game. */
InitAppleGame(&theGame);
/* Play the game, until the last child is found. */
while (!IsGameOver(&theGame))
{
PlayGame(&theGame);
}
/* Search the last child's seat number. */
num = LastChildSeatNum(&theGame);
printf("The last child's seat number is %d.\n", num);
return 0;
}
//////////////////////////////////////////////////////////////////////////
//
void InitAppleGame(APPLE_GAME* game)
{
MY_DEBUG("Init the apple game.\n");
}
int IsGameOver(APPLE_GAME* game)
{
static int n = -1;
MY_DEBUG("Only one child?\n");
++n;
return n;
}
void PlayGame(APPLE_GAME* game)
{
MY_DEBUG("Play game...\n");
}
int LastChildSeatNum(APPLE_GAME* game)
{
int n = 1;
MY_DEBUG("Searching last child's seat number\n");
return n;
}
“喏,就是这个意思,尽量用现实生活的语言对需要解决的问题进行描述,并将关系相近的变量用结构体放在一起。”老C说,“然后将对这些名词的操作写成可以
用现实生活语言表达的函数,并将结构体作为函数的入口参数传入函数中。”老C咽了一口唾沫,“咳咳,你再比较比较我们这两版的注释有什么变化?”
“叫我看看……”小P开始比较代码,“哦,在这个版本你用 game 代替了 queue,用 child 代替了 one, 但是有什么实质区别?”小P有些不解。
“嗯,这个是一个用问题域词汇编程而不是解决域词汇编程的例子,最大的优点是意图明确,容易理解,代码可读性强;另外一个好处是相对稳定——比如用
game 代替
queue——其一,评审代码的人可能会不明白这个queue是做什么的,为什么和下面的初始化函数格格不入,从而造成你频繁的回答大量的沟通性的问题,
这将大大影响你生活的稳定性和质量;其二,如果我们将来——我是说如果——使用list数据结构来替换queue,避免了还要更改注释的风险——代码更新
而注释陈旧,正是我们在进行项目开发时一个特别特别特别的n次幂严重的问题……而使用问题域的词汇,只要需求不发生变更,则我们就不需要修改什么而导致一
些……代码人格上的分裂……”
“哦,我再消化消化……”小P开始看代码,“下面这些函数的实现是什么意思?”
“嗯,是测试,”老C说,“我们先不看具体函数,先看看main()函数的主要结构。”
int main()
{
APPLE_GAME theGame;
int num;
/* Initialize the game. */
InitAppleGame(&theGame);
/* Play the game, until the last child is found. */
while (!IsGameOver(&theGame))
{
PlayGame(&theGame);
}
/* Search the last child's seat number. */
num = LastChildSeatNum(&theGame);
printf("The last child's seat number is %d.\n", num);
return 0;
}
“我们用实际的代码完善刚才的注释——刚才的注释其实就是伪代码的一部分——然后在框架函数中加入测试代码,检验我们的算法是否可行。”老C解释道,“现在我们的算法一目了然,你看看是否是用问题域的词汇表达算法更清晰一些呢?比一些a,b,c之类,或者其类似的解决域内的名字更好理解吧?”
“哦,我再看看……”小P答道,“我要消化一下……”
“嗯,”等小P抬起头,老C补充道,“接下来一个重要的规则是
先构思如何测试,更先于编码!”
“稍等,”可能被新的信息灌输的有些头晕,“这个是什么意思?”小P有些反应不过来。
“就是说,在编码之前,我们要先想好如何测试我们即将要编写出来的代码。我们的代码是否易于被测试,关系到我们代码质量的生命!”老C解释道,“如果你一
开始就考虑到这些问题,并留有充分的余地,那么在做代码自测和测试人员测试时,会节省组织内部大量的精力……算了,这些也是要靠编写代码的规模积累起来的
经验,你以后会慢慢的明白的。但是,无论如何你在编写代码的时候要保持足够的意识,要不断提醒自己,我所写的代码易于测试吗?”
“好,我记住了。”小P说。
“呵呵,其实经历了一些挫折你才会真正明白——不过就算建立了概念也不错。”老C笑道,“现在你编译并运行一下代码吧,观察一下屏幕输出的信息……”
“好,”小P看了看文件底部的函数实现,然后又看了看屏幕输出信息,“哦,算法的脉络这样看就比较清楚了,果然我脑海中就是这么想的,不过现在更具体,也好追踪了。”
“O.K.!我们第二版的程序又有了!”
“这么快?为什么?”小P不解道。
“我们验证了算法,证实算法框架运行与设计——就是你脑海中的步骤——是一致的,这样当然ok了!”老C一边说一边将main.c拷贝到
AppleGame_V0.02,并且又新建了一个AppleGame_V0.03目录。
(请等待V0.03版本)