- Foreword
此次的结对项目终于告一段落,除了本身对软件开发的整体流程有了更深刻的了解外,更深刻的认识应该是结对编程对这一过程的促进作用。
在此想形式性但真心地啰嗦几句,十分感谢能端同学能够不厌其烦地接受我每次对软件的修改提议,并在代码实现过程中为团队贡献了许多人性化的tips;
另外,他积极好学的心态也很让我佩服。从初入面向对象,数据结构的使用,实际工程的开发,他快速地掌握了其中的技巧;
并在过程中不嫌辛苦地和我一起熬夜,才能在短短48h内高效利用时间,开发出这款颇多功能的软件。感谢!=)
最后,用户需求请见:http://www.cnblogs.com/jiel/p/4830912.html
- Brief View
这一部分将对软件功能做简要介绍:
【基础要求】
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的苦都白吃了。。