- Foreword
此次的结对项目终于告一段落,除了本身对软件开发的整体流程有了更深刻的了解外,更深刻的认识应该是结对编程对这一过程的促进作用。
在此想形式性但真心地啰嗦几句,十分感谢能端同学能够不厌其烦地接受我每次对软件的修改提议,并在代码实现过程中为团队贡献了许多人性化的tips;
另外,他积极好学的心态也很让我佩服。从初入面向对象,数据结构的使用,实际工程的开发,他快速地掌握了其中的技巧;
并在过程中不嫌辛苦地和我一起熬夜,才能在短短48h内高效利用时间,开发出这款颇多功能的软件。感谢!=)
最后,用户需求请见:http://www.cnblogs.com/jiel/p/4830912.html
- Brief View
这一部分将对软件功能做简要介绍,软件下载地址见:http://pan.baidu.com/s/1hqhg2Wk
【基础要求】
1)该软件采用C#编写,在MSUNIT框架下通过需求测试。
2)软件功能分为三大部分:四则运算表达式生成器、四则表达式评分器、四则表达式计算器,均已达到用户要求。
3)采用xml文件的方式在前后端模块中传递参数,提高可拓展性。
【拓展部分】
1)UI界面开发,采用多线程前后端任务同时进行,增加进度条,并支持菜单栏对文件、设定、帮助三个方面进行导航。
2)生成器:1. 支持自定义是否表达式中有小数,以及生成小数的精度。
2. 支持两种模式,普通/批处理模式:在普通模式下生成单个文件;在批处理模式下可自定义生成文件数。
3. 支持记忆上次设定,防止每次打开程序都要重复调整参数(通过xml实现)。
4. 支持停止按钮,防止用户需求过于苛刻,无法生成相应数量的式子,此时用户可手动中止,但又不丢失已生成的内容。
5. 支持选择是否生成完毕立即打开文件/目录。
评分器:1. 支持带小数的评分方式,并可以自定义正确的精度(eg. 当精度为0.01时,正确结果为0.33333....所给答案为0.33即认为正确)
2. 支持两种模式,普通/批处理模式:在普通模式下对单个文件进行评分;在批处理模式下用户可输入正则表达式来筛选文件,程序将对和正则表达式匹配的多个文件进行批量评分。
3. 支持记忆上次设定,防止每次打开程序都要重复调整参数(通过xml实现)。
4. 支持停止按钮,功能同上。
5. 支持软件运行日志内容输出,当评分系统遇到不合法的表达式或者任何异常情况时,都会在Log窗口中输出进行提醒。
6. 支持等级评分制,用户可在Setting中开启等级评分,并设定每个等级的范围,程序将为每个结果按范围划分等级。
7. 支持选择是否生成完毕立即打开文件/目录。
计算器:1. 高冗余性,所有错误情况将给出对应的提示。
2. 支持记忆上步计算结果,并可一键输入该结果。
3. 支持开启/关闭键盘输入。
4. 支持分数小数混合运算。
3)Setting:支持用户通过统一的设定界面对多种功能进行设定。
【界面截图】
1)生成器(普通模式)
2)生成器(批量模式)
3)评分器(普通模式)
4)评分器(批量模式)
5)计算器
6)设定界面
7)运行时界面
- Blog Assignment
- Pair Programming
对于结对编程的模式已经不是第一次体验,但每次经历都会收获颇丰,不禁感叹:
“啊,原来人与人之间思考问题的方式差别这么大。“ “他的这一点想法很好,值得借鉴。” “原来我在....这方面比较有优势。” ....
每一次结对编程都会从对方身上学到许多值得借鉴的地方,同时对自己的认识更深了一步。
如在此次的结对编程任务中,当我在书写界面代码能端审核时,他在我多次纠结于代码整洁性而浪费时间时提出了意见,及时挽回了大量时间;
我在书写的过程中吹毛求疵式地注意代码的利用率,代码的封装,风格的整洁,导致很多本不应该在开发初期成为重点问题被过分关注,一定程度上降低了开发效率。
而能端则在我每次“犯病”时都进行提醒,因此我能够快速完成前期代码的开发,而将封装、精简等工作一次性留到后期进行。
在这件小事上,结对编程反映出了我们各自思想侧重点的不同,同时也给我敲了个警钟,更清晰地划分各个阶段的任务。
除了认识到这点不足外,在互换角色后我也对OO思想有了进一步的认识。review能端代码书写的过程中,我发现他对类的设计、类间的交互、类的角色把握地不是很准,因此我除了在review过程中不断修正他的OO思维外,还为他设计了一些练习帮助他掌握这一思想。通过这样的交流,一方面能端在OO编程上有了极大提高,我自身也对OO有了更为系统的认识。
另外,修改计算器的功能时,急于完成功能且粗心大意的我忘了同时考虑多种情况,如对界面上"="按钮的点击事件的修改需要同时赋与KeyPress事件中"=/Enter"事件的修改。
这一问题若是我独自编程必然会留下一个潜藏的bug,然而好在能端及时指出了我的疏忽。
结合我在这次结对编程中的经历,我为reviewer的工作做出了如下三点定义(见彩色部分):
正所谓“当局者迷,旁观者清”,结对编程中的coder将更多的精力着眼于局部代码的书写,思考代码逻辑和代码风格,而很难做到纵观全局;相反,结对编程中的reviewer无需考虑接下来的代码如何书写,而是站在全局的角度,综合coder当前的进度来分析(critical thinking)、预测(active prevision)与修正(resonable revision):分析当前已有代码是否和先前的模块冲突,逻辑是否完善;预测coder接下来可能会利用的逻辑;结合自己的预测和coder实际完成的工作进行修正,这里的修正包括两个方面,若预期与实际相符,则无需修正,否则要考虑是自身的预测失误还是coder的疏忽,这样的预测不仅可以提高reviewer自身的逻辑水平,也能让彼此相互借鉴,杜绝了更多潜在的bug。
在效率方面,我觉得结对编程带来的影响真的是十分巨大的!
由于考虑到我和能端之前彼此不认识,且专业背景不同,立即开始结对编程是件浪费时间的事情。因此我们明智地选择了,将更多的时间放在前期的磨合阶段,而非软件开发。
因此,在开始软件开发之前,我们见面并了解了彼此的水平。鉴于能端在代码能力上稍有不足,快速磨合并提高的方法即让他参考我先前写过的代码,借鉴其中面向对象的思维以及代码风格。
我们共同商榷确定了一个统一的风格和思维方式后,能端用两三天时间进行了简单的“学生教务管理系统”的开发,已经达到了能够一起结对编程的要求。
在这之后,结对编程的效率之高就立马体现出来了!我们很快地对用户需求进行分析,设计代码结构,分析week1工程代码需要修改的部分。
由于有两个人,很多容易被忽视的细节能够在前期得到重视,如参数传递应该用xml还是函数传参,前后端应该是两个线程还是单个,等等。
用xml和用函数传参设计出来的模块思维是不同的。用xml设计的后端独立性更大,拓展性更高,同时后端要进行的修改和冗余测试就更多;而用函数传参虽然简单却难以拓展,前后端独立性较低。
我们两*衡利弊,和实现难度后,一致决定采用xml。而如果前期未注意到这点就开始后端开发的话,等真正注意到时再修改就要改动大量代码结构,大大降低效率。
诸如此类的细节实在太多,在此不一一细提。因为有两个大脑一起分析,让再细微的东西都难以逃脱思维的捕捉。
除了让我们考虑地更周到,结对编程还“逼迫”我们“脱产式”地投入到任务中去。很明显的一个感觉是,以往我自己在编程时老是习惯写一会儿刷下微博、逛下bilibili、check一下QQ,缺乏自主监督的能力。
而在结对编程中,双方是彼此最好的监督,这就要求我100%地集中。正得益于此,我们才能在较短的时间内完成开发。
最后,结对编程还让我认识了第一个留学生朋友,thanks =)
最最后,例行公事的优点和缺点:
能端的优点在于不过分注重细节,阶段性目标明确;
细微入至,能精准发现潜藏的小问题;
学习能力强,并且是主观学习而非被动接受,因此在商榷过程中他的主观思想很重要。
能端的不足之处应该是先前代码经验少,在书写代码时还需要我时常提醒。
我的优点在于有较好的OO思想;
对软件整体架构和接口设计地较好;
先前的代码经验多,书写效率较高。
我的不足之处在于有时过分注重细节,而有时又会粗心大意(- -|||我是奇美拉吗。。这么矛盾。。)
最后附上颓废的现场图一张:
- Information Hiding, interface design, loose coupling
- Information Hiding, interface design, loose coupling
该内容在Code Complete一书中涉及。
1)Information Hiding:
此处摘录书中Section 5.3的原文:
“Information hiding is part of the foundation of both structured design and object-oriented design.
In structured design, the notion of 'black boxes' comes from information hiding.
In object-oriented design, it gives rise to the concepts of encapsulation and modularity,
and it is associated with the concept of abstraction.”
根据上述可知信息隐藏是结构化编程和面向对象思想的基础。它产生了结构化编程中黑盒测试的想法,促进了面向对象过程中的封装与模块化的概念,并和抽象的编程思维相关。
为了论证这一点,作者采用了“冰山理论”作为说明。我们的代码与冰山一致,需要将大部分外界不关心的东西隐藏起来,而将可见的部分留给真正需要的人。
在遵守这一原则时,要注意隐藏两种因素:
1. 隐藏复杂度:复杂性高的功能需要单独封装隐藏起来,一方面为了防止使用时要重复书写,一方面防止外部调用增加程序不确定性。
2. 隐藏变化源:一些在程序中经常出现的全局变化源需要隐藏起来,比如计数器,可以用函数封装进行累加,避免直接对计数值操作。这样可以增强代码安全性和可拓展性。
应用于此次的结对编程中,对于式子计算中的子过程,一个逻辑较为复杂的字符串分析函数,我们将其单独封装起来,并仅在上级公有方法中调用,做到了计算复杂过程的隐藏。
另外,对表达式生成,需要设定一些基础参数,我们采用将这些变化源用方法单独封装起来,外部和内部只能通过这一方法改变参数,增强了程序的稳健性。
2)Interface Design:
根据本书的Section 6.2节中对良好类接口的定义,一个好的接口设计应要满足:Good Abstraction 和 Good Encapsulation。
一方面要对类职责、角色进行高度抽象,将类本身内部、类与类之间的交互、行为、职能都做出一个抽象,并且保证这些抽象之间依赖性最小。
当这些抽象完成后,再根据细节去设计接口,进行方法的封装。
应用于这次的结对编程中,一个最佳的例子是分数类的设计。首先从分数这个概念出发,思考下列问题:
它本身有哪些构成元素?本身需要用到什么方法?它与其他分数实例,甚至是不同类的实例间可以有什么交互?
通过这样的考虑可以很清晰地知道,分数核心是分子分母,内部间可以有化简这一操作,相互间可以有四则运算等操作。
因此分数类的接口设计就很清晰了,对操作符进行重载,支持多种类运算;内部实现一个约分的函数用于化简。
3)Loose Coupling:
根据书中Section 5.2对耦合的定义,耦合可以分为以下几种:简单数据耦合、简单对象耦合、对象参数耦合、语义上的耦合。
其耦合的紧密程度一步步提高,最后一种的耦合往往是很危险的。
在此次的结对编程项目前期,我们的程序前后端确实遇到了语义上耦合的问题。如界面端的stop命令要中止后端的运行程序,
这时候我们最开始采用传递标识位的方式,通过前端告知后端停止的信息。
但这种方式在多线程的设计中立刻被否决了,相反,我们让后端程序主动轮询前端的状态,避免了参数的传递。
另一方面,为了防止过多参数的传递,我们采用xml文件,让前后端通过读写xml文件传递参数,这样实际传递的参数仅有xml文件路径一个。
- Design by Contract, Code Contract
说到这个话题,脑袋里第一反应的都是大二时OO课程的噩梦。。w(゚Д゚)w
(考试时候纯手写数据抽象、前后置条件和不变式啊!!!一场考试洋洋洒洒写了我半只水笔啊!!怒!(╯‵□′)╯︵┻━┻)
根据wiki对其的解释,它将普通的数据抽象拓展增加了前置条件(precondition),后置条件(postconditions)以及不变式(invariants)
根据吴际老师PPT中的内容,他对契约(Contract)的理解如下:
“数据抽象规格的目标是为使用者提供一个契约,为实现者提供一个规约;
使用者无需关心一个类保存了哪些数据,只需要了解这个类能做什么;
实现者关心一个类应该保存哪些数据,需要在实现时来确定相应数据的类型和存储结构(即数据结构)
---->从而能够有效、高效率和健壮的实现所承诺的契约!”
由此可见,Design by Contract的方式是衔接使用者和实现者一个重要的桥梁。缺少了契约,可能会导致方法被不安全调用,实现功能不符合预期等结果。
因此在设计前期需要对各个类都进行数据抽象及契约的设定,但在此次项目开发前期我们未进行契约的制定,然而通过后期反思,可以发现一个好的契约能够规避很多bug。
如,在设计中的Expression类(采用根据运算顺序分步存储计算式的数据结构),在生成表达式时,以如下方式记录:
(1 + 2) × (3 - 1) ------> exp[3] = {{1, 2, '+'}, {3, 1, '-'}, {①, ②, '×'}};
其中对①、②这些标记符的出现位置是有强烈要求的,比如,在 exp[i] 中不能出现 >=i 的标识符;
且在最后一步的时候,所有的标记符应该全部出现过,否则前面生成的式子将没被利用。
由于开始时没有制定合适的数据抽象,不变式等,导致后期在调试时难以定位bug位置,大大浪费了时间。
若有给Expression类写个不变式,那就会强迫我们去思考,这个类到底需要满足什么条件?并且在每个方法内都要满足这些条件,即使不满足,错误原因也能根据不变式出错的地方很快定位到。
总的来说,OO的苦都白吃了。。
- Unit Test
- UML
- Implement
实现部分我就从数据结构和算法两个方面来说吧。
1)数据结构
1. Expression类
其实若单纯从表达式这个角度来说,各种存储方式都可以是不错的选择。
但棘手的在于用户需求需要对表达式进行判重,且表达式中的项可为整数,分数,甚至小数(此处是我们为了程序功能健全性添加的),且过程中可能会出现括号等。
通过综合上述需求,不难发现一个较为理想的数据结构要能够清楚地划分该表达式中的每个部分,确定运算顺序,以及要支持泛型(各项的类可不同)。
而将表达式存储在字符串中的做法无疑是将这些信息全部杂糅在一起,在需要利用信息时还需要实时分析,大大降低了效率。
因此我们想利用一种能够按照运算顺序分步存储表达式的数据结构,如:
(1 + 1/2) × (3 - 1) ------> exp[3][3] = {{1, 1/2, '+'}, {3, 1, '-'}, {①, ②, '×'}};
(其中,1/2为Fraction分数类,①/②为Notation标识符类,具体介绍见下文)
这样的数据结构为计算以及重复性检验都带来了较高的效率(关于重复性检验的方法请见下文)。
2. Fraction类
为了方便(偷懒)我将整数、分数、小数三种类型均集成到了这一个类中。
但这么做有它的理由(其实只需将其改名为Item类一切就合理了。。):整数是分母为1的小数,小数为分数分子除分母的另一种表达形式。
因此,分数类的设计很清晰,一个分子,一个分母,一个小数标志位,done。
最后,我们为分数类添加了许多构造函数以及重载操作符,大大地增强了它的易用性,使得后期开发过程十分顺利。
3. Notation类
该类的实例仅存储一个数字,用于代表是表达式第几步运算的结果。
2)算法(纯靠intuition。。毫无技巧。。可略过。。)
1. 重复性检验
重复性检验的过程我和大多数同学一样,对于新生成的表达式,需要一一和之前生成的表达式进行比较,但在单步比较的过程中花费的时间较少。
得益于上述分步存储表达式的数据结构,使得对于两个表达式的重复性检验步骤十分清晰明了,下面举个例子:
1) (1 + 2) * (3 * 4) 2) (4 * 3) * (2 + 1)
第一个式子存储为:1) 1 + 2; 2) 3 * 4; 3) ① * ②
第二个式子存储为:1) 4 * 3; 2) 2 + 1; 3) ① * ②
接下来根据式子1)的每一步逐个到2)寻找是否有重复项,当操作符为‘—’或‘÷’时可以不考虑交换律,否则还需将两端操作数交换进行比较。
同时,为了便于比较带标识符的①、②是否相同,我们可以建立一个长度为3的数组 map[3] = {0, 0, 0} 用于记录1)中的每一步和2)的哪步重复。
当判断第一步1 + 2在两个式子中是否相同时,发现第一个式子的第一步和第二个式子的第二步相同,那么表中就记录上map[0] = 1; 同理:map[1] = 0;
这样当遇到第三步① * ②,这种带有标记符的重复判断时,就可以直接查表而不必递归判重。
这样的做法虽然看起来复杂,但在实践过程中无论从效率还是准确性上来说都是较高的。
根据前期测试,设定为数在0~2间,操作符最多三个的条件下,可在很短时间内生成60000+的式子(无重复),最多能生成多少还未测试。
2. 表达式计算
表达式计算我选择一个个字符分析的方法,通过构造数字栈和操作符栈,以及自动机来实现了高冗余性的计算函数。
具体过程是数据结构课程的基础,大家应该都会,在此不再赘述。:)