交点求解大师——结对项目做业

交点求解大师——结对项目做业

1、做业要求简介

本文是北京航空航天大学计算机学院软件工程课程的结对项目做业,在本次做业中,两位同窗一组,以结对编程的方式共同完成一项需求。html

结对编程是软件工程中的一种开发方法,两我的肩并肩坐在一块儿,共用一块屏幕和一份键盘鼠标,共写一份代码。两我的有不一样的分工,领航者负责指明方向,执行者负责动手写代码,在两人的默契配合下,造成一种无间隙的代码复审模式,使开发出的程序质量更高。python

项目 内容
本做业属于北航软件工程课程 博客园班级博客
做业要求请点击连接查看 结对项目做业
班级:006 Sample
GitHub项目地址 IntersectProject
GUI项目地址 IntersectionGUI
同组同窗博客连接 eitbar
我在这门课程的目标是 得到成为一名软件工程师的能力
这个做业在哪一个具体方面帮助我实现目标 实践结对编程

2、PSP表格

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

3、接口思想与接口设计

(一)接口设计原则

  • 单一指责:一个接口就干一个事
  • 数据格式:输入输出定义清晰简单,不能产生歧义,最好为傻瓜式的,让调用者毫不会错用,有些经验的用户能够直接使用API而不须要阅读文档。
  • 方便易用:包括命名清晰简洁,最小惊讶原则

(二)接口实现

基于上述原则,咱们设计了以下两个函数做为计算核心模块的接口,能够自由的与命令行和GUI进行对接。知足上述三条设计原则。这两个函数都会调用计算核心模块,不一样之处是输入输出格式。c++

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

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

4、5、计算模块设计文档与UML

(一)PipeLine

PreProcess

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

CalcIntersect

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

(二)类间关系图(UML)

  • 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里。github

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

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

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

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

(四)【新增】代码说明

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

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<double>的值将不一样,因此应该截取某精度,转换成整形进行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、契约式设计

契约式设计一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的而且可验证的接口,这样,为传统的抽象数据类型又增长了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”是一种比喻,由于它和商业契约的状况有点相似。

在面向对象课程中,咱们就接触过契约式设计,将逻辑约束在设计时定义好,编码实现时只须要遵照契约式设计就能够写出正确的代码,减小了出错的可能性。利用一些现有的软件,还能够利用契约式设计作代码正确性的形式证实。

从上次做业开始,个人重要pipeline函数就采用了契约式设计模式,下面举例说明。

//算直线和圆的交点的函数
// 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插件测试获得代码覆盖率为:99%

其中部分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++标准异常:

  • 输入异常:
    • 有关图形数量的异常:如没法读入NN不符合规范、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;
}

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

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

他们采用的gui接口函数格式与咱们的彻底相同,只需将对应的dll和lib替换便可直接运行。

IMPORT_DLL int guiProcess(std::vector<std::pair<double, double>>* points,std::string msg);
IMPORT_DLL void cmdProcess(int argc, char *argv[]);
  • 命令行对接结果

  • GUI接口对接结果

11、描述结对的过程

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

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

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

如下是咱们结对过程当中部分截图:

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

(一)结对编程的优势:

  • 更快的攻破技术难点:对实现的细节技术难点能够很方便地讨论,并且两人都熟悉代码,交流无鸿沟。例如在处理double值的hash函数时,咱们通过讨论商榷,发现了c++自带的hash<double>不能直接在咱们的代码中使用,故共同商量出用长整型的hash函数方式,解决了此技术难点。
  • 加快对新知识的熟悉和学习:例如以前咱们二人都没用用vs封装过dll,结对编程中就可让领航者提早进行学习和搜集资料。待编程者完成代码编写后能够直接进行封装。
  • 代码质量更高:结对编程时,有搭档盯着屏幕看,一些细小的粗心致使的错误,每每可以被当即发现,省得以后被粗心致使的bug卡住。
  • 愉快的编程氛围:一我的编程一般比较枯燥无聊,特别是当发现bug的时候,而结对编程时,两我的一块儿工做的氛围要更轻松,若是遇到bug的时候,也不像一我的的时候那么绝望,两人能够利用知识互补,更容易定位bug,解决bug。

(二)结对编程的缺点:

  • 时间协调:结对编程须要两人在同一时间作到电脑前,即便是远程开展,也要经过共享屏幕等方式。可是。两我的的习惯工做时间可能不一样,好比我就喜欢晚上工做,队友喜欢早起工做,此时就须要协商解决,好比采用隔一天一换的策略。
  • 远程结对编程交流不便:有一些细节小问题,好比某一行、某一个字母、某一个数字,若是在线下的话能够直接用手指出,或”夺下“键盘鼠标直接改掉。可是线上交流就不得不用用语言描述和定位,一些很简单的操做,因为着急或者其余缘由一时间表达不清楚,就会浪费时间。若是是线下结对编程的话,应该不会存在该问题。
  • 效率平衡:原本是两个生产力,硬性的只容许一我的写代码,虽然代码质量会变高,可是效率毕竟多是减半的。如何维持效率平衡,须要在软件工程实践中不断探索。

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

  • 我搭档的优势

    • acm大佬,写代码能力超强,可以梳理很复杂的程序结构,写了大部分复杂的条件分支判断代码。例如处理输入异常的函数。
    • debug和测试更有耐心,对繁杂的版本控制和测试任务都能游刃有余地处理
    • 对算法与代码实现的细节关注的更多一些。好比浮点数的hash值问题,他进行了详尽的测试,最终找到了解决方案。
  • 个人优势

    • 充满学习热情,能很快的学习新的语言和开发工具,如本次项目中的QT开发工具。
    • 表达能力强。可以快速发现问题并描述出来,负责与其余队伍交流接口和对拍等事务。
    • 对C++语言更熟练
  • 个人缺点

    • 对一些繁琐枯燥的工做不太认真
    • 版本控制混乱,各类版本容易把本身绕晕
  • 搭档的缺点

    • OOP和c++基础不牢固

总结:逻辑清晰、代码规范、耐心细心、规划合理,个人搭档的是我见过最强的搭档。

十3、警告信息消除

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

十4、思考

(一)接口数据类型如何设计?

在设计接口数据类型时,咱们产生了一些异议,与其余小组讨论,意见也不是很一致。大致分为如下两种。

  • 松式设计

    所谓松式设计指采用万能数据类型,如字符串、整形等做为输入输出。

    • 优势:任何人,不须要任何库或包就能使用,易于跨平台,移植性强。
    • 缺点:数据自检错能力差,调用者能够传任何字符串到接口,易形成:
      • 用户须要学习数据格式,构造特定格式的字符串,增长学习成本
      • 易粗枝大叶形成格式错误或故意攻击接口,让核心程序获得非法的输入
      • 接口程序必须严格对输入格式进行错误处理,防止核心程序崩溃
  • 紧式设计

    采用自定义数据类型做为接口,提供特定的构造函数,提供检错判断等。

    • 优势:易学易用,防粗心,防攻击,使不构造出合法的数据没法经过编译,或抛出异常。
    • 缺点:须要提供自定义数据类型的库,不易跨平台,移植性差,不方便对接,并且将class导出dll比函数麻烦。

讨论以后,咱们最终采用了折中的办法,用c++STL中的vector<pair<double,double>>这种数据类型返回点集,即不直接用纯粹字符串,也不使用自定义数据类型。较方便地解决了本次需求。

可是,若是有些状况下没法采用STL来描述某些接口的数据类型,又该采用什么样的方式设计呢?

相关文章
相关标签/搜索