软件工程项目之二:结对编程——四则运算生成计算程序
0x01 :简介
本次的编程任务是完成一个具有UI界面的,具备四则运算生成、计算、判断对错功能的程序。本次程序使用C#语言编写,用时为2周。
0x02 :软件工程和PSP表格记录
PSP 2.1 | Persinal Software Process Stages | Time(Estimated) | Time(Real) |
Planning | 计划 |
|
|
Estimate | 估计这个任务需要多少时间 | 24h | 36h |
Development | 开发 |
|
|
Analysis | 需求分析(包括学习新技术) | 8~10h | 10h+6h 基本UI框架:1.5h 树的最小表示:2h 随机数效率评估:1.5h XML参数形式:1h |
Design Spec | 生成设计文档 | 2~3h | 2h |
Design Review | 设计复审 | 1~2h | 3h |
Coding Standard | 代码规范 | 1~2h | 10min(*) |
Design | 具体设计 | 1~2h | 3h |
Coding | 具体编码 | 15h~16h 基本框架:7h 单元测试:5h UI框架:3h | 18h 基本框架:7h 单元测试:5h UI框架:3h 调试编码:3h |
Code Review | 代码复审 | 2~4h | 4h |
Test | 测试 | 5~7h | 6h |
Reporting | 报告 |
|
|
Test Report | 测试报告 | 2~3h | 1h |
Size Measurement | 计算工作量 | 15min | 10min |
Postmorten & ProcessImprovement Plan | 事后总结, 并提出过程改进计划 | 2~3h | 1h |
| 合计 | 39h15min~ 52h15min | 66h20min |
表格补充说明1:(*)代码规范时间较短的原因将在后期进行说明
表格补充说明2:而此次预估时间和实际时间相差较大,主要因为大量时间用以设计阶段的优化,保证各类对自身的功能划分清晰;而后期因为测试和编码是同时进行的,因此合计时间是两人共同的消耗时间的计算,实际消耗的总时间可能少于预期;
表格补充说明3:XML参数形式:1h,考虑到程序本身的结构,且XML解析的部分由于自身Core逻辑的缘故,Core尽可能向前端提供了充裕的信息,而不必使用中间层过渡“前后端”,类似于停留在“非直接耦合”到“数据耦合”的阶段,因此模块本身的独立性较强
0x03 :结对编程
图1:结对编程中编码者同步进行的白盒测试
图2:结对编程中测试者同步进行的黑盒测试
图3:结对编程中针对测试中标注“红色且程序崩溃的错误”测试者立刻提出,并交予编码者共同完成代码调试和复审
0x0304:结对编程队友的优缺点
我的队友钱林琛具有性格主动且负责任。当我在编程中遇到重大困难时,他总是主动提出帮我解决问题。程序的一些核心模块、功能,他占了很大一部分功劳。
我的队友具有良好的编程习惯。他把程序的不同功能,如生成题目,计算,处理异常等分别包装在不同的模块中,为后期调试程序提供了极大的方便。在我设计了大量测试用例后,发现程序中仍存在不同程度的bug,模块化的编程方式使得我们迅速发现了所有的问题并最终一一解决。
我的队友具备合理的时间分配能力。他认为,2周的时间必须重点分配在各模块的编写和调试上,而UI界面则可以在修复所有bug后再做。事实证明,我们这样做,是完全正确的。
我的队友的主要问题是有时考虑问题不够周全,会犯一些比如未处理溢出等的错误。
0x0308:结对编程的优点和缺点
结对编程的优点还是很明显的。两个人合作编程,就可以让两个人的智慧和能力共同发挥出来,来完成一个人难以单独办到的事。一个具有实际应用价值的程序,代码量会达到成百上千行,工作量非常大。如果两个人合作,平均每个人的工作量就可以减少一半。而且,由于两个人的思维不同,可以互相取长补短。两个人擅长的方面不同,所以分工合作就可以同时发挥两个人的优点。
结对编程的缺点有主观性的也有客观性的。客观性主表现在两个人电脑程序可能存在不兼容现象,导致一个人的程序无法在另一个人的机器上运行,但这个问题可以通过程序的升级而轻易解决。主观性表现在两个人性格不合,不能友好相处,因此合作的效率也受影响。人际交往的障碍是妨碍合作编程的重大问题,而且难以在短时间内解决,所以应当引起重视。
0x04 :重要的设计方法
“我们在应用程序开发中,一般要求尽量两做到可维护性和可复用性。应用程序的复用可以提高应用程序的开发效率和质量,节约开发成本,恰当的复用还可以改善系统的可维护性。而在面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则,遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。面向对象设计原则和设计模式也是对系统进行合理重构的指导方针。”
此段内容摘自吴际老师的面向对象课程的讲义中,提及到信息隐藏原则(OCP的理解)、接口设计、契约式编程等概念,这里就不妨罗列出部分阅读的参考资料和个人理解,进一步阐释和说明
0x0404:Information Hiding(信息隐藏)
在概要设计时列出将来可能发生变化的因素,并在模块划分时将这些因素放到个别模块的内部。这样,在将来由于这些因素变化而需修改软件时,只需修改这些个别的模块,其它模块不受影响。信息隐蔽技术不仅提高了软件的可维护性,而且也避免了错误的蔓延,改善了软件的可靠性。现在信息隐蔽原则已成为软件工程学中的一条重要原则
这里的论述相对有些晦涩,其实可以简单理解为变量的“封装”,而这里的封装,从简单入手,就是我们需要确保某一类中的数据成员为private属性,不会因为其他类随意调用了你的类的实例中的某一属性,而导致后面在调用这一对象是出现“莫名其妙的错误”。而从复杂方面理解,其实就是“抽象化”的趋势,换句话说,我们不允许最重要的抽象层模块被修改,我们希望在扩展某一软件的新功能时,尽可能提供新的行为函数,而不是对你的代码进行反复的修改,这样能导致软件开发过程中的软件始终保持较好的稳定性,不至于瞬时间“面目全非”。
其实,对于信息隐藏,感触最深的还是可读性问题。从底层类逐步封装到高层类,如果存在超大类、超长类、超长方法的存在,一旦更改某一底层的函数,我们将连带修改若干变量,而这一修改如果,不全面,将会带来非常严重的后果!甚至会导致你情绪崩溃!这里必须展示一段当时自己写过的一段注释:
/*Dear maintainer:
Once you are done trying to optimize the routine with HUGE class or method ,
and have realized what a mess debuging corresponding parameter,
increment the following counter as a warning to another program:
total_hours_debugging_wasted :
*/
在此次的设计中,从底层类到高层类的封装时,我尽可能保证高层类通过调用底层类的特定的public函数,而将其他方法尽可能隐藏为private,使得高层类不会重复调用底层类的组合引发错误。当然这里特别说明一点,因为此前在浏览SOLID原则的时候发现DIP原则,个人理解是,高层类需要通过调用底层类的函数实现,但最后实现上要考虑的是高层的抽象。比如,开关中,的确包含灯的开关,但不能完全依赖灯开关的实现;我们要做的,是关注开关的标准接口,这样,一旦我们需要扩展开关为电器的开关,我们只需要保证灯、电器都继承了开关的接口。这和高层类调用底层类,个人理解,是两个不同的原则方面
0x0408:接口设计(Interface Design)
Interface Segregation Principle,接口隔离原则,是SOLID原则中重要的组成部分。“比如,我们对电脑有不同的使用方式,比如:写作,通讯等,如果我们把这些功能都声明在电脑的抽类里面,那么,我们的上网本,PC机,服务器,笔记本的实现类都要实现所有的这些接口,这就显得太复杂了。所以,我们可以把其这些功能接口隔离开来,比如:工作学习接口,编程开发接口,上网娱乐接口,计算和数据服务接口,这样,我们的不同功能的电脑就可以有所选择地继承这些接口。”(参考链接:http://www.educity.cn/se/1383082.html),这很有搭积木的感觉,因此,个人理解优秀的接口,应当对整体的功能框架有着良好的划分。这里也展示一下,此次设计的接口。
interface CoreInterface
{
void setting(int MinRange, int MaxRange,
int minOp, int maxOp, long number,
bool isFactor, bool isDecimal, bool isMin,
bool isBracket, bool isMul);
bool setJudge();
string CreateSingleExpression();
string[] CreateExpression();
string[] CorrectionJudge(string exercise, string answer);
string Calc(string formula);
}
是的,我其实非常想吐槽自己的接口。这次即便在设计阶段耗费了大量的时间,但最终接口的实现上仍不尽人意,甚至最后的程序为了兼容这一接口,不得不添加一些属性来保证效率。这里还是存在改进方法的,一方面是设计一个中间类,兼容我的UI和Core,另一方面,重新设计接口并实现。个人感觉,此次底层的实现思路都比较清晰,重点就在于接口中我想给我的其他程序什么信息?我必须实现哪些功能?。因为现在自己仍在尽力耦合自己的Core核心和其他团队的UI界面,瞬间感觉当时自己应当联系其他团队开发出统一的一套接口标准,方便后期的耦合;但事已致此,我们只能在两个接口之间搭建一个中间类,实现逻辑层面的转化,而这也是此次附加题中打算耦合的思路。
0x040c:松耦合(loose coupling)
In computing and systems design a loosely coupled system is one in which each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. Sub-areas include the coupling of classes, interfaces, data, and services —— 摘自Wikipedia(https://en.wikipedia.org/wiki/Loose_coupling)
一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。换句话说,对于OOD而言,此法则的初衷就在于降低类之间的耦合,尽量减少类对其他类的依赖(写到这里,突然对DIP原则有着一定的认识,换句话说,对于高层类,我们需要抽象化接口并始终维护保持稳定性,底层类,则尽可能少地依赖其他类的实现,保证不会因为某一类的改变影响全局)
个人感觉,此次松耦合概念实现难以预判,因为,实现过程中,自己的高层类很多都需要底层类的方法,但是由于接口层的存在,所以使得程序稳定性相对没有较大的变化。不过,松耦合最重要的就是talk only to your immediate friends。
关于这一概念的理解,在进一步的团队编程中,将进一步熟悉这一原则。
0x05 :契约式设计(Design By Contract)
契约式设计(Design By Contract)把类和它的客户程序之间的关系看作正式的协议,描述双方的权利和义务。Bertrand Meyer把它称作构建面向对象软件系统方法的核心。
契约式设计的提出,主要基于软件可靠性方面的考虑。可靠性包括正确性和健壮性,正确性指软件按照需求规格执行的能力,健壮性指软件对需求规格中未声明状况的处理能力。健壮性主要与异常处理机制相关。(摘自:http://www.cnblogs.com/riccc/archive/2007/11/28/design-by-contract-dbc.html)
在此次的结对编程项目中,契约式设计或契约式编程是非常重要的组成部分!因为个人在编码时,能够通过前置条件的设定完成基础的单元测试,而在后期封装时,我们可以通过对不满足前置条件的设定抛出异常的方式解决此问题,从而使得编码有着类似“工程化”的方法;而对于测试者,测试能够通过对前置条件的“划分”,实现全面的前置条件的测试,也为测试提供了很多便利。
因此,从个人的角度理解,契约式编程是一种基于代码正确性证明的编程方式,能够方便编码者和测试者之间的交流,也极大增强的代码的可读性;个人在底层类的Factor和Poiland中也在单元测试中设置了类似的前置条件,方便本身的单元测试。
0x06 :单元测试(Unit Test)
单元测试、白盒测试、黑盒测试,自己一开始混淆了这三部分概念,这里针对某一个问题,展开自己对三方面测试的理解。
Q : 单元测试属于白盒测试?
自己的误区主要集中于这一句话,因此,根据个人理解将以此展开进行论证。首先,单元测试是允许黑盒测试存在的,因为黑盒和白盒测试主要分别验证总体功能是否实现要求、内部操作是否符合规格要求,因此,我们不能单纯将白盒测试和单元测试设置一个单纯的自己关系,两者从测试目的上就有着较为明显的差异:单元测试主要集中于测试独立模块的正确性,白盒测试主要测试程序的整体逻辑,即内部操作的规范性。因此,个人理解,两者之间存在联系,即在编码阶段实现单元测试的开发人员实施白盒测试的成本较低。
函数名 | 属性与对象说明 | 参数1 | 参数2 | 结果 |
public static Factor operator +(Factor lhs, Factor rhs) | 首先根据isDecimal的bool属性值进行划分,将参数划分为浮点数类型和分数类型(整数类型归类为特殊的分数),因为不妨构造测试样例,测试浮点数、非整数分数、分数、假分数间的加法,同时交换顺序保证笔误;补充上溢出检查 | lhs | rhs | |
Factor(true, 1.7, 1, 2) | Factor(true, 2.1, 2, 1) | Factor(true, 3.8, 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(true, 1.7, 1, 2) | Factor(true, (decimal)1/3+1.7, 0 ,0) | ||
Factor(true, 2.1, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(true, 2.1+(2/1), 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(false, 0, 3, 7) | ||
public static Factor operator -(Factor lhs, Factor rhs) | 同理 | lhs | rhs | |
Factor(true, 1.7, 1, 2) | Factor(true, 2.1, 2, 1) | Factor(true, 1.7-2.1, 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(true, 1.7, 1, 2) | Factor(true, (decimal)1/3-1.7, 0 ,0) | ||
Factor(true, 2.1, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(true, 2.1-(2/1), 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(false, 0, 3, -5) | ||
public static Factor operator *(Factor lhs, Factor rhs) | 同理 | lhs | rhs | |
Factor(true, 1.7, 1, 2) | Factor(true, 2.1, 2, 1) | Factor(true, 1.7*2.1, 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(true, 1.7, 1, 2) | Factor(true, (decimal)1/3*1.7, 0 ,0) | ||
Factor(true, 2.1, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(true, 2.1*(2/1), 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(false, 0, 3, 2) | ||
public static Factor operator /(Factor lhs, Factor rhs) | 同理,但是在此函数中,我们抛出了两个异常DivideByZeroException,OverflowException;前者由题意即可判断,而后者是由于long类型的加法溢出导致,因此,在关键函数嵌套了checked关键字保证了溢出检查; | lhs | rhs | |
必须在前置条件中保证除数非0,否则将造成infinte结果的返回 | Factor(true, 1.7, 1, 2) | Factor(true, 2.1, 2, 1) | Factor(true, 1.7/2.1, 0, 0) | |
Factor(false, 1.7, 3, 1) | Factor(true, 1.7, 1, 2) | Factor(true, (decimal)1/3/1.7, 0 ,0) | ||
Factor(true, 2.1, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(true, 2.1/(2/1), 0, 0) | ||
Factor(false, 1.7, 3, 1) | Factor(false, 1.7, 1, 2) | Factor(false, 0, 6, 1) | ||
Factor(true, 2.1, 3, 1) | Factor(true, 0, 1, 2) | [ExpectedException(typeof(DivideByZeroException))] | ||
Factor(true, 2.1, 3, 1) | Factor(false, 1.7, 0, 1) | [ExpectedException(typeof(DivideByZeroException))] | ||
Factor(false, (decimal)2.1, 1000000000000000000, 1) | Factor(false, (decimal)2.1, 9000000000000000000, 1) | [ExpectedException(typeof(OverflowException))] | ||
public int CompareTo(Factor other) | 实现Icomparable<Factor>接口的方法,重点考察浮点数和分数的等值;这里由于选用decimal方法导致,可以通过"="判断; | this | other | |
Factor(false, 1.7, 3, 1) | Factor(true, 1.7, 1, 2) | -1 | ||
Factor(true, 1.7, 1, 2) | Factor(false, 1.7, 3, 1) | 1 | ||
Factor(true, (decimal)2.0, 3, 1) | Factor(false, (decimal)1.7, 1, 2) | 0 | ||
public override void ToString() | this | |||
ToString()函数重载,测试样例主要确保假、真分数、整数、非最简分数的正确表示,特别说明,对于负数强制添加括号,因此,主要围绕此方面设计测试样例 | Factor(true, (decimal)1.2, 0, 1) | "1.2" | ||
Factor(true, (decimal)-1.2, 0, 1) | "(-1.2)" | |||
Factor(false, (decimal)1.2, 1, 3) | "3" | |||
Factor(false, (decimal)1.2, 7, -3) | "(-3/7)" | |||
Factor(false, (decimal)1.2, 3, -7) | (-2'1/3) | |||
Factor(false, (decimal)1.2, 8, 2) | "1/4" | |||
public static long gcd(long denominator, long numerator) | static | |||
gcd()辗转相除法,测试样例仅在单元测试中给予,未列于表格中 | ||||
private void Console_Integer() | private函数本身测试需要改写为public,而此三函数的主要作用是解析不同类型的字符串,转换为定义的Factor类的对象,但由于public void StringToFactor()本身是调用此三函数实现,因此仅对后者进行检测; | |||
private void Console_Factor() | ||||
private void Console_Decimal() | ||||
public void StringToFactor() | this | |||
这里掺杂大量黑盒测试和部分白盒测试,因为测试量相对较大,因此这里仅列出部分的测试,更为充足的黑盒测试请翻阅其他表格; | 17 | Factor(false, 0, 1, 17) | ||
0 | Factor(false, 0, 1, 0) | |||
003 | null | |||
-7 | Factor(false, 0, 1, -7) | |||
0/17 | Factor(false, 0, 17, 0) | |||
17/0 | null | |||
-3/7 | Factor(false, 0, 7, -3) | |||
3/-7 | null | |||
0'3/7 | null | |||
-2'3/7 | Factor(false, 0, 7, -17) | |||
01'5/7 | null | |||
3'-5/7 | null | |||
003.31415 | null | |||
3.141500000 | Factor(true, (decimal)3.141500000, 0, 0) | |||
.7 | null | |||
public Poiland(string str) | this | |||
此函数为读取表达式,简单验证其运算符合要求,再将其转换为后缀表达式;这里更准确地描述,测试样例主要通过黑盒测试的方式构造,因为此正则表达式的构造相对复杂,而且这其中也有一段非常有趣的经历,这里也仅列举必要的测试样例,具体详见其他表格 | Poiland("2 + 1 - (3+ 1)") | —— | ||
Poiland("(-2) + 3") | —— | |||
Poiland("(-2) + (-3)") | —— | |||
Poiland("21 - 2/5 × (-9 + 9.4 ÷ 4.7)") | —— | |||
Poiland("13 ÷ 12 + (8 - 7/12)") | —— | |||
Poiland("1 + (- 9)") | —— | |||
Poiland("× 13 - 9 + (2 - 3)") | ExpectedException(typeof(NotImplementedException)) | |||
Poiland("17 - 8 + (-9 + (9 - 1)") | ExpectedException(typeof(NotImplementedException)) | |||
Poiland("8 × -1") | ExpectedException(typeof(NotImplementedException)) | |||
Poiland("7 ×+ 7") | ExpectedException(typeof(NotImplementedException)) | |||
Poiland(" (+2) + 3") | ExpectedException(typeof(NotImplementedException)) | |||
Poiland("-2 + 3") | ExpectedException(typeof(InvalidOperationException)) | |||
Poiland("- 3 + 1") | ExpectedException(typeof(InvalidOperationException)) | |||
private bool comparePrior(char op_1, char op_2) | ||||
此函数为经典的运算符优先级比较函数,由于private类型且逻辑可由算法正确性直接证明,因此,不予测试 | ||||
private Factor calFactor(Factor number_1, Factor number_2, string op) | ||||
此函数调用此前测试的operation [+][-][*][/]方法,其测试样例的正确性可由此前的测试样例证明,同时代码本身的逻辑实现正确,可直接证明,因此,不预测试 | ||||
private void ConvertToPoiland() | ||||
此函数为中缀表达式转换为后缀表达式的算法实现,算法可予证明,而本身的正确性,不妨通过后缀表达式运算的正确性间接验证,因此,在测试过程中,若测试通过则默认此函数实现正确,否则再次单独设计调试样例进行测试 | ||||
public string getResult() | this | |||
此函数用以计算后缀表达式,并返回计算结果Factor类的ToString()形式,在计算过程中,由于中缀表达式检测是删去空格进行检测,因此部分能通过中缀表达式检测的运算式,可能无法计算,如:"9 +2"等形式,因此,与此前类似,仍添加大量黑盒测试保证其正确性 | Poiland("2 + 1 - (3 + 1)").getResult() | "(-1)" | ||
Poiland("(-2) + 3").getResult() | "1" | |||
Poiland("(- 2) + 3").getResult() | "1" | |||
Poiland("13 ÷ 12 + (8 - 7/12)").getResult() | "13 ÷ 12 + (8 - 7/12)" | |||
Poiland("1 + (- 9)").getResult() | ExpectedException(typeof(InvalidOperationException)) | |||
Poiland("1 +2").getResult() | ExpectedException(typeof(NotSupportedException)) | |||
Poiland("1 2.2").getResult() | ExpectedException(typeof(NotSupportedException)) | |||
Poiland(".2").getResult() | ExpectedException(typeof(FormatException)) | |||
Poiland("7/0").getResult() | ExpectedException(typeof(FormatException)) | |||
Poiland("1÷2").getResult() | ExpectedException(typeof(FormatException)) | |||
public void setting(int MinRange, ……,Random rand) | ||||
此函数用于设置生成表达式的关键参数,属于getter and setter函数类型,因此,不予测试 | ||||
private void CreateFactorList() | ||||
此函数用于求解所生成的数字范畴,因此,此部分测试采用白盒测试,但未体现于代码中,主要通过列举不同的range和number后,打印createNumber的值判断是否符合要求,具体的范围列举在右侧 | range | number << 3 | isFactor,isDecimal(false,—) | |
range | number << 3 | isFactor,isDecimal(false,—) | ||
range << 6 | number | isFactor,isDecimal(true,true) | ||
range << 6 | number | isFactor,isDecimal(true,true) | ||
range * (maxDenominator - 1) | number << 3 | isFactor,isDecimal(true,false) | ||
range * (maxDenominator - 1) | number << 3 | isFactor,isDecimal(true,false) | ||
private void Judgement_ISD_ISF(long createNumber, bool isRandom, int minDenominator, int maxDenominator, int MinRange, int MaxRange) | ||||
此函数用于生成符合要求的数据,由于此函数作用于最终的结果密切相关,因此,这里采用黑盒测试,而具体数据请翻阅其他表格 | ||||
public string CreateExpression() | ||||
此函数用于生成符合要求的表达式,由于此函数作用于最终的结果密切相关,因此,这里采用黑盒测试,而具体数据请翻阅其他表格 | ||||
public List<string> getResult() | ||||
此函数用于生成符合要求的表达式组,直接调用前面封装的函数实现,因此此函数正确性与前者密切相关,因此,这里采用黑盒测试,仅列出一组用以捕获异常的测试点,而具体数据请翻阅其他表格 | ||||
public bool setJudge() | ||||
此函数用以检查参数的正确性,由于if-else结构相对比较清晰,较易获取其全部分支,这里不列举出测试样例 |
表1:单元测试样例表格,包含部分代码复审的内容
如图5,ExceptionSolutions为异常处理类,主要将异常转换为用户能够理解的提示信息的字符串;Program为Main()方法入口;FormNumerical为UI界面文件,在单元测试不必测试,而对于其他类,其代码覆盖率均值高于90%,极差为16.44%,整体代码覆盖率符合预期。而具体的单元测试构造代码如上表所示。
图5:结对编程中黑盒测试四色测试方法
0x07 :UML图的设计思路整理与程序正确性证明
To the world,you maybe a person.
But to a person,you maybe the world.
图6 : UML类图的整体框架
图7-10 :图6中隐藏部分的UML类图的具体细节
这里根据UML图,来简单概述一下此次四则运算的需求分析和算法层次的实现。
0x0704:需求分析和项目工作
对于C#和四则运算生成和计算的进阶部分,首先我们确定基本的功能模块(http://www.cnblogs.com/jiel/p/4830912.html)
对于任意给定的表达式,能够识别并解析为标准的四则运算表达式,若匹配失败则需要反馈于用户具体的错误信息,若匹配成功,则需要尽可能快速地计算出正确结果
对于给定的一系列参数,我们能够读取用户所需求的参数,并生成给定参数的题目和答案,将它写入当前进程所在的文件夹;特别地,若参数不符合要求,我们需要反馈于用户具体的错误信息
对于给定的题目文件和答案文件,我们能够根据一定规则,对题目文件和答案文件进行多对多的检查,并返回最终的检查结果,输入到文件中;特别地,文件不存在或错误,我们需要抛出异常
ü 增加试题挑战功能,能够生成一定范围的题目,让用户输入,并在挑战结束后核算成绩,用以检测用户的四则运算水平(模拟试题功能)
ü 增加竖式表示功能,能够将输入的算式转化为竖式表示,方便使用此软件的人员,能够根据最经典的竖式计算,逐步熟悉计算过程
ü 增加一元或二元运算符,如sin, cos, log等基本运算符
ü 增加进制转换和进制选择功能
因此,由基本的功能模块,我们可以将此次结对编程项目的编码过程分解为阶段性的模块工作
在底层上,需要实现同时支持{数域:真分数,自然数,浮点数}{运算符:四则运算符,左右括号}的数据结构或类,由于我们选择.NET的面向对象过程的建模,我们需要实现基础类class Factor(),并定义基本的四则运算(通过运算符重载实现即可),同时实现Icomparable<Factor>接口,提高排序效率
在功能层次上,主要实现表达式的构造、生成和计算,并能通过中缀表达式向后缀表达式的运算实现计算结果的正确性
同时,我们能尽可能在参数设置的范围内更多实现表达式的构造,同时又必须保证不具备重复性,根据参数设定的环境不能被破坏
在前端层次上,通过经典的Winform界面反馈给用户,令用户能够友好地操作程序,实现所需要的功能
0x0708:Factor基础类的架构
public class Factor : IComparable<Factor>
{
public bool isDecimal; //isDecimal = true : double decimalNumber is valid
public decimal decimalNumber; //Save decimal number while (isDecimal = true)
public long denominator; //Save fraction with numerator/denominator
public long numerator;
}
由UML图的基本架构中,在Factor类的基础类架构中,我们通过isDecimal的bool类型的值确定存储为浮点数或分数(含整数),重载基本的四则运算符和负数运算符,同时重载Tostring()方法,实现CompareTo()方法,保证输出的标准型和排序的快速性;
因此,基于此数据结构的架构,我们同样隐性地做出了如下的定义
假分数实现基本的加减乘除运算,同时为尽可能扩大假分数的运算范围,我们在每调用计算的时候都会调用最小公约数gcd()函数(由于为O(log)级的算法,所以可以相对忽略此时间消耗),从而保证计算效率
在打印的过程中,将通过重载ToString()函数保证输出的正确性
因此,对于程序的正确性,首先我们需要从契约性设计的角度进行理论上的验证,对于修改了对象属性的方法,我们需要从理论上证明这种情况的改变是符合预期的,而类实现的过程也始终满足不变式的限制和整体Overview所展示的功能,同时,我们也通过覆盖性的单元测试,从浮点数,真分数,整数,带真分数四方面入手进行计算,运算结果符合预期,因此,通过单元测试,不妨默认程序在此测试样例的情况下正确运行,因此,在无特例的情况下不妨认为Factor类满足程序正确性。
0x070c: CreateList类的架构和其他
这里对于程序的正确性的证明在此前的博客中,已经提供了完整的论述,详见http://www.cnblogs.com/panacea/p/4831018.html,同时,此次的生成算法和上次的思路基本相同,但为了提高效率,削减此前由于数值无意义打表浪费的时间,这里做出了如下的优化策略:
CreatedList类主要实现随机四则运算表达式的生成,我们首先确定这样的基础思想,在任何range参数的取值情况下,生成数量与重复率成正相关,但在range参数足够大时,若通过随机化方法生成操作数和操作符随机组合,重复概率极小,理论计算基本处于1%%的数量级,甚至在随机化的处理方法上,表达式重复概率基本可以忽略不计,而若通过查重的方式逐步插入将占用大量的时间消耗,其消耗的CPU的资源率相当高,甚至在此前测试时占据96%的采样资源;
因此,这里我们从两方面入手分析这一问题,如何构造不重复的表达式,如何快速查询表达式的重复性
这里,我们采取子集生成的方式,对于随机的表达式集合,我们只生成其中的一部分子集,这里对子集做出如下的定义和证明:
表达式的操作符数和数字数具备“数字数 - 操作符数(不含括号) = 1”的数学关系,因此可将操作符随机选取依次穿插在数字间,生成中缀表达式
表达式的数字必须呈递减关系,且与非递增关系不同,我们必须确保生成的表达式的相邻三个数字不相同,从而保证表达式可以直接通过String.Equals()方法进行判断
对于减法关系的生成,在此前的策略中,我们通过限制操作符组合的方式完成了基本表达式的生成,但是,这里我们不妨采取通过减法前后表达式的对调完成正确表达式的生成
这里证明,对于交换性质的运算符+和×,对于A≥B,则A[+|×]B = B[+|×]A,当且仅当A=B,在此基本定理的情况下,可以证明直接通过String.Equals()方法实现表达式的查重
同时,我们重点讲述对此过程的优化空间
private void CreateFactorList()
{
long range = MaxRange - MinRange;
int maxDenominator = MaxRange > -MinRange ? MaxRange : -MinRange;
int minDenominator = MinRange <= 0 ? 1 : MinRange;
//isFactor : The Existence of Factor , false ::= only integer allowed
//isDecimal : The Existence of Decimal, false ::= only factor(including integer) allowed
long createNumber = (this.isFactor == false) ? ( range > (number << 3) ? (number << 2) : range ) //Number Of Integer
: this.isDecimal == true ? ( (range << 6) < number ? (range << 6) : (number << 2)) //Factor and Decimal
: range * (maxDenominator - 1) > (number << 3) ? (number << 2) : range * (maxDenominator - 1); //Only Factor
bool isRandom = (createNumber == (number << 2));
this.list = new Factor[createNumber];
Judgement_ISD_ISF(createNumber, isRandom, minDenominator, maxDenominator, MinRange, MaxRange);
}
图11:此前关于this.list生成的异常处理
在此前,我们通过了直接打表的方式生成足够数目的数字,而表达式直接通过调度list列表中的数字生成,避免重复数字的重复随机生成,提高生成数字的效率;但因此,当所生成的运算符数目过大时,会导致程序没有足够的内存存储相应的数组,而无法正确执行;
因此,在数组长度的选择中,此次我们通过打表和随机结合的方式保证数组中的数字足够生成充足数目的表达式。因此,这里我们折衷一种生成方法,当能够生成的数值个数远超过所需要的数值个数时,我们选择后者的一种运算结果作为数组长度,否则,我们将仅生成能够生成的数值个数。
0x0710:参数的控制渠道
参数控制需求 | 控制途径 | 具体模块 |
生成题目的数目 | 表达式生成 | CreateList.getResult() |
生成题目的范围 | 数值生成 | CreateList.CreateFactor() |
生成题目的运算符个数 | 表达式生成 | CreateList.CreateExpression() |
生成题目是否存在分数 | 数值生成,除法控制 | CreateList.CreateExpression(),CreateList.CreateFactor() |
生成题目是否存在浮点数 | 数值生成 | CreateList.CreateFactor() |
生成题目是否存在乘除法 | 符号生成 | CreateList.CreateExpression() |
生成题目是否存在负数 | 数值生成,减法控制 | CreateList.CreateFactor(),CreateList.CreateFactor() |
生成题目是否存在括号 | 表达式生成 | CreateList.CreateExpression() |
表 2 :参数控制渠道表
0x08 :一些有趣的事和虫
0x0804:正则表达式的的分组构造
关于正则表达式,此前不了解分组构造的策略时,对正则表达式的使用一般都较为繁琐
/* @ Unsigned
* @ Support : Pure-Unsigned-Integer-Number , Such As 1, 17
* @ Incorrect Input Screened: 001, -3, +7
*/
private static Regex regex_Unsigned_Integer = new Regex("^(([0-9]{1})|([1-9][0-9]+))$");
private static Regex regex_Signed_Integer = new Regex("^([+|-]?)(([0-9]{1})|([1-9][0-9]+))$");
/* @ Unsigned
* @ Support : Pure-Unsigned-Factor-Number , Such As 17'3/7, 7/17, 17/7, 0/7
* @ Incorrect Input Screened: +/-1/7, 1'0/7
*/
private static Regex regex_Unsigned_Factor = new Regex("^(([0-9]+[/][1-9]{1}[0-9]*)|([1-9]{1}[0-9]*['][1-9]+[/][1-9]{1}[0-9]*))$");
private static Regex regex_Signed_Factor = new Regex("^([+|-]?)(([0-9]+[/][1-9]{1}[0-9]*)|([1-9]{1}[0-9]*['][1-9]+[/][1-9]{1}[0-9]*))$");
/* @ Unsigned
* @ Support : 0.7, 1.7
* @ Incorrect Input Screened: +/-1.7, 00.7, 001.17
*/
private static Regex regex_Unsigned_Decimal = new Regex("^((([0-9]{1})|([1-9]{1}[0-9]+))[.][0-9]+)$");
private static Regex regex_Signed_Decimal = new Regex("^([+|-]?)((([0-9]{1})|([1-9]{1}[0-9]+))[.][0-9]+)$");
在此过程中,我们通过大量的正则表达式的验证来保证所截取的字符串符合需求,这其中浪费了大量的时间成本,而且代码的可读性非常糟糕,同时,对于类似情况的正则表达式,我们很难以描述清楚如下的状况,当数字为有符号数时我们需要保证左右括号必须同时存在,而此时繁琐的正则表达式也不符合需求,这时翻看MSDN,终于有了意外的收获:
https://msdn.microsoft.com/zh-cn/library/vstudio/bs2twtah.aspx
如上的连接中详细讲述了分组构造的方法,简而言之,我们通过模拟简单的堆栈情况来保证正则表达式的匹配,而类似的条件判断的表达式也可以以此书写:
(?(groupName)thenExpression|elseExpression)
据此,我们可以轻松此改写正则表达式,改写后如下所示:
^(?:
(?<sign>\([-+])?
(?:[0-9]+')?
[0-9]+(?:.[0-9]+)?
(?:/[0-9]+(?:.[0-9]+)?)?
(?(sign)\))
)$
这样匹配,可以极大增加正则表达式的可读性
特别鸣谢:https://stackoverflow.com/users/3764814/lucas-trzesniewski,关于正则表达式的见解清晰而准确
0x0808:OpenFileDialog与ThreadStateException
private void button_FileAns_Click(object sender, EventArgs e)
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.Filter = "(*.txt)|*.txt";
**** if (fileDialog.ShowDialog() == DialogResult.OK){}
}
如代码所示,我们在点击button_FileAns按钮后触发如下事件,而在这一过程中,我们尝试实例化OpenFileDialog的对象,保证“上传功能”的实现,但运行至****标记的语句时,程序触发ThreadStateException异常,而在找到的各式“OpenFileDialog”攻略,基本都做出了如下定义,而未见报错。在逐步了解Winform的机制后,发觉这属于典型的线程安全问题,因为任何显示UI的线程都将其声明为STA(单线程模式),同时,执行调度回路;因此,在此实例化后将“可能”导致主界面线程和新实例化线程的竞争,因此,这里我们需要改写此方法,保证这一过程线程安全;
具体解决方案可翻阅如下链接探索:
http://www.codeproject.com/Articles/841702/Thread-Apartment-Safe-Open-Save-File-Dialogs-for-C
0x09 :测试样例与代码分析
这里不妨给出,当时黑盒测试的测试样例表格,以此共勉当初面向对象的测试(泪奔T T)
输入表达式 | 输入类型 | 期望答案 | 实际结果 | 符合预期 | 是否存在说明不清楚 |
以下是非法输入 | |||||
N/A | 无输入 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
38 + 831 - | 末尾运算符 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
× 13 - 9 + (2 - 3) | 开头运算符 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
- 15/4 | 负号与数字分离 | 中缀表达式检测未通过 | -15/4 | 否 | |
1 + (- 9) | 负号与数字分离 | 中缀表达式检测未通过 | 程序崩溃 | 否 | |
.2 | 开头小数点 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | |
-14. | 末尾小数点 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | |
172.3.7 | 多余小数点 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | |
-3 × 9 + 9) | 右括号不匹配 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
17 - 8 + (-9 + (9 - 1) | 左括号不匹配 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
-8 - 1 % 2 | 非法运算符 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
10000000000000000000000000 | 长数字 | 数字过长 | 程序崩溃 | 否 | |
1 -2 | 运算符右侧未加空格 | 运算符未加空格 | -2 | 否 | |
8÷2.2 | 运算符两侧未加空格 | 运算符未加空格 | 解析数字时存在非法现象 | 否 | |
12 - 9+ 8 | 运算符左侧未加空格 | 运算符未加空格 | 解析数字时存在非法现象 | 否 | |
12 /2.2 | 分数线左侧多余空格 | 分数中有多余空格 | 解析数字时存在非法现象 | 否 | 应说明多余空格 |
2 9.2 | 数字间缺少运算符 | 缺少运算符 | 9.2 | 否 | |
8. 211 | 小数点后多余空格 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | |
8.(2) | 小数点后紧跟括号 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
13 ÷ 0 | 除数为0 | 除数不可以为0 | 1/0 | 否 | |
2.3/8 | 分数分子不为整数 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | 应说明分子应当为整数 |
4/-9 | 分数分母为负数 | 解析数字时存在非法现象 | 解析数字时存在非法现象 | 是 | 应说明分母应当为正整数 |
8/0 | 分母为0 | 分母不可以为0 | 解析数字时存在非法现象 | 否 | 应说明分母不可以为0 |
1000000000000000000 + 9000000000000000000 | 溢出 | 溢出 | -8446744073709551616 | 否 | |
7 ×+ 7 | 连续运算符 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
7 × + 7 | 运算符中间缺少数字 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
8 × -1 | 负数放中间时未加括号 | 中缀表达式检测未通过 | 中缀表达式检测未通过 | 是 | |
以下是合法输入 | |||||
218 | 正整数 | 218 | 218 | 是 | |
-12.7 | 负分数(小数形式) | -12.7 | -12.7 | 是 | |
21.00 | 末尾的0可以去掉的小数,输出时应去掉 | 21 | 21.00 | 否 | |
0/9 | 分子为0的分数,等于0 | 0 | 0 | 是 | |
2/8 | 非最简分数 | 1/4 | 2/8 | 否 | |
17 + 2.6 | 整数与小数加法 | 19.6 | 19.6 | 是 | |
-9 - 8/12 | 整数与分数减法 | -29/3 | -29/3 | 是 | |
0.4 + 2/7 | 小数与分数加法 | 24/35 | 0.6857142857142… | 否 | 计算规则规定 |
19 × (-8) | 整数乘法 | -152 | -152 | 是 | |
7 ÷ 2.1 | 整数与小数除法 | 10/3 | 3.3333333333333… | 否 | 计算规则规定 |
7 ÷ 21/10 | 整数与分数除法,与上题等价 | 10/3 | 10/3 | 是 | |
18 + 11 × 9 | 乘加混合 | 117 | 117 | 是 | |
11 × 9 + 18 | 与上题等价 | 117 | 38 | 否 | |
11.9 + (-2.448 × 1.5) | 小数加法与乘法 | 8.228 | 8.2280 | 否 | |
-2.448 × 1.5 + 11.9 | 与上题等价 | 8.228 | 10.952 | 否 | |
19 × (5 - 1) | 单重括号 | 76 | 76 | 是 | |
18 - ( 1 + (2 - 11) ) | 双重括号 | 26 | 26 | 是 | |
21 - 2/5 × (-9 + 9.4 ÷ 4.7) | 四则混合 | 23.8 | 23.8 | 是 | |
8 × 3 - 9 ÷ 3 | 四则混合 | 21 | 2 | 否 | |
8 × 3 - 9/3 | 与上题等价 | 21 | 2 | 否 | |
8 × 3 - 3 | 与上题等价 | 21 | 2 | 否 | |
-3 + 8 × 3 | 与上题等价 | 21 | 21 | 是 | |
(8 + 3) × (-8 - (-1)) | 四则混合 | -77 | -77 | 是 | |
14 + 8 + 8 × (11 - 9) - ((-3) + 29) | 四则混合 | 12 | 2 | 否 | |
13 ÷ 12 + (8 - 7/12) | 四则混合 | 17/2 | 389/12 | 否 | |
13/12 + 8 - 7/12 | 与上题等价 | 17/2 | -15/2 | 否 | |
13/12 - 7/12 + 8 | 与上题等价 | 17/2 | -15/2 | 否 | |
7/1 × 1/8 | 分数乘法 | 7/8 | 7/8 | 是 | |
7 ÷ 1 × 1 ÷ 8 | 与上题等价 | 7/8 | 7/9 | 是 | |
7/11 - 9/19 | 分数减法 | 34/209 | 34/209 | 是 | |
7 ÷ 11 - 9/19 | 与上题等价 | 34/209 | -85/19 | 否 | |
7 ÷ 11 - 9 ÷ 19 | 与上题等价 | 34/209 | -85/19 | 否 | |
7/11 - 9 ÷ 19 | 与上题等价 | 34/209 | 34/209 | 是 | |
2000 + 2000 + …(共17个)… + 2000 + 2000 | 较长表达式 | 17000 | 17000 | 是 |