结对编程项目文档前端
/* 本文档主要包含了项目分析、设计等各方面内容,是为后期写博客时直接摘抄准备的。因为博客容许共享项目相关的部份内容,所以,尽可能将可共享的内容写到此文档中。 */git
根据这次的需求,咱们将以前我的项目时的需求分析表进行了进一步完善,将这次新增的需求填入其中,结果以下。算法
|
功能编程 |
质量数据结构 |
约束架构 |
业务级需求并发 |
程序核心封装为Core库:模块化 支持计算表达式结果函数 支持生成表达式工具 支持参数设定 |
发生错误时支持Exception |
|
用户级需求 |
自动生成四则运算,支持整数和真分数(以及负数) 支持括号 题目存入Exercises.txt中 同时生成题目答案,存入Answers.txt中 能够判断答案对错 支持一万道题目的生成 支持参数设定题目中: 题目数量 数据范围 最大运算符数量 (或运算过程当中)有无分数 是否有乘除法 是否有括号 (或运算过程当中)有无负数 |
用户输入有误时程序不崩溃 错误输入时需输出帮助信息 |
生成的算式需知足: 减法计算无负数 除法计算结果为真分数 运算符不超过3个 +和*交换不重复 真分数五分之三表示为3/5 真分数二又八分之三表示为2’3/8 Exercises.txt文件格式以下: 1. 四则运算题目1 2. 四则运算题目2 …… Answers.txt文件格式以下: 1. 答案1 2. 答案2 判卷结果输出到文件Grade.txt,格式以下: Correct: 5 (1, 3, 5, 7, 9) Wrong: 5 (2, 4, 6, 8, 10) |
开发级需求 |
写出至少10个测试用例确保你的程序可以正确处理各类状况。 须要有单元测试 |
算法复杂度在O(n)到O(nlogn)左右 通过Code Quality Analysis工具的分析并消除全部的警告 采用TFS进行管理 每一个阶段完成后均需进行回归测试 单元测试覆盖率足够高 |
采用C++或者C#语言实现,可使用.Net Framework 运行环境为32-bit Windows 7或8 分为五个阶段,每一个阶段均须要一次提交: 计算及生成接口 “设置”功能接口 支持各种Exception 界面、测试、库的松耦合 改进程序并发布 |
在接口的设计中,咱们尽可能保持信息隐藏、松耦合等原则。为了提高模块的内聚性,下降和外部的耦合性,咱们只暴露出最必要的数据结构和接口函数。对于外界无需了解的任何信息,咱们的接口中都不会将其暴露出去。如下是咱们遵循这些想法设计的结果
算式采用如下的接口表达,经过标准的ToString进行输出。经过value()能够得到以分数形式表示的值,经过numValue则能够得到相应的double类型的值。
interface ExprAction { Fraction value(); double numValue(); }
因为外部不须要改变表达式自己,所以,咱们只须要将该接口暴露给用户便可。至于该接口具体怎样实现,则是内部完成的。用户所须要的只是表达式的值以及输出该表达式而已,这已经足以知足用户全部的需求,所以,咱们没有再暴露更多的东西给外部。
public static ExprAction getExpr(String str)
经过字符串构造表达式。因为表达式支持返回答案的功能,故而这一接口可用于实现计算。
public static List<ExprAction> generate(int n, int r)
生成必定数量的表达式,参数n为题目数量,r为范围限制
上述两个接口也保持了咱们以前的设计思路,彻底经过ExprAction这个接口来将表达式的相关信息给用户,而不暴露任何多余的内容。这有利于咱们维护内部的算法细节。例如,在实现的过程当中,咱们的生成算法几经调整,但除了对generate函数内部进行了一些改变以外,没有影响其他的任何部分。
设置经过settings函数进行,经过如下结构体将具体的配置信息传入到库中
public class Configure : ICloneable { // 最大运算符数目 public int MaxOperatorNum { get { return maxOperatorNum; } set { maxOperatorNum = value; } } // 运算过程当中可否有分数 public bool Fraction { get { return fraction; } set { fraction = value; } } // 是否容许乘除法 public bool MulAndDiv { get { return mulAndDiv; } set { mulAndDiv = value; } } // 是否容许括号 public bool Parentheses { get { return parentheses; } set { parentheses = value; } } // 是否容许运算中出现负数 public bool Negative { get { return negative; } set { negative = value; } } }
咱们认为,采用单独的结构体传递数据相较于XML来讲有这样几点优点。XML在传递设置的时候须要进行必定量的解析工做,而采用结构体是不须要的。在耦合性上,XML中的内容、设置等字段与咱们的库也是紧密耦合的,因此,在咱们看来XML就是一个以XML形式写成的结构体。咱们最终仍是会将XML解析成一个结构体的,与其这样,不如直接用结构体传递信息。
咱们认为,即便最终的配置经过XML储存在磁盘上,那么解析XML的步骤也应当由前端来完成,而不该该由库自己来完成。库只负责接收设置并根据这一设置进行生成。而不该该牵扯到与用户相关的细节上。
这是咱们最终设计库的接口时采用结构体的理由。
在对输入的字符串进行解析时,咱们采用了文法制导的解析方式。这种方式实现较为简单,只需按照文法中的非终结符号进行自顶向下的递归分析便可构造出表达式所对应的语法树。所以,咱们参考需求中给出的文法,将整个文法补齐。
<e> := <sf> | <e> + <e> | <e> − <e> | <e> × <e> | <e> ÷ <e> | (<e>)
<sf> := <f> | -<f>
<f> := <digit> | <digit>’<digit>/<digit> | <digit>/<digit>
<digit> := <n> | <n><digit2>
<n> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
然而,咱们发现因为有+/-和*/÷两个优先级,直接采用上述文法没法有效地按照运算的优先级构造出正确的表达式树。乘除法和加减法是不一样优先级的,构建表达式树时,先计算的应当放在下面,后计算的放在上面,经过上述文法进行语法分析,是难以达到这一要求的。
所以,咱们改写了上述文法,以<e>来表示+/-这一优先级,<term>来表示*/÷这一优先级。这样就可以经过语法分析来构建出知足优先级关系的表达式树了。改写后的文法以下:
<e> := <e> + <term> | <e> − <term> | <term>
<term> := <term> × <factor> | <term> ÷ <factor> | <factor>
<factor> := <sf> | (<e>)
<sf> := <f> | -<f>
<f> := <digit> | <digit>’<digit>/<digit> | <digit>/<digit>
<digit> := <n> | <n><digit2>
<n> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
但该文法具备左递归性,没法直接进行自顶向下分析。因此为了使用自顶向下分析,咱们又对该文法进行消除左递归的处理。消除左递归后的文法以下:
<e> := <term><e2>
<e2> := + <term> <e2> | − <term> <e2> | ε
<term> := <factor><term2>
<term2> := × <factor> <term2> | ÷ <factor> <term2> | ε
<factor> := <sf> | (<e>)
<sf> := <f> | -<f>
<f> := <digit> | <digit>’<digit>/<digit> | <digit>/<digit>
<digit> := <n> | <n><digit2>
<n> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
采用上述文法进行分析,便可构造出输入的表达式的表达式树。
咱们将表达式的数据结构设计为一颗二叉树。每一个叶节点为一个单独的数,每一个内部节点为一个子表达式。整个表达式树按照中序遍历进行计算。在咱们的设计中,咱们将表达式树的内部节点和叶节点抽象为了ExprAction这个接口。
Fraction value();
double numValue();
value()函数会将表达式的值以Fraction,即咱们自定义的分数类的形式返回。而numValue则是其所对应的double值。numValue()主要用于单元测试。咱们将按照分数的计算法则算出来的值,与咱们直接使用浮点数计算出来的表达式的值进行比较,以肯定咱们的分数计算是否正确。
咱们的Fraction类(叶节点)和Expression类(内部节点)这两个类实现了这一接口。
咱们发现,四则运算的计算过程当中的每个步骤,实际上都是在根据左右两个表达式的值,计算当前表达式的值。所以整个计算过程只涉及Fraction直接的运算。所以,咱们重载了Fraction类的四则运算操做符。
对于表达式树的设计,还有一点须要补充的是:Fraction和Expression是不可变对象。一旦被构造,则其对外表现出的状态是永远不变的。这样设计简化了代码实现的难度,也提高了程序的稳定性。
首先咱们提出一种不须要判重且能够生成所有不等价式的线性算法。
首先定义狭义上的一元的概念,一元就是指知足值域要求的天然数、真分数,而后狭义上的二元式就是指两个一元和一个操做符。
那么咱们发现,在此基础上定义的三元式,四元式,多元式,他们的计算过程的最后一步必然是两个能够用一元表示的值按照最后剩下的操做符进行运算,也就是说,任何多元式均可概括为二元式,而此时,一元就不只仅是指狭义上的一元,而是包含了狭义上的二元式,多元式。
判断二元式的等价是很是简单的,只要使之+和*运算不知足交换律便可。
那么,咱们只要由不等价的一元生成二元式,由一元和不等价的二元式生成三元式和四元式,那么这些式子必定都是不等价的。或者说,就是由广义上不等价的元生成全部广义上不等价的二元式。
给出其数学证实:
首先定义元a,元集A。二元式b,二元式集B。运算符f,运算符集F。
A := 知足值域要求的 天然数、真分数 | B;
B := AFA;
F := ‘+’|’-’|’*’|’/’;
若是b1=a1 f1 a2,b2=a3 f2 a4属于B,则:
b1与b2不等价的充分条件是:
f1 != f2 或
f1 == f2 且 f1 == ‘+’ 或 ‘*’ ,f1(a1,a3) != f2(a3,a4) && f1(a1,a3) != f2(a4,a3)
由以上定义和推理咱们能够概括出不生成重复表达式的线性算法,即用全部一元构造狭义二元式(即两个一元和一个运算符组成),以后用狭义二元式和一元构造足 够数目的广义二元式(即二元式自己也可做为元),只要在递归和迭代的生成过程当中,任意二元式知足不重复的充分条件,则必定没有重复的二元式。
如今咱们已经有了一个高效的线性生成算法,然而咱们按照需求把它调整成随机化。
由上述算法不难看出,他是由少元向多元生成的,因此直接打乱顺序的方法是不行的。
因此咱们采起这样的作法,首先生成全部的狭义一元,以后把这些一元打乱顺序,以后每次取极小一部分一元进行生成,并将生成结果存储,以后再取一小部分一元,先于其自身生成,以后再将其结果与上次生成的全部表达式组合进行生成,所有生产完以后将这些第二次生成的表达式合并到第一次的结果中,重复此操做直到知足个数或者所有生成完成为之。
最后因为咱们采起的数据结构,直接这样输出仍是有必定的线性规律,以后再把全部结果打乱一次顺序便可。
这种生成算法效率极高,30000个表达式的生成耗时1s左右。
咱们对代码进行了细致而详细的单元测试。单元测试对于咱们代码的正确性有很大的帮助。因为TFS始终链接出错,所以,咱们暂时采用了git进行版本控制。每次进行git pull拉取下来同伴的代码后,咱们都会跑一遍全部的单元测试,以确保合并正确,而且本地新增的代码没有致使原有功能的倒退。
咱们的代码总体的覆盖率比较高。对于一些比较重要的类,咱们的覆盖率更是高达90%以上。多数没有被覆盖到的代码都是与随机生成相关的,因为其自己的随机性,难以达到单元测试的结果能够重现的要求。因此,这部分代码咱们并无进行单元测试。其他的相对较容易测试的代码,咱们都进行了相应的单元测试。特别是解析表达式、带分数等关键的类,咱们都尽量地覆盖到了全部分支。
结对编程可让双方的代码效率和代码质量都有所提升,由于是两我的合做,因此不想让本身的工做进度耽误了对方,因此都加紧开发,而且为了能让两方的模块能链接起来,而且让对方能读懂本身的代码,因此代码结构,代码注释都会有提升。而且结对编程中遇到一些问题时两我的的思路会更加广阔和灵活。并且,每一个人在编程中擅长的方面不一样,结对编程可让双方合理分工,取长补短。
结对编程也有缺点,两我的在理解对方代码时总多多少少要花些时间,若是一我的作的代码架构,另外一我的没有理解或者应用,极可能致使整个工程的代码混乱,因此队员之间必定要作好交流。
我在结对编程中的优势,我对一些核心算法有一些本身独到的思考,可以构建出高效的算法,而且我开发效率比较高,能够快速解决问题,写出代码。
我在结对编程中的缺点:我对架构,模块化,封装这些概念上的理解不够深入。
队友在结对编程中的优势:超级牛逼的架构师,前期对软件的架构,模块化的封装,各部分的关系作了简洁的处理,使我在构建算法的时候很是轻松,而且让后来的调用和异常处理也变得异常简单。
在接口的设计中,咱们尽可能保持信息隐藏、松耦合等原则。为了提高模块的内聚性,下降和外部的耦合性,咱们只暴露出最必要的数据结构和接口函数。对于外界无需了解的任何信息,咱们的接口中都不会将其暴露出去。外界对内部的改变只能经过一些特定的调用完成,而且经过深拷贝,使得其外部不能有在设计以外的操做。而且将全部的模块的接口都封装到一个类中,方便调用和操做。
请见上面的概要设计章节。