结对编程项目

Posted on 2024-03-22 13:00  Aderversa  阅读(116)  评论(0编辑  收藏  举报
作业所属班级 软件工程4班
作业的要求 结对编程
我理解的作业目标 实践结对编程的流程

本项目由:张永祥(学号3122004546),黄俊杰(学号3122004525)

项目的GitHub链接如下:

正式版:
TimeP1ayer/SimpleArithmeticProgram: 自动生成小学四则运算题目的命令行程序 (github.com)

测试版:
测试版
测试版中包含了我们的所有代码

PSP Persional Software Process Stages 预计耗时(分钟) 实际耗时(分钟)
Planning 计划 10 5
- Estimate - 估计这个任务需要多少时间 10 5
Development 开发 920 915
- Analysis - 需求分析 10 5
- Design Spec - 生成设计文档 100 60
- Design Review - 设计复审(和同事审核设计文档) 20 15
- Coding Standard - 代码规范(为目前的开发指定合适的规范) 10 10
- Design - 具体设计 300 360
- Coding - 具体编码 400 360
- Code Review - 代码复审 20 30
- Test - 测试(自测、修改代码、提交修改) 60 75
Reporting 报告 40 30
- Test Report - 测试报告 20 10
- Size Measurement - 计算工作量 10 10
- Postmortem & Process Improvment Plan - 事后总结,并提出过程改进计划 10 10
总时间 930 920

需求分析

本此需求分析在题目里已经有了明确的说明,已经详细到不能再详细了。

该项目中,最为重要的就是查重模块了,为什么?

我们用不同的视角来思考一下。

如果我们的用户是老师

同一张试卷中遇到两道完全相同的试题,学生本人是十分兴奋的,因为一个解法拿双倍的分。但这样的话学生的能力就得不到锻炼

对于老师来说,在同一张试卷中出现相同的试题,那就是这个老师出题水平不行,这可能导致我们的程序对老师产生一种不利影响。

所以,考虑老师的使用体验,查重功能是必要的。


如果我们的用户的学生,他们通常渴望着刷题,所以寻求一种能快速生成大量题目的程序

同一张卷子里,如果程序给的试题中,有10%的题目是重复出现的,那学生本人会对这个生成的题目不满意

因为我本来可以练更多的,为什么你一直让我刷重复的题?

当然也不是说不能完全不重复,有时候隔很远出现一次重复,这对学生来说是很好的复习。但这样的功能,我们觉得也必须建立在程序本身具有查重能力的基础上。


所以综上,我们决定:我们的软件中必须要有查重题目的功能。

生成设计文档

这一次将系统设计成三个模块:

  1. 解释命令行参数模块:负责将接收到的命令行参数转化成实际程序可用的参数,并且会告诉使用该模块的人,什么功能由于收集到足够的参数,因而可以正常运转。
  2. 随机表达式生成模块:给它要生成的题目数 n 和数值的范围 r ,它就能给你生成一个 Map ,这里的键 Key 是表达式,Value 是表达式对应的答案。
  3. 输出和读取模块:负责在选择生成表达式功能时,将表达式和对应的结果按某种方式输出到 Exercises.txtAnswers.txt 中。

具体设计

解释命令行参数模块

对于参数-n,-r,采用正则表达式来判断其后面的参数是否为纯数字。

对于参数-e,-a,采用判断文件路径是否存在的方式来防止出现读取错误。

实现方式比较简单。

随机表达式生成模块

因为算法解释起来太长了,于是写了另外一篇博客,用来解释我们随机表达式生成模块的运行。这是一个最让人疯狂的模块好吧,花了我们两天的时间把查重的各个方面都考虑了一遍。

关于四则运算的一些想法 - onezhan - 博客园 (cnblogs.com)

大概就是利用表达式树进行表达式的生成,并利用算法规定好连续的乘法或加法下,数据序列的唯一。

接下来解释一下 Expression 应该是什么样的构成。我们的算法将利用类继承的多态性

也就是说,一棵表达式树,会有很多的结点,这些结点都是 Expression 的子类

表达式中数据的单元 Fraction 分数类,不论是整数还是分数,在本项目中全看做分数

Fraction 类负责管理实数的加减乘除。

这个类将提供一个方法,会根据分数对象自身的数据情况,选择输出分数或整数。不用担心分数会输出成假分数,因为这个方法会保证不管分数是真分数还是假分数,都输出真分数。

有了这个类,我们就可以从分数等数据运算中解脱出来,只用关心:怎么样搭建一棵数学意义和计算机意义一样的表达式树。

Add()Sub()Mul()Div() 这三个方法帮助我们进行分数的运算,外界不用关心他们是否需要简化分数,类内会帮外界完成这些琐碎的事。

Great()Equal() 提供了分数之间比较的方法

由于生成表达式的过程中,可能会出现除法的分母表达式结果为零的情况,所以提供了判断这个分数对象是不是零的方法 isZero()

ToString() 会将将根据自身的数据状况,输出符合需求的数值(可能是整数、也可能是真分数)

基类 Expression

属性:

  1. left(Expression):用于存储左表达式子树
  2. right(Expression):用于存储右表达式子树
  3. maxLeaf(Fraction):本表达式树的最大叶子节点,这个东西算法需要
  4. result(Fraction):这棵表达式树的结果,是一个分数
  5. opType(枚举值):提供一个判断结点类型的依据,这是算法所需要的

方法:

  1. getResult():String -> 返回一个结果的字符串形式,实际上就是调用了result分数对象的 ToString()
  2. getExp():String -> 将左子树和右子树的表达式用 “operator” 按一定的算法拼接。本方法设定上是虚方法,需要子类的具体实现

方法我们只介绍重要的,其他的都是外界获取对象内部数据的一种方法,没什么好讲的。

Expression 的子类实现中,我们的打算很简单,重写类的构造函数和getExp()就可以满足项目的需求了

下面我们只挑出 SubExpression 这个比较简单的来介绍,更复杂的其他类,请了解我们上面给出的文章。

SubExpression类

构造函数

SubExpression(Expression left, Expression right);

对该类的构造函数进行如下规范:在传入的left,right中,选择一棵result大的表达式树作为左子树。

这样就做的话就能保证用 - 号连接在一起的表达式,一定是正数。

getExp()函数

取得左子树的表达式 leftExpression.getExp() 并根据我们的算法决定加括号还是不加括号

右子树在某些情况下必须加括号。

输入输出模块

答案的读取:

文本读取采用UTF_8格式。

采用正则表达式读取作答的答案与原题目答案进行对比,将结果输出到一个ArrayList里,再交由其他模块进行结果的写入。

写入文件时均采用覆写方式。

最终软件实现的效果

对原来的乘法符号,和加法符号进行了修改,并且调整的一下表达式符号之间的间隙。

同优先级下实现了括号的随机加入。

能在一定程度上对表达式进行数学意义上的查重。

但我们项目的表达式生成功能在r过小,n过大时候会有问题:这样可能会触及排列组合的极限,导致我们程序的生成表达式循环一直到达不了用户的要求。

(也就是说,一直在随机生成已经有了的表达式,表达式的数量增长不了,满足不了用户的要求,因此先入死循环)

这个极限大概是这样的:

r=2 时,约为 960

r=3 时,约为 4255

r=4 时,超过了 12000 (后面增长一个表达式所要时间非常长,所以就选择了一个算法能到达的值)

这就是查重要付出的代价,但对于试题来说是必要的,因为考试的试题中,老师是不希望有两道解题方式基本一样的题目的。

所以,当 r<4 请谨慎选择您要生成的题目数量,您当然可以选择一个非常大的 n 算法也能正常工作,但效率会极低。

因此我们会在循环超过一定次数后,强制结束循环。

为了简单说明,我们只用一个简单了例子说明对比答案模块的功能

其实就是将答案文件的答案提取,然后将练习文件所写答案提取出来

最后对比两个提取出来的字符串就完了

单元测试(使用JUnit4)

我们为每一个单元尽最大努力模拟了用户可能的输入

命令行解释模块

public class CommandUtilTest {

    @Test
    public void getParameter() {
        CommandUtil c = new CommandUtil();

        //正常输入参数
        String args[] = {  "-r","70"    ,       "-n","80"          ,"-a","-el","-e","-xp"};
        c.getParameter(args);

        System.out.println("A:"+c.getA());
        System.out.println("E:"+c.getE());
        System.out.println("R:"+c.getR());
        System.out.println("N:"+c.getN());

        System.out.println("-----------");

        //缺少不必要参数
        CommandUtil c1 = new CommandUtil();
        String args1[] = {"-n","80","-r","70","-e","-xp"};
        c1.getParameter(args1);

        System.out.println("A:"+c1.getA());
        System.out.println("E:"+c1.getE());
        System.out.println("R:"+c1.getR());
        System.out.println("N:"+c1.getN());

        System.out.println("-----------");

        //缺少必要参数
        CommandUtil c2 = new CommandUtil();
        String args2[] = {"-n","80","-e","-xp"};
        c2.getParameter(args2);

        System.out.println("A:"+c2.getA());
        System.out.println("E:"+c2.getE());
        System.out.println("R:"+c2.getR());
        System.out.println("N:"+c2.getN());
        System.out.println("-----------");

        //参数缺少

        CommandUtil c4 = new CommandUtil();
        String args4[] = {  "-r",      "-n"          ,   "-a","D:\\Users\\Desktop\\exp.txt"    ,   "-e","-xp"   };
        c4.getParameter(args4);

        System.out.println("A:"+c4.getA());
        System.out.println("E:"+c4.getE());
        System.out.println("R:"+c4.getR());
        System.out.println("N:"+c4.getN());

        System.out.println("-----------");

    }
}

随机表达式生成模块

public class RandomExpressionTest {
    @Test
    public void ToStringTest() {
        // 测试整数
        Fraction f1 = new Fraction(2, 1);
        System.out.println(f1.ToString());
        // 测试真分数
        Fraction f2 = new Fraction(1, 6);
        System.out.println(f2.ToString());
        // 测试假分数
        Fraction f3 = new Fraction(16, 6);
        System.out.println(f3.ToString());
        // 分子和分母相等
        Fraction f4 = new Fraction(3, 3);
        System.out.println(f4.ToString());
        // 测试负数
        Fraction f5 = new Fraction(-1, 3);
        System.out.println(f5.ToString());
        Fraction f6 = new Fraction(1, -3);
        System.out.println(f6.ToString());
        Fraction f7 = new Fraction(3, -2);
        System.out.println(f7.ToString());
        // 测试分母为零
        try {
            Fraction zero = new Fraction(1, 0);
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }
        // 对分数加法的测试
        // 正数的加法
        Fraction a1 = new Fraction(2, 1);
        Fraction a2 = new Fraction(1, 4);
        System.out.println(a1.Add(a2).ToString());
        // 存在负数的加减法
        a1 = new Fraction(-1, 2);
        a2 = new Fraction(2, 1);
        System.out.println(a1.Add(a2).ToString());
        // 对分数的减法测试
        a1 = new Fraction(1, 4);
        a2 = new Fraction(2, 1);
        System.out.println(a1.Sub(a2).ToString());
        System.out.println(a2.Sub(a1).ToString());
        // 对分数的乘法进行测试
        a1 = new Fraction(3, 4);
        a2 = new Fraction(-4, 3);
        System.out.println(a1.Mul(a2).ToString());
        a2 = new Fraction(4, 3);
        System.out.println(a1.Mul(a2).ToString());
        // 对分数的除法进行测试
        a1 = new Fraction(3, 4);
        a2 = new Fraction(2, 5);
        System.out.println(a1.Div(a2).ToString());
        a1 = new Fraction(2, -5);
        System.out.println(a1.Div(a2).ToString());
    }
    @Test
    public void CreateExpressionTest() {
        CreateExpression a = new CreateExpression(30, 2);
        Map<String, String> tmp = a.getExpressionAndResult();
        for(String exp : tmp.keySet()) {
            System.out.println(exp);
        }
    }
}

输入输出模块

public class ReadUtilTest {

    @Test
    public void countLine() {
        System.out.println("line:"+ ReadUtil.CountLine("D:\\Users\\Desktop\\exp.txt"));
    }

    @Test
    public void getResult() {
        System.out.println(ReadUtil.GetResult("D:\\Users\\Desktop\\exp.txt",3,"."));
        System.out.println(ReadUtil.GetResult("D:\\Users\\Desktop\\exp.txt",5,"="));
    }

    @Test
    public void ResultCompare(){

        ArrayList<Integer>[] GetCompareResult = ReadUtil.ResultCompare("D:\\Users\\Desktop\\result.txt","D:\\Users\\Desktop\\result2.txt");
        ArrayList<Integer>Correct = GetCompareResult[0];
        ArrayList<Integer>Wrong = GetCompareResult[1];
        System.out.print("Correct:");
        for (int num:Correct){
            System.out.print(num+" ");
        }

        System.out.println();
        System.out.print("Wrong:");
        for(int n:Wrong){
            System.out.print(n+" ");
        }

    }

    @Test
    public void writeExpression() {
        CreateExpression a = new CreateExpression(10,10);
        Map<String, String> tmp = a.getExpressionAndResult();

        WriteUtil.WriteExpression(tmp,"D:\\Users\\Desktop\\exp.txt","D:\\Users\\Desktop\\result.txt");

    }

    @Test
    public void CompareResult(){
        WriteUtil.WriteCompareResult(ReadUtil.ResultCompare("D:\\Users\\Desktop\\result.txt","D:\\Users\\Desktop\\result2.txt"),"D:\\Users\\Desktop\\Compareresult.txt");
    }

}

性能测试

给了这么一串参数,性能测试的结果如下:

String[] args0 = {"-n", "100000000", "-r", "1000"};

耗费最多的就是 FractionExpression 及其子类,这是生成大量表达式以及查重模块所带来的代价。当生成的题目数量越多时,Expression 生成的表达式树就会越多,为了进行查重,存起来的表达式树就会越来越多,因此当数据规模足够大时,该算法对内存的消耗量也是非常大的。

但考虑到我们只要求一万道题目的数量,而我们所做测试是一千万道题目的数量,所以可以想象大概是本次性能测试的所有资源消耗量,都几乎会下降一千倍,那这样的性能消耗,对于我们来说就是可以接受的。

总结

张永祥

对本次项目进行总结:

(1)项目的成功之处:

  • 高效沟通:在结对编程中,我们保持了高效的沟通,及时解决了编程过程中遇到的问题。

  • 互相学习:通过互相监督,我们互相学习了对方的编程技巧和思维方式,提高了自身的编程水平

(2)不足之处:

  • 没有足够严格地按照结对编程的方法解决问题
  • 因为前期没有定义好相关规范、任务,导致开发过程中有种撕裂感,有种在完成单人项目的感觉。

(3)经验分享

  • 在团队中,合理的系统设计文档,能有效组织人员进行开发,让每个人的能力得到充分利用。

黄俊杰

对本次项目进行总结:

(1)项目的成功之处:

  • 高效沟通:在结对编程中,我们保持了高效的沟通,及时解决了编程过程中遇到的问题。
  • 互相学习:通过互相监督,我们互相学习了对方的编程技巧和思维方式,提高了自身的编程水平
  • 使用新方式:第一次使用git进行多人项目的管理,代码版本管理良好

(2)不足之处:

  • 没有足够严格地按照结对编程的方法解决问题
  • 对git的使用方法不熟练,个人代码版本更新有可能因为交流滞后,造成代码冲突,在提交和merge时出现问题
  • 我比较像摸鱼,对算法设计基本没有贡献

Copyright © 2024 Aderversa
Powered by .NET 8.0 on Kubernetes