2020软工结对项目做业-简单几何形状间交点统计

2020软工结对项目做业-简单几何形状间交点统计

项目 内容
课程连接 2020春季计算机学院软件工程(罗杰 任健)
做业要求 结对项目做业
课程目标 系统学习软件开发理论和流程,经过实践积累软件开发经验
本博客的收获 开发了一个简单带有UI的项目,总结结对编程过程的经历和优缺点
教学班级 005
项目地址 https://github.com/zwx980624/IntersectProject
UI地址 https://github.com/zwx980624/IntersectionGUI
队友地址 xgnb

1、估计将在程序的各个模块的开发上耗费的时间

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务须要多少时间 30 20
Development 开发
· Analysis · 需求分析 (包括学习新技术) 720 600
· Design Spec · 生成设计文档 60 60
· Design Review · 设计复审 (和同事审核设计文档) 60(同时) 60(同时)
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 20
· Design · 具体设计 80 60
· Coding · 具体编码 600 500
· Code Review · 代码复审 600(同时) 500(同时)
· Test · 测试(自我测试,修改代码,提交修改) 300 400
Reporting 报告
· Test Report · 测试报告 30 30
· Size Measurement · 计算工做量 30 30
· Postmortem & Process Improvement Plan · 过后总结, 并提出过程改进计划 240 300
合计 2110 2020

2、结对编程中是如何利用Information Hiding,Interface Design,Loose Coupling方法设计接口

  • information Hiding 是指信息的隐藏和封装python

    In computer science, information hiding is the principle of segregation of the design decisions in a computer program that are most likely to change, thus protecting other parts of the program from extensive modification if the design decision is changed. The protection involves providing a stable interface(computer_science) which protects the remainder of the program from the implementation (the details that are most likely to change).c++

    从维基百科中,咱们能够看出information Hiding是指外部人员在能够调用接口而没必要关心内部的实现,即便内部实现发生变更,对外部人员也不会产生任何影响。在咱们设计一些接口时,就采用了这种思想。好比处理输入的接口能够接受文件、字符串、标准输入等任意输入流,调用接口者不须要知道咱们内部是怎么处理的,就能够完成对输入图形的处理和保存。还有GUI使用的guiProcess接口,只须要传入字符串和保存交点的vector,不须要额外构建该接口内部使用的各类类,便可完成交点计算。git

  • interface Design 指接口设计,包含单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、最少知识原则、开闭原则等。github

    咱们根据以上原则的一部分,设计了以下两个计算核心的接口:正则表达式

    • guiProcess算法

      函数传入符合格式的字符串,交点集用指针返回,交点个数用返回值返回。编程

      • 由数据格式的设计原则,只使用c++标准库中的容器,而不用自定义数据类型。如CPoint要转成std::pair<double,double>表示,保证调用者清晰理解。
      • 输入采用特定格式的字符串,符合方便易用原则,若是格式错误,由核心处理并抛出异常
    • cmdProcess为了命令行调用方便,采用需求规定的命令行输入格式进行输入输出canvas

  • 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. Subareas include the coupling of classes, interfaces, data, and services.数据结构

    咱们的dll在抛出异常时会将错误信息存储为标准异常,别人在调用时,不须要专门使用咱们设计的异常类,就能够捕捉异常,能够减少耦合度。

3、4、计算模块设计与实现过程及UML。

(一)PipeLine

PreProcess

  • ReadShape:读取文件接收所有输入的直线和圆
  • Shape construct:根据输入构建形状对象,计算直线斜率。
  • Classified by Slope:按斜率将直线分组存起来。
  • CalcIns Same Slope:处理在同一直线上的线段、射线的共端点状况。

CalcIntersect

  • CalcLines:计算全部直线、射线、线段之间的交点:
    • 依次考虑每一个平行组,按每条线遍历计算交点。平行组内的线不用计算交点。
    • 查交点表,若是存在,就能够不求同一交点的其余线了。
      交点表:Map<点,Vector<线>>
      维护交点表:新增的交点加入交点表,线加入表中对应的线集
    • 射线和线段的交点还要知足在射线和线段范围内才有效
  • CalcCircles:全部线算完后,再一个个遍历圆。
    • 暴力求其与以前图形的所有交点
    • 一样须要考虑射线和线段的范围问题

(二)类间关系图

  • CIntersect类:实现控制流,方法包含输入计算两图形交点计算交点总数
  • CShape类:图形类基类,为每一个图形实例建立惟一id,并记录图形的类型
  • CLine类:继承图形类基类,做用为表示直线、线段、射线的代数方程参数。
  • CCircle类:继承图形类基类,做用为表示圆的代数方程参数。
  • 直线方程两种表示方法
    • 通常方程:\(Ax + By +C = 0\)
    • 斜截方程:\(y = kx + b\)
    • 圆方程两种表示
      • 通常方程: \(x^2 + y^2 + Dx + Ey +F = 0\)
      • 标准方程: \((x-x_0)^2 + (y-y_0)^2 = r^2\)
  • CSlope类和CBias类:为解决斜率无穷大设计,isInf和isNan为true时表示直线的斜率为无穷,此时k和b的具体值无效。因为要按斜率分组,采用C++STL的unordered_setunordered_map,CSlope要实现Hash方法等于运算符
  • CPoint类:表示交点,做为map的key,一样须要实现Hash方法等于运算符

(三)关键函数

  • inputShapes: 处理输入函数,直线、射线、线段按斜率分组,放到map<double, set<CLine>>_k2lines中,圆直接放到set<CCircle>_circles里。

    新增:上一次需求中直线和直线平行不可能产生有限交点,可是这次需求新增的线段和射线就可能产生共线相交在端点的状况,是符合需求说明书的状况,须要特殊考虑。在此函数中实现,后续就能够正常按照平行分组计算了。

  • calcShapeInsPoint:求两个图形交点的函数,分三种状况,返回点的vector。

    • 直线与直线
    • 直线与圆
    • 圆与圆
  • cntTotalInsPoint: 求全部焦点的函数,按先直线后圆的顺序依次遍历求焦点。已经遍历到的图形加入一个over集中。

    • 直线两个剪枝方法:
      • 砍平行:依次加入每一个平行组,不需计算组内直线交点,只需遍历over集中其它不平行直线。
      • 砍共点:倘若ABC共点,按ABC的顺序遍历,先计算了AB,交点为P;以后计算AC时发现交点也是P,则无需计算BC交点。方法为维护_insp2shapes这个map<CPoint, vector<CShape>>数据结构,为交点到通过它的线集的映射。
    • 再依次遍历圆,暴力求焦点。加到_insp2shapes
    • 函数返回_insp2shapes.size()即为交点个数。

(四)代码说明

具体说明本次需求新增的重要代码

1. 判断求交公式算出的交点在射线、线段范围内

// require: cx cy belongs to the line
// return: if point in shape range
bool CLine::crossInRange(double cx, double cy) const
{
	if (type() == "Line") { // 统一接口,直线返回true
		return true;
	}
	else if (type() == "Ray") { // 射线
		if (k().isInf()) { // 斜率无穷,比较y值,dcmp为浮点数比较函数,定义见下
			if (dcmp(cy, y1())*dcmp(y2(), y1()) != -1) {
				return true;
			}
		}
		else { // 正常状况,比较x
			if (dcmp(cx, x1())*dcmp(x2(), x1()) != -1) {
				return true;
			}
		}
		return false;
	}
	else { // 线段
		... //相似于射线,代码略
	}
}
// 浮点数比较函数,相等返回0,前者大返回1,不然返回-1
#define EPS 1e-10
int dcmp(double d1, double d2) {
	if (d1 - d2 > EPS) {
		return 1;
	}
	else if (d2 - d1 > EPS) {
		return -1;
	}
	else {
		return 0;
	}
}
2. 浮点数hash方法

浮点数因为有浮点偏差,其hash值将不一样,因此应该截取某精度,转换成整形进行hash。

#define COLLISION 100000.
class SlopeHash
{
public:
	std::size_t operator()(const CSlope& s) const
	{ // 乘精度,四舍五入,转整形,算Hash
		unsigned long long val = dround2ull(s.val() * COLLISION);
		return std::hash<bool>()(s.isInf()) + (std::hash<unsigned long long>()(val) << 16);
	}
};

5、计算模块接口部分的性能改进。

咱们随机生成了8000条几何图形数据用于性能测试,其中直线、线段、射线、圆各2000个,最终计算获得交点数为18175002个交点。随机数生成模块为python中random包。

第一版性能测试

VS2019中使用性能探查器,分析CPU利用率以下:

能够看出,耗费时间最多的函数,是计算输入全部图形的交点总数的函数cntTotalInsPoint,进入函数入内部查看分析结果:

与我的项目相似,耗费时间最多的仍然是记录交点的数据结构set对交点的插入。因为已经使用unordered_set相比于本来的set时间复杂度已经低了不少,所以固有的数据结构维护时间不可避免。其次咱们还注意到计算两个图形间交点的函数calcShapeInsPoint占用了较大的时间开销。进入该函数中查看分析结果:

能够看出判断点是否在线段或射线上,花费时间较多,判断点是否在交点上的函数,内部是这样的:

仔细想一想后,发现其实彻底没有必要再这个函数内部再判断一次点是否在线端或射线所处直线上,由于咱们自己会用到这个函数的场景,就是先计算出线段或射线所在直线与其余直线的交点,再判断交点是否在线段或射线上,所以,这个判断属于画蛇添足,是一个能够优化的点。

优化后性能分析

删除点是否在线上的判断后,再次使用性能分析,结果以下:

能够发现总时间下降了4~5s,再次进入calcShapeInsPoint函数中查看

能够发现crossInRange函数已经再也不是花费最多的部分,说明仍是颇有效果的。其他部分优化也都相似,不断对比分析,删除冗余计算结果。

6、看 Design by Contract,Code Contract 的内容,描述这些作法的优缺点,说明你是如何把它们融入结对做业中的

Design by Contract,也称契约式设计:

It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants. These specifications are referred to as "contracts", in accordance with a conceptual metaphor with the conditions and obligations of business contracts.

简单来讲,就是为每一个接口写好各类使用的规范,包括调用前的规范和调用后的结果规范。在曾经的OO课上,咱们接触并学习过写函数的约束和规范,我的感受优势和缺点以下:

  • 优势:经过书写前置条件、后置条件和不变式这种逻辑化的语言,能够避免开发人员对模块的实现产生歧义,其次经过合理的设计约束,可让模块的耦合度更低,更加符合上述提到的三个设计原则。

  • 缺点:须要花费大量时间,而且对开发人员的素质要求高。好比本次做业过程当中,咱们就没有严格使用标准的前置、后置语句,而是使用通俗化的语言描述,不然又是一笔巨大的时间开销。

咱们的pipeline函数就采用了契约式设计模式,为了提升效率简化过程,咱们采用了比较书面化的语言来描述规范,而没有严格遵循标准得contract格式。如如下几个函数:

// calculate Intersections of one circ and one line
// need: para1 is CCirc, para2 is CLine
// return a vector of intersections. size can be 0,1,2.
std::vector<CPoint> calcInsCircLine(const CShape& circ, const CShape& line)
{	... }
// calculate all intersect points of s1 and s2
// return the points as vector
// need: s1, s2 should be CLine or CCircle.
// special need: if s1, s2 are CLine. They cannot be parallel.
std::vector<CPoint> CIntersect::calcShapeInsPoint(const CShape& s1, const CShape& s2) const
{ ... }
// the main pipeline: loop the inputs and fill in _insp2shapes or _insPoints
// return the total count of intersect points
// need: _k2lines and _circles have been filled
int CIntersect::cntTotalInsPoint() { ... }

在设计时咱们都是先写出契约,以后的代码实现中严格遵照契约中的需求,好比使用calcShapeInsPoint函数时就必须遵照下面需求,不然将产生错误。

// special need: if s1, s2 are CLine. They cannot be parallel.

7、计算模块部分单元测试展现

首先展现使用OpenCppCoverage插件测试获得代码覆盖率为:

其中部分Uncovered line是因为VS强大的编译优化,把一些函数在内联处理了,因此执行不到。

在询问了一些同窗而且到网上查询以后,咱们仍是没有找到如何直接将VS的单元测试项目加入OpenCppCoverage的代码覆盖检测中,所以咱们选择将单元测试代码手动从单元测试项目中移入主函数中。主要测试结构以下:

int main() {
    ...
    ...
    //从单元测试项目中转移至此的单元测试1
    ...
    //从单元测试项目中转移至此的单元测试2
    ...
}

单元测试中,主要包括文件读写的测试,对一些关键函数如计算交点等的测试,对异常的测试。

部分单元测试代码展现以下:

  • 测试图形之间交点的计算:该部分比较繁杂,须要考虑的状况有如下几种,我的项目中已经出现过的就再也不放测试代码,与上次相似

    • 直线与直线仅有一个交点、没有交点,其中须要包括直线斜率不存在、为0、其余的状况

    • 直线与圆有两个交点、一个交点、没有交点,其中须要包括直线斜率不存在、为0、其余的状况

    • 直线与线段有一个交点、没有交点,其中须要包括直线与线段平行、不平行有一个交点、不平行没有交点的状况。如下例子为一个交点的状况:

      CLine t1 = CLine(0, 2, 0, 0, "Seg");
      CLine t2 = Cline(3, 2, 4, 2);
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
    • 直线与射线有一个交点、没有交点,其中须要包括直线与射线平行、不平行有一个交点、不平行没有交点的状况。如下例子为没有交点的状况:

      CLine t1 = CLine(0, 2, 0, 0, "Ray");
      CLine t2 = Cline(3, 2, 3, 4);
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(0, (int)ans.size());
    • 圆与圆有两个交点、一个交点、没有交点,其中须要包括外离、外切、相交、内切、内含的状况

    • 圆与线段有两个交点、一个交点、没有交点,其中须要包含线段所在直线与圆相离、相切、相交以及不一样交点数的状况。如下例子为两个交点的状况:

      CCircle c = CCircle(0, 0, 2);
      CLine t1 = Cline(2, 0, 0, 2, "Seg");
      ans = ins.calcShapeInsPoint(t1, c);
      Assert::AreEqual(2, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
      Assert::AreEqual(true, CPoint(2, 0) == ans[1]);
    • 圆与射线有两个交点、一个交点、没有交点,其中须要包含射线所在直线与圆相离、相切、相交以及不一样交点数的状况。如下例子为一个交点的状况:

      CCircle c = CCircle(0, 0, 2);
      CLine t1 = Cline(0, 0, 0, 2, "Ray");
      ans = ins.calcShapeInsPoint(c, t1);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 2) == ans[0]);
    • 线段与线段有一个交点、没有交点,其中须要包含两个线段所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑线段与线段共线时端点重合的状况。如下例子为共线时一个交点的状况:

      CLine t1 = Cline(0, 0, 0, 2, "Seg");
      CLine t2 = Cline(0, -2, 0, 0, "Seg");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
    • 线段与射线有一个交点、没有交点,其中须要包含线段与射线所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑线段与射线共线时端点重合的状况。如下例子为共线时一个交点的状况:

      CLine t1 = Cline(0, 2, 0, 0, "Seg");
      CLine t2 = Cline(0, 0, 0, -2, "Ray");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(1, (int)ans.size());
      Assert::AreEqual(true, CPoint(0, 0) == ans[0]);
    • 射线与射线有一个交点、没有交点,其中须要包含两个射线所在直线之间的各类关系以及不一样交点数的状况,须要特别考虑射线与射线共线时端点重合的状况。如下例子为共线时没有交点的状况:

      CLine t1 = Cline(0, 0, 2, 0, "Ray");
      CLine t2 = Cline(-1, 0, -2, 0, "Ray");
      ans = ins.calcShapeInsPoint(t1, t2);
      Assert::AreEqual(0, (int)ans.size());
  • 交点数统计测试:测试各类状况下,如不一样图形之间有重合交点、图形数量不多、图形数量不少等,交点统计是否正确。举例:

    TEST_METHOD(TestMethod10)
    {
    	ifstream fin("../test/test10.txt");
    	if (!fin) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	ins.inputShapes(fin);
    	int cnt = ins.cntTotalInsPoint();
    	Assert::AreEqual(433579, cnt);
    }
  • 异常单元测试:对各类异常状况的单元测试,主要是经过构造异常数据传入输入函数,捕捉其抛出的异常与预期异常的信息进行比较,将在第八章中详细说明,这里仅放出几个样例。

    • 测试射线与射线重合的状况(其中之一)

      TEST_METHOD(TestMethod_RR1)
      {
      	string strin = "2\nR 0 0 5 5\nR 6 6 -1 -1\n";
      	ShapeCoverException s(2, "R 6 6 -1 -1", "R 0 0 5 5");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }
    • 测试射线与线段重合的状况(其中之一)

      TEST_METHOD(TestMethod_SR1)
      {
      	string strin = "3\nS 0 0 0 4\nS 0 0 4 0\nR 0 -2 0 -1";
      	ShapeCoverException s(3, "R 0 -2 0 -1", "S 0 0 0 4");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }
    • 测试线段与线段重合的状况(其中之一)

      TEST_METHOD(TestMethod_SS1)
      {
      	string strin = "3\nS 0 -1 0 1\nS 0 3 0 6\nS 0 0 0 2\n";
      	ShapeCoverException s(3, "S 0 0 0 2", "S 0 -1 0 1");
      	InputHandlerException std = s;
      	istringstream in(strin);
      	if (!in) {
      		Assert::AreEqual(132, 0);
      	}
      	CIntersect ins;
      	try {
      		ins.inputShapes(in);
      		Assert::AreEqual(132, 0);
      	}
      	catch (InputHandlerException e) {
      		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
      	}
      }

8、计算模块部分异常处理说明

计算模块异常处理大体分类以下,均继承c++标准异常:

  • 输入异常:
    • 有关图形数量的异常:如没法读入N、N不符合规范、N与图形数量不匹配等
    • 有关图形的异常:如输入图形为非法格式、数字范围超出约束、图形重复输入、图形与已有图形重合、线类图形输入的两点重合等
  • 计算异常:目前并无发现须要特殊捕捉的计算过程当中产生的标准异常以外的异常,待将来拓展。
  • 文件读写异常:没法打开文件、文件不存在等异常。

异常类的UML图以下:

关键异常详细介绍:

  • ShapeNumberException

    当没法从输入流中读入N、读入的N范围不符合规范、N与实际输入的图形数量不匹配时,抛出该异常。

    该异常构造方式与标准异常相同,传入错误信息字符串,可使用what()函数获取错误信息,获取到的错误信息与传入错误信息相同。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_N6) {
    	string strin = "1\nL 1 2 3 4\nC 1 1 2";
    	ShapeNumberException s("The number of graphics is larger than N.");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }
  • IllegalFormatException

    当读入过程当中某行既不是空行、也不符合规定的输入格式即(L <x1> <y1> <x2> <y2>R <x1> <y1> <x2> <y2>S <x1> <y1> <x2> <y2>C <x> <y> <r>四种格式中的一种)时,抛出该异常。

    该异常构造方式为,传入格式错误的图形序号和该行字符串,可使用what()函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_ILLShape)
    {
    	string strin = "1\n\nL 1 2 3 F\n";
    	IllegalFormatException s(1,"L 1 2 3 F");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }
  • OutRangeException

    当读入图形信息时,图形的参数超过规定的范围,抛出该异常。

    该异常构造方式为,传入参数超过规定范围的图形序号和该行字符串,可使用what()函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_OutRange)
    {
    	string strin = "1\n\nC 1 1 0\n";
    	OutRangeException s(1, "C 1 1 0");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }
  • ShapeRepeatedException

    当读入某一图形,发现与已有图形彻底相同时,抛出该异常。

    该异常构造方式为,传入出现重复的图形序号和该行字符串,可使用what()函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_ShapeRe)
    {
    	string strin = "2\n\nC 1 1 1\nC 1 1 1\n";
    	ShapeRepeatedException s(2, "C 1 1 1");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }
  • IllegalLineException

    当读入直线、线段、射线,且发现描述线类图形的两个点重合时,抛出该异常。

    该异常构造方式为,传入端点重合的图形序号和该行字符串,可使用what()函数获取错误信息,获取到的错误信息为序号、字符串以及相应修改建议。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_IllLine)
    {
    	string strin = "2\n\nL 1 1 1 2\nL 0 1 0 1\n";
    	IllegalLineException s(2, "L 0 1 0 1");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }
  • ShapeCoverException

    当读入直线、线段、射线等与已有线类图形重合(即有无限个交点)时,抛出该异常。

    该异常构造方式为,传入该图形(指新读入的图形)序号、该图形字符串信息、与其重合的图形字符串信息,可使用what()函数获取错误信息,获取到的错误信息为序号、该图形、与其重合的图形信息及相应修改建议。如:

    单元测试举例以下:

    TEST_METHOD(TestMethod_RS1)
    {
    	string strin = "3\nS 0 0 4 0\nR 0 -2 0 -1\nS 0 0 0 4";
    	ShapeCoverException s(3, "S 0 0 0 4", "R 0 -2 0 -1");
    	InputHandlerException std = s;
    	istringstream in(strin);
    	if (!in) {
    		Assert::AreEqual(132, 0);
    	}
    	CIntersect ins;
    	try {
    		ins.inputShapes(in);
    		Assert::AreEqual(132, 0);
    	}
    	catch (InputHandlerException e) {
    		Assert::AreEqual(true, strcmp(std.what(), e.what()) == 0);
    	}
    }

9、界面模块的详细设计过程

首先给出界面模块的总体外观。看这可爱的外星人

(一)界面模块设计

  • 输入面板模块:提供添加、删除、清空功能

    • 添加:提供文件导入和手动添加两种方式,添加后在列表框中显示,并绘制到绘图模块中
    • 删除:在列表框中勾选,点击删除键便可删除选中图形,并自动在绘图模块中删除
    • 清空:清空全部图形,并清空绘图面板
  • 计算核心接口:添加图形后,点击求解交点调用计算核心模块,返回交点个数对话框,并在绘图面板中绘制交点,后文将详述接口函数的设计。

  • 绘图面板模块:在添加图形时绘制图形,在计算交点后绘制交点。

  • 错误反馈模块:反馈计算核心模块抛出的异常,例如

    • 直线两点重合

    • 产生无数交点

    • 输入数据范围错误

(二)关键代码说明

本项目的GUI采用c++的Qt库实现,对比之前使用过的MFC,Qt有易上手,跨平台,界面美观等特色。对于不多写图形程序的我来讲,采用Qt是一个很合适的选择。下面分别介绍各个模块的关键代码实现。

  • 输入面板模块:充分利用Qt ui设计器提供的诸多功能强大的组件

    • 整个输入面板置于可浮动拖出的Dock组建,对于屏幕分辨率低的用户,可将输入面板分离出主窗口,让绘图面板占据所有主窗口
    • 输入数据部分选用QcomboBox和QspinBox等组件
    • 按钮利用qt提供的QtoolButton组件,方便定义样式和在工具栏重用

    • 代码主要定义添加,删除,清空按钮的槽函数,以文件添加为例展现代码
void IntersectionGUI::on_actAddFile_triggered() {
	if (lastPath == QString::fromLocal8Bit("")) { //记录上次打开的路径
		lastPath = QDir::currentPath();//获取系统当前目录
	}
	//获取应用程序的路径
	QString dlgTitle = QString::fromLocal8Bit("选择一个文件"); //对话框标题
	QString filter = QString::fromLocal8Bit("文本文件(*.txt);;全部文件(*.*)"); //文件过滤器
	QString aFileName = QFileDialog::getOpenFileName(this, dlgTitle, lastPath, filter);
	if (!aFileName.isEmpty()) {
		lastPath = aFileName;
    // 读入文件数据,文件格式与需求定义相同
		std::ifstream fin(aFileName.toLocal8Bit());
		int N;
		std::string line;
		fin >> N;
		std::getline(fin, line);
		while (N--) {
			std::getline(fin, line);
			QString qline = QString::fromStdString(line);
			if (!isShapeStrValid(qline)) { // 正则表达式简单判断输入格式
				QString dlgTitle = QString::fromLocal8Bit("输入格式错误");
				QMessageBox::critical(this, dlgTitle, qline);
				break;
			}
			draw_shape_from_str(qline); //在绘图面板上绘图
      // 图形列表中添加一行
			QListWidgetItem * aItem = new QListWidgetItem(); //新建一个项
			aItem->setText(qline); //设置文字标签
			aItem->setCheckState(Qt::Unchecked); //设置为选中状态
			aItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
			ui.listWidget->addItem(aItem); //增长一个项
		}
	}
}
  • 计算核心接口与异常处理模块:用户点击求解交点按钮后,调用槽函数on_actSolve_triggered
void IntersectionGUI::on_actSolve_triggered()
{
  // 从图形列表中构造给核心接口的输入字符串
	std::string input;
	input += std::to_string(ui.listWidget->count()) + "\n";
	for (int i = 0; i < ui.listWidget->count(); i++)
	{
		QListWidgetItem *aItem = ui.listWidget->item(i);//获取一个项
		QString str = aItem->text();
		input += str.toStdString() + "\n";
	}
  // 调用核心接口
	std::vector<std::pair<double, double> > points;
	int cnt = 0;
	try {
		cnt = guiProcess(&points, input); // 调用核心接口dll函数
	}
	catch (std::exception e) { // 反馈计算核心抛出的异常
		QString dlgTitle = QString::fromLocal8Bit("计算出现错误");
		QMessageBox::critical(this, dlgTitle, e.what());
		return;
	}
	// 绘制交点  
	for (auto vit = points.begin(); vit != points.end(); ++vit) {
		int w, h;
		xy2wh((int)(vit->first), (int)(vit->second), w, h);
		draw_point(w, h, Qt::red);
	}
  // 反馈交点总数
	QString dlgTitle = QString::fromLocal8Bit("计算结果");
	QString strInfo = QString::fromLocal8Bit("交点总数为:");
	strInfo += strInfo.asprintf("%d", cnt);
	QMessageBox::information(this, dlgTitle, strInfo,QMessageBox::Ok, QMessageBox::NoButton);
}
  • 绘图面板模块:采用QLabel和QPixmap组件进行绘图,主要的绘图函数有如下几种

    • 绘制直线、射线
    • 绘制线段
    • 绘制圆
    • 绘制点
    • 坐标轴初始化

    最难写的函数就是绘制直线、射线了,Qt自带的绘制直线函数drawLine其实是给定两点绘制线段,因此想要达到绘制直线和线段的效果就必须求出与画板边界的交点。先根据方向算左右方向的边界,若是发现碰的不是左右边界,再算上下边界。

    void IntersectionGUI::draw_ray(int x1, int y1, int x2, int y2, QColor const c, int const w) {
    	QPainter Painter(&curPixmap);
    	Painter.setRenderHint(QPainter::Antialiasing, true); //反走样
    	Painter.setPen(QPen(c, w));
    	if (x2 == x1) { // 竖直
    		if (y2 > y1) {
    			y2 = REAL_SIZE;
    		}
    		else {
    			y2 = -REAL_SIZE;
    		}
    	}
    	else if (y1 == y2) { // 水平
    		if (x2 > x1) {
    			x2 = REAL_SIZE;
    		}
    		else {
    			x2 = -REAL_SIZE;
    		}
    	}
    	else { // 向右上倾斜 先算左右边界交点,超范围就算上下交点
    		double k = (double)(y2 - y1) / (x2 - x1);
    		double b = y1 - k * x1;
    		if (x2 > x1) { 
    			double y_ = REAL_SIZE * k + b;
    			if (y_ > REAL_SIZE) { 
    				y2 = REAL_SIZE;
    				x2 = (y2 - b) / k;
    			}
    			else if (y_ < -REAL_SIZE) {
    				y2 = -REAL_SIZE;
    				x2 = (y2 - b) / k;
    			}
    			else {
    				x2 = REAL_SIZE;
    				y2 = y_;
    			}
    		}
    		else { ... } // 相似
    	}
    	QPoint p1 = xy2whPoint(x1, y1);
    	QPoint p2 = xy2whPoint(x2, y2);
    	Painter.drawLine(p1, p2);
    	ui.canvas->setPixmap(curPixmap);
    }

10、界面模块与计算模块的对接

(一)接口函数设计

为了设计松耦合,咱们将核心部分设计了以下两个函数做为接口,命令行和GUI均可以经过这两个接口访问计算核心。

#ifdef IMPORT_DLL
#else
#define IMPORT_DLL extern "C" _declspec(dllimport) //指的是容许将其给外部调用
#endif

IMPORT_DLL int guiProcess(std::vector<std::pair<double,double>> *points, std::string msg);

IMPORT_DLL void cmdProcess(int argc, char *argv[]);
  • guiProcess函数传入符合格式的字符串,交点集用指针返回,交点个数用返回值返回。
  • cmdProcess采用需求规定的命令行输入格式进行输入输出

注:这两个函数并非分别针对gui和cmd,只是输入输出格式不一样,均可以任意调用,保证松耦合。

(二)导出dll

经过此接口将计算核心封装成动态连接库calcInterface.dll和库calcInterface.lib

(三)模块对接

  • 命令行接口对接
int main(int argc, char *argv[])
{
	typedef int (*pGui)(vector<pair<double,double>>* points, string a);
	typedef void (*pCmd)(int argc, char* argv[]);
	
	HMODULE hDLL = LoadLibrary(TEXT("calcInterface.dll"));

	vector<pair<double, double>>points;
	string tmp = "2\nL 1 1 0 1\nL 1 1 1 0\n";
	if (hDLL) {
		pGui guiProcess = (pGui)GetProcAddress(hDLL, "guiProcess");
		pCmd cmdProcess = (pCmd)GetProcAddress(hDLL, "cmdProcess");
		try {
			int ans1 = guiProcess(&points, tmp); // 测试接口函数1
			for (int i = 0; i < points.size(); i++) {
				cout << points[i].first << " " << points[i].second << endl;
			}
			cout << ans1 << endl;
		}
		catch (exception t) {
			cout << t.what() << endl;
		}
		cmdProcess(argc, argv); //测试接口函数2
	}
}

  • GUI接口对接
#pragma comment(lib,"calcInterface.lib")
_declspec(dllexport) extern "C" int guiProcess(std::vector<std::pair<double, double>> *points, std::string msg);
_declspec(dllexport) extern "C" void cmdProcess(int argc, char *argv[]);

// 调用核心接口
std::vector<std::pair<double, double> > points;
int cnt = 0;
try {
	cnt = guiProcess(&points, input); // 调用核心接口dll函数
}
catch (std::exception e) { // 反馈计算核心抛出的异常
	QString dlgTitle = QString::fromLocal8Bit("计算出现错误");
	QMessageBox::critical(this, dlgTitle, e.what());
	return;
}

11、描述结对的过程

因为单个软件存在或多或少的问题,咱们综合使用VS Live Share腾讯会议以及github来进行远程结对编程。

VS Live Share能够更加真实的模拟两我的共同面对同一文件编程的效果,“领航员”也能够更方便的参与到代码的编写中,但VS Live Share没法让被邀请的人观看到程序的运行结果以及整个解决方案的结构。所以,咱们辅以腾讯会议共享屏幕,来观看整个项目的架构和编译、运行等信息。而后,咱们根据两我的擅长的不一样部分,经过github进行代码同步,分别在不一样的模块担任“驾驶员”和“领航员”的角色。

结对过程当中,因为两人已经在许多课程做业中创建了深厚的合做基础和友谊,互相信任,所以,在遇到一些犹豫不定的状况时,为了提升效率,在共同讨论各类方法的优劣以后,多采用断言和说服的方式肯定思路。

如下是咱们结对过程当中同时使用LiveShare和腾讯会议时的截图:

12、说明结对编程的优势和缺点。同时描述结对的每个人的优势和缺点在哪里

结对编程的优势:

  • 更快的攻破技术难点。因为VS用的很少、QT也是第一次使用,在咱们结对编程时常常会碰到一些操做、语言上的问题和障碍。这个时候,咱们能够分头查找相关资料,解决问题的速度大大提高。
  • 保持愉快的编程氛围,平衡心态。本身一我的编程的时候,常常会由于一些莫名其妙的bug卡几个小时,心情烦躁到想要拍桌子,并且最后无处诉说。但在结对编程的时候,两我的一块儿开发代码,一块儿享受debug的”乐趣“,看到有人和本身一块儿由于共同的代码受苦,心态也好了许多,debug也更加积极,编程氛围也很轻松愉快。
  • 加快对新知识的熟悉和学习,下降开发代价。个人搭档是一名大佬,曾啃过《C++ Primer》,所以对于C++比我熟悉的多,在我编程过程当中,能够给我提供许多指导,避免了我花费在查找资料上的时间,也预防了不少细节上的错误。
  • 结对期间产出的代码质量更高一些。结对编程时,有搭档盯着屏幕看,一些细小的粗心致使的错误,每每可以被当即发现,省得以后被粗心致使的bug卡住。

结对编程的缺点:

  • 时间协调问题。因为咱们还都是学生,没办法把精力全用在开发项目之中。特别到了大三,每一个人选的课几乎都不相同,也都有科研、助教、冯如杯等等一些不一样的私事,并且如今仍是在家远程结对编程,家事也不可能无论不顾,所以时间的协调比较麻烦,有一些比较简单的、已经肯定好设计的部分也就各自抽空完成了。
  • 远程结对编程还存在交流不便的问题。有一些细节小问题,好比某一行、某一个字母、某一个数字,若是在线下的话能够直接用手指出,或”夺下“键盘鼠标直接改掉。可是线上交流就不得不用用语言描述和定位,一些很简单的操做,因为着急或者其余缘由一时间表达不清楚,就会浪费时间。若是是线下结对编程的话,应该不会存在该问题。

结对编程的每个人的优缺点:

  • 个人搭档的优势
    • 学习能力强。充满学习热情,能很快的学习新的语言和开发工具,如本次项目中的QT开发工具。
    • 表达能力强。可以快速发现问题并描述出来,也负责与其余队伍交流接口和对拍等事务。
    • OOP思想、C++语言细节等基础知识很熟悉。给了我不少帮助
  • 个人优势
    • debug和测试更有耐心一点。
    • 对算法与代码实现的细节关注的更多一些。
    • 对做业要求更上心一些。(分奴嘴脸)
  • 个人搭档的缺点
    • 不喜欢一些繁琐枯燥的工做
  • 个人缺点
    • OOP和c++基础不牢固,代码一股面向过程的风格
    • 有的时候过于死板和犹豫,须要队友”断言“以提升效率

总结:学识渊博、代码规范、能说会道、规划合理,个人搭档的是我见过最强的搭档。

十3、警告信息消除

使用默认的规则。能够看到已经没有任何错误和警告。

【附加题】 跨组对接调用其余组的计算核心

合做小组中的两位成员:17373124 闫苗、17373299 刘紫涵

咱们与其合做,互换了计算核心dll,测试结果以下,能够正常运行,git仓库连接

这是咱们简单测试他们的dll的结果:

这是他们测试咱们dll的结果:

因为在最初设计时就已经进行过大体的商量和规划,他们采用的gui接口函数格式与咱们的不一样之处不多,只需修改接口部分少许代码便可正常对接。

IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::vector<string> msg);
  • 这是咱们测试他们dll的GUI接口对接结果

  • 这是他们测试咱们dll的GUI接口对接结果

相关文章
相关标签/搜索