本次结对编程已经结束,至今也有了一些相关感想,从头开始介绍。
我们抽到的是UI第三组。
一、如何确定架构和技术选型
最重要的一点:一定不要“拍脑袋”,所谓“拍脑袋”,就是看见需求,就立刻认为自己的某种技术储备可以解决这个问题(通常是因为需求和自己某个有印象的技术问题很相似)。
为什么?因为这个实际的需求很可能与你印象中的技术储备解决的问题完全不同。如果断然开始编码,那么很有可能落下一个临近DLL,却要推倒重来的结局。实际上我这次选型就发生了类似的情况,按照我的印象,这次的出题UI有着“设置->下一步->结果”的性质,正好与Qt向导页面QWizard很像。于是按照QWizard编写了所有UI图形界面。到了处理传输参数等等逻辑业务的时候,发现QWizard每个页面是分开的,不像QStackedWidget一样实际上所有控件在一个窗体上,这样导致我完全不知道QWizard的数值传递该如何完成,在网上搜索相关资料,也没有能够说清楚的。最终经过几个小时的挣扎,我还是放弃了QWizard的功能。改用QStackedWidget,所幸有惊无险,还是按时交付了UI。这也告诉我们,该放弃的旧系统就要果断放弃,不要多浪费无谓的时间。
如何解决?我觉得,决定架构之前,先在脑海里把工程写一遍,每个模块用什么算法,都想一遍。也就是,需求分析一定要做细。
另外一点深刻的体会就是,国内的论坛网站实在是不能提供有效的技术支持,以后多使用Google和Stack over flow。
二、对接的麻烦
UI主要工作量集中在设计界面,代码量并不大,大概在300行左右(除开Qt Creator生成的代码,自动生成的代码在1500行左右)。但是对接的时候,各组Core的API甚至逻辑功能的差别,给我们带来了巨大的麻烦。
首先是逻辑层面,有的组要求加减、乘除分别绑定,而有的组要求可以单独支持任一种运算;有的组要求小数和分数不能同时存在,而有的组要求一个式子里既有小数又有分数,这样的情况下,同样的UI界面不可能满足所有Core的要求(比如QRadioButton选项互斥,而QCheckBox可以复选,不支持复选的 core用了QCheckBox,就会造成非法输入),也许需要根据不同的core发布不同版本的UI。但这个工作量显然太大了。
另外则是每个组的接口参数不同,需要编写不同的接口转换代码,而且代码量的高低,很大取决于API的好坏。本次所有Core中的API主要分为三个档次:
第一档:接口函数少,传参合理,可以直接把UI获取的用户作为函数参数。这样的接口,对接代码量是最小的。
这是第一档的接口代码:
//setting
set_setting(oNum-1,ui->oUppRange->value(),ui->precisionSpin->value(),ui->fraction->isChecked(),ui->real->isChecked(),ui->mulDiv->isChecked(),ui->power->isChecked());
// generate one question and one answer (loop this)
std::string *question = new std::string();
std::string *answer = new std::string();
generate(&question,&answer);
ui->questionBrowser->setText(QString::fromStdString(*question));
ui->answer->setText(QString::fromStdString(*answer));
第二档:接口函数多,传参合理。这是本次大部分组core的API所处在的档次,这种API把setting的每一个参数分别设置为一个函数,需要单独传输,但是参数设计合理,可以直接将用户输入作为函数参数。这样的接口,setting的代码量较大,有时需要做用户输入到core接口的转换(例如“是否支持小数/分数”的参数是0,1,2,但UI得到的数据是选择框是否被选中的布尔型,因此要先做逻辑判断,再进行转换)。
这是第二档所需要的接口转换代码(为了避免可以看出是哪一组,我对函数名作了修改):
//setting
core.setQueNum(ui->qNum->value()); core.setDataNum(ui->oNum->value()); core.setRange(ui->oUppRange->value());
core.setAccuracy(ui->precisionSpin->value());
//set operator type if(((ui->mulDiv->isChecked())&&(ui->power->isChecked()))==true) core.setOprType(3); else if(ui->mulDiv->isChecked()==true) core.setOprType(2); else core.setOprType(0);
//set operand type if(ui->fraction->isChecked()&&(!ui->real->isChecked())) core.setDataType(2); else if(ui->real->isChecked()&&(!ui->fraction->isChecked())) core.setDataType(1); else if((!ui->real->isChecked())&&(!ui->fraction->isChecked())) core.setDataType(0);
//generate all questions
question=core.getQue();
answer=core.getAns();
ui->questionBrowser->setText(QString::fromStdString(question[0]));
ui->answer->setText(QString::fromStdString(answer[0]));
//loop this (i is defined in the header file)
if(i<totalQNum)
{
ui->questionBrowser->setText(QString::fromStdString(question[i]));
ui->answer->setText(QString::fromStdString(answer[i]));
i++;
}
第三档:最差的一档,函数多,而且参数设置不合理。
某一组,曾经将所有的函数的传入参数全部设置为了字符串!!!这意味着什么?意味着UI组要将所有得到的整形、布尔型先进行逻辑判断,再转化成对应的字符串,最后再调用接口函数。而当我们为这个组的接口编写了复杂的转换代码并实现了之后,这个组将API改回了直接传参的模式。(就不贴代码了)。
(这里对号入座太简单,在这里对那一组说声抱歉,但是我实在很想吐槽这一点)
不难想到,实际上这种输入为字符串的方式一定也给core组增加了工作量,那么他们为什么会这样做?
我可以理解这个core组的想法,可以说出发点是非常好的,他们希望自己的接口更加“易懂”,对于最终用户,什么东西最好懂?当然是自然语言!比如,字符串“precision”代表了“精度”,“+-*/”代表“加减乘除”,函数的功能是简单易懂的。但是他们忽略了一点,这种考虑应当是相对于最终用户,也就是使用这个软件的人而言的,而不是相对UI编写者而言的。对于UI编写者,只需要定义清晰易懂的变量名、提供能够理解的API文档,就足够了。而如何用自然语言去通俗易懂地表述输入要求,应当是我们UI组所考虑的内容。
三、工欲善其事,必先利其器
因为习惯了VS的界面,听说VS支持Qt插件,我们便愉快地决定了使用Qt for VS,那时我还不知道Qt Creator内置了Qt Designer,这件事情,所以第一个被我们放弃掉的UI界面,是纯代码编写和布局的,简陋而且工作量大。
换成Qt Creator后,才发现之前做了许多没有效率的工作。(当然,能了解Qt的代码实现也是一种收获了)。
以上就是这次结对编程的主要感想。