高小强的汽车暴晒场 2018-04-19
这次的结对编程作业是编写给小学生出四则运算的题目,我们组的任务是编写核心模块 core ,给 UI 组提供 API。在需求分析阶段和编码阶段,我们都遇到了很多问题,在经过一些试错和重构之后,我们最终得到了一个比较好的实现。
在需求分析阶段,我们一起阅读了老师和助教给出的需求文档,提取业务逻辑。
我们最初对业务逻辑的理解:
生成合法的四则运算题目
定制题目类型
生成题目
检查题目合法性
计算四则运算表达式
计算表达式
因此,程序应该分为两部分,一部分是生成合法的表达式字符串,另一部分是独立的计算程序。
但是,对于第一部分,我们不仅要生成随机字符串表达式,还要检测它的合法性;对于第二部分,我们还需要写一个词法分析、语法分析器。
但是后来,经过仔细分析之后,我发现我们要做的,其实只有一个功能,就是生成合法的四则运算的题目和答案。
因此,我们对需求的理解调整如下:
生成合法的四则运算的题目和答案
定制题目类型
生成题目
生成答案
题目合法性检查
理解了以上核心需求,我们的思路就清晰了很多。
有了需求,就要确定核心模型,我们选择了抽象语法树对四则运算表达式进行建模。
对抽象语法树,我们的了解如下:
抽象语法树
求值算法:递归求值子树,再处理运算符
随机生成算法:随机产生一颗树
产生文本表达式:可以顺着 BNF 表达式进行文本生成
实现:代数数据类型,递归类型
有了以上模型,我们对整个程序的架构就有了信心:只要把每个部分完成,我们的程序就完成了。这种自顶而下的设计方法,既可以保证代码质量不会太糟糕,也可以让我有一种安稳感。
下面的工作就是完成抽象语法树的各个部分。因为预见到我们需要一种分数数据类型来进行符号计算,所以,我们在这里又采用了自底而上的设计方法。先不管抽象语法树怎么实现,把分数数据类型做出来再说。于是,赵瑞同学写了 fraction
类,并进行了单元测试。
我们还缺少一些脚手架。因为在 c++ 中没有原生支持的代数数据类型,我们也不太确定能不能用 c++17。因此我选择了用 tagged union
的方式来模拟代数数据类型。
有了上面的工具类,我们就可以以很直接的方式去解决问题了。
经过这次结对编程作业,我发现在架构设计和代码编写的过程中,最好的事情就是“以最直接的方式解决问题”,即我们在写程序时操纵的对象的抽象程度必须恰到好处,应该和人类平常思考的方式接近。例如,你要造小区,你操纵的对象最好就是一栋一栋的房子,而不是一块一块的砖。自顶而下的设计方法的好处就在于,它可以让你专注于你要解决的问题,而且你面对的对象是在当前抽象层次下最合适的对象。但有时候系统太过复杂,很难做到自顶而下的分析。当自顶而下的分析遇到困难时,如果你知道某些小工具是一定有用的,不妨先把这些小工具迅速完成,并对它们进行单元测试,这样可以保证你以后用到它们的时候,排查bug时可以不用管它们,这对开发也是很有帮助的。
对于测试,我们一开始的方式是输出表达式和我们给出的结果,例如:
1+2+3
和 6
然后我们手算或用计算器计算1+2+3
,和我们给出的结果进行比较。
后来,我发现可以利用 Python 对表达式进行求值,于是,我的测试给出了以下结果:
eval('1+2+3')
6
然后在只需要把 eval 那部分代码贴在 Python repl 中,我们就可以得到答案,进而可以和我们程序给出的答案进行对比。
但这都还不不够好,于是,我又把输出改成了这样
assert(eval('1+2+3') == 6)
于是,只需要用 python 执行一下程序产生的脚本,就可以测试我们的程序是否正确。这是我们能测试百万级数据的基础。
经过这次结对编程中,我学到了用程序进行代码测试的方法,并打算在之后的学习工作中积极应用这种方法。
这次和赵瑞同学结队编程,我们轮流做领航员,架构设计过程中产生的冗余设计和编码过程中犯的低级错误都被很容易地发现了。尤其值得一提的是,我们测试的时候配合得非常好,通过git,我们迅速同步代码修改。有时候有一些特定的测试脚本需求,这时,我就负责输出测试代码并让python测试,找到反例后,又把反例交给赵瑞同学进行单步跟踪,我们这样做效率非常高。