BUAA软工-结对项目作业
阅读原文时间:2022年03月14日阅读:1

结对项目作业

项目

内容

这个作业属于哪个课程

2020春季计算机学院软件工程(罗杰 任健)

这个作业的要求在哪里

结对项目作业

我在这个课程的目标是

通过这门课锻炼软件开发能力和经验,强化与他人合作的能力

这个作业在哪个具体方面帮助我实现目标

体验结对编程的模式

  • 教学班级:006(周五)

由于GUI生成的文件过大(20MB)因此将代码和生成的GUI界面分别放在了两个仓库

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

5

5

· Estimate

· 估计这个任务需要多少时间

5

5

Development

开发

1,160

1,160

· Analysis

· 需求分析 (包括学习新技术)

180

180

· Design Spec

· 生成设计文档

15

15

· Design Review

· 设计复审 (和同事审核设计文档)

10

10

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

10

10

· Design

· 具体设计

45

45

· Coding

· 具体编码

600

600

· Test

· 测试(自我测试,修改代码,提交修改

240

240

Reporting

报告

30

40

· Test Report

· 测试报告

10

10

· Size Measurement

· 计算工作量

10

10

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

10

20

合计

1,195

1,205

​ 这次在构思代码上想了很久,学习Qt花费的时间比较多,因此总花费的时间也比较多,时间都用在学习新知识上了。

​ 信息隐藏、接口设计和松耦合在面向对象课程中都有学习、应用过,在这里再次应用了。

  • Information Hiding

    David Parnas在1972年最早提出信息隐藏的观点。他在其论文中指出:代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的。

    ​ 信息隐藏原则本意是希望类里面定义的变量和结构应当按照一定的原则分配可见性,从而防止模块内容被恶意篡改,但考虑到这次作业的规模,以及对象类型,每个几何元素更倾向于类似结构体的结构,并不是一个真正的模块,因此在结对编程中我们考虑了以后还是将类里面的属性定义成了public,减少了代码的复杂性。

  • Interface Degisn

    接口设计有六大原则:

    单一职责原则:应该有且仅有一个原因引起类的变更。

    里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。

    依赖倒置原则:面向接口编程

    接口隔离原则:建立单一接口,不要建立臃肿庞大的接口。

    迪米特法则:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。

    开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

    ​ 这次作业并没有体现出很多面向对象的特性,但是我们也遵循了迪米特法则、里氏替换原则等,除了防止一个函数过于冗长而拆分成几个小函数之外,各个模块的功能独立。

  • Loose Coupling

    一个松耦合的系统中的每一个组件对其他独立组件的定义所知甚少或一无所知。

    ​ 本次作业在函数调用中,每个通信的参数都是基本类型的参数,可以直接调用,没有必要为互相的实现考虑。

    //判断是否为数字,否则抛出异常
    __declspec(dllexport) bool isNum(std::string s);
    
    //判断范围是否合理,否则抛出异常
    __declspec(dllexport) bool rangeVaild(int n);
    
    //检查直线类型输入是否合法,是则更改x1, x2, y1, y2的值,否则抛出异常
    __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x1, int& y1, int& x2, int& y2);
    
    //检查圆类型的输入是否合法,是则更改x, y ,r的值,否则抛出异常
    __declspec(dllexport) void inputCheck(ifstream& fileIn, int& x, int& y, int& r);
    // 计算两直线的交点
    __declspec(dllexport) Point* calLineLineIst(Line line1, Line line2);
    
    // 计算圆与直线的交点
    __declspec(dllexport) vector<Point> calLineCircleIst(Line line, Circle circle);
    
    // 计算两圆交点
    __declspec(dllexport) vector<Point> calCircleCircleIst(Circle circle1, Circle circle2);
    
    //计算交点
    __declspec(dllexport) MySet calculate(ifstream& fileIn, ofstream& fileOut);
    
    //line类,代表直线
    class Line;
    
    //Ray类,代表射线,继承了Line类
    class Ray;
    
    //Segment类,代表线段,继承了Ray类
    class Segment;
    
    //Circle类,代表圆
    class Circle

    ​ 各个接口的作用已在注释中阐明,实现如下:

    • isNum:通过正则表达式来检查一个数字是否合法

    • rangeVaild:通过return n > -100000 && n < 100000来检查范围的合法性

    • inputCheck:通过使用以上两个函数来完成对输入数据的检查并赋值,重载了两个函数,分别针对圆和直线

    • calLineLineIst:使用公式法来求直线交点

    • calLineCircleIst:使用公式法来求直线交点

    • calCircleCircleIst:将两圆交点转换成圆与直线的交点,调用calLineCirclelst来求解

    • calculate:根据文件流处理输入、使用以上6个函数来检查输入合法性、求解交点,并输出

    • Line类:设计了两个方法,一个是检查直线是否平行,另一个是检查直线是否重合,都使用了公式法

    • Ray类:继承了Line类,重写了父类检查是否重合的方法,并增加了检查点是否在射线上的方法

    • Line类:继承了Ray类,重写了检查重合与点在线段上的方法

      计算模块部分沿用了上一次作业的计算方式,因此没有变化,我采用的是我的搭档的计算方法:博客

      需要增加的关键方法为检查交点是否在线段/射线上,以及两条线段\直线\射线之间重合的情况,这个比较复杂,也是通过数学的计算以后分情况讨论。

    ​ 考虑到射线和线段是一种特殊的直线,因为它们可以看成是直线截断形成的,因此产生了继承的想法,子类通过重写父类方法来实现自己的个性。

    ​ 性能分析结果:

    ​ 其中消耗最大的函数为Calculate,而unordered_set的维护了耗费了近一半的时间,为此我也查阅了关于容器效率的资料但是似乎库函数提供的已经是比较优的算法了,而计算手段上也通过减少浮点类型的运算进行了浅层的加速,inputCheck等函数则是为了异常处理而牺牲的性能。

    • Design by Contract

      ​ 即契约式设计,在面向对象课程中专门有一个单元让我们通过JML来体会契约式设计的思想,其强调前置条件、后置条件与不变式,是一种形式约束。也就是说,只要满足了这个条件,那么所设计的模块在理论上就一定是正确的,非常可靠,但是缺点是契约撰写的成本比较高,在复杂的模块中会很麻烦,也变得不易阅读。

    • Code Contract

      ​ 和DBC的思想类似,优点也是使得模块变得可靠、安全,但是缺点是需要牺牲一部分的性能,增加代码的复杂度,降低运行效率。

    本次作业中为了提高编码效率,我们没有过多地使用契约式设计的思想,在一开始就明确定义每个模块的需求,可以有更高的效率。

    单元测试部分和上次类似,但是增加了异常处理部分和直线与射线交点部分,例如测试数字是否合法:

    TEST_METHOD(isNumTest) {
            Assert::IsTrue(isNum("0"));
            Assert::IsTrue(isNum("1"));
            Assert::IsTrue(isNum("100"));
            Assert::IsTrue(isNum("-1"));
            Assert::IsTrue(isNum("-100"));
            Assert::IsFalse(isNum("001"));
            Assert::IsFalse(isNum("-001"));
            Assert::IsFalse(isNum("a"));
            Assert::IsFalse(isNum("0a"));
            Assert::IsFalse(isNum("-0a"));
    }

    通过构造错误的样例各1例来检查正则表达式是否正确

    在其他模块的测试也是类似,构造正确样例和错误样例来检查模块功能是否正确,对于没有异常抛出计算模块则和上次作业一样分情况讨论:

    单元测试覆盖率:

    异常处理模块我设计了8种异常

    名称

    定义

    例子

    输出

    TFException

    输入图形个数过少

    1

    请输入至少两个图形!

    DSException

    用来确定直线两点重合

    L 1 1 1 1

    用来确定直线的两点不能重合!

    SLException

    两条直线有无穷的交点

    S 1 1 3 3
    R 0 0 2 2

    有两个几何图形之间有无穷的交点

    TException

    图形种类错误

    K 1 2 3 4

    支持的图形种类仅为:C, L, S, R

    INException

    输入非整数

    L 001 a 3 2

    坐标请输入一个(-100000, 100000)之间的无前导0标准整数

    RIException

    圆的半径不合法

    C 1 1 -2

    圆的半径不可以小于或等于0或者大于或等于100000

    ArgumentError

    参数数量不对

    argc != 5

    请检查命令格式: \n\tintersect.exe -i -o \n

    FileError

    打开文件失败

    文件不存在

    打开文件失败!

界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')

本项目的图形化界面采用 VS + Qt 进行开发的,图像的绘制主要使用 QPainterpaintEvent机制。下面按照开发的时间顺序,对GUI的设计过程进行详细介绍。

  1. 分析需求,确定图形化界面的组件和布局
  2. 动手编程,实现各组件之间的触发逻辑
  3. 测试与美化

    最终版的GUI

界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')

  1. DLL调用:DLL调用花费了较大的精力,最初我们企图使用 .dll+.h 的显示调用方法,利用QLibraryload 方法加载DLL库,但是很不幸,失败了。于是我们选用了 .dll+.h+.lib 的隐式调用方法,最终成功加载了DLL,具体调用方法如下,

  2. 完成计算:这一步很简单,就是调用计算模块已经封装好的接口函数,输入图形容器,输出交点容器,遍历交点容器进行交点绘制即可,代码如下

    if (!ui.radioButton->isChecked()) { return;    }
    //s_graphics中存放的是当前列表中的图形参数
    if (s_graphics.size() > 0) {
        s_graphics.insert(s_graphics.begin(), to_string(s_graphics.size()));
        for (int i = 0; i < s_graphics.size(); i++) {
            qDebug() << QString::fromStdString(s_graphics.at(i));
        }
        try {
            m_Points = result(s_graphics);
            qDebug() << m_Points.size();
            for each (Point var in m_Points) {
                painter.drawPoint(QPointF(var.x * m_scale, var.y * m_scale));
                qDebug() << var.x << var.y;
            }
            ui.label->setText("Total: " + QString::number(m_Points.size()));
        }
        catch (const std::exception& e) {
            qDebug() << e.what();
        }
    }
  3. 最终实现的功能

    • 支持从文件导入几何对象的描述

    • 支持几何对象的添加、删除、修改

    • 支持绘制现有几何对象(请见上图)

    • 支持求解现有几何对象交点并绘制

    • 其他功能:右上角缩放滑块可以调整画布大小,滑动条可以调整画布交点。

​ 我们使用了腾讯会议进行交流:

结对编程

同伴

优点

提高编码效率、在开发阶段能尽早发现bug、交流效率较高

编码效率高、编程基础扎实、思考较仔细

乐于学习新知识、接受能力强、交流积极

缺点

容易造成思维定势,两人都无法发现bug、出现分歧较难处理

耐心不足

有点粗心

模块之间的松耦合

在博客中指明合作小组两位同学的学号,截图展示互换后的运行结果和测试结果。此外,博客中还需分析两组不同的模块合并之后出现的问题,为何会出现这样的问题,以及是如何根据反馈改进自己模块的。

  1. 合作小组同学的学号

    我方

    对方

    LJC: 17373456 WXC: 17373459

    SYB: 17373452 SXD: 17231151

  2. 互换后的运行结果与测试结果

    完成对接的项目地址:https://github.com/AmanogawaSaya/IntersectGUI

    在项目文件中的 “DLL_对接” 文件夹中,分开存放了我方的DLL文件与对方的DLL文件,将想要测试的DLL文件复制到与 CoopWorkGUI.exe 同级的目录下,双击.exe即可运行.

    • A 的核心模块,加上 B 的测试模块和用户界面模块(命令行和 GUI)

    • B 的核心模块,加上 A 的测试模块和用户界面模块(命令行和 GUI)

     由于双方的接口函数都是void类型的函数,因此我们以最终结果为依据进行了测试,测试结果均正确。
  3. 合并后出现的问题分析