【BUAA软工】结对编程作业

项目 内容
课程:2020春季软件工程课程博客作业(罗杰,任健) 博客园班级链接
作业:BUAA软件工程结对编程项目作业 作业要求
课程目标 学习大规模软件开发的技巧与方法,锻炼开发能力
作业目标 完成结对编程项目
教学班 周五上午006班
项目GitHub地址 GitHub链接

PSP

Q:在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。

  • 在开始设计之前,进行了PSP规划如下
PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
Estimate 估计这个任务需要多少时间 60 80
Development 开发
Analysis 需求分析 (包括学习新技术) 300 560
Design Spec 生成设计文档 60 85
Design Review 设计复审 (和同事审核设计文档) 30 46
Coding Standard 代码规范 (为目前的开发制定合适的规范) 5 10
Design 具体设计 60 48
Coding 具体编码 360 573
Code Review 代码复审 60 127
Test 测试(自我测试,修改代码,提交修改) 60 94
Reporting 报告
Test Report 测试报告 30 38
Size Measurement 计算工作量 10 15
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 30 63
合计 1065 1739
  • 其中实际花费时间比预估时间要多得多的几个环节分别是:需求分析,具体编码,代码复审阶段。因为一开始项目设计思路是沿用上一次的有理数设计思路设计的,但是在进行测试是发现有理数在实现上存在一些困难(后续章节分析),因此重新进行了一次设计,改由使用双精度浮点数实现。因此这几个环节会比预估要多一倍
  • 在测试环节,因为要做到高覆盖率测试,因此对于各个小函数基本都做了单元测试。

core模块设计

Q:看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

信息隐藏是结构化设计与面向对象设计的基础。在结构化中函数的概念和面向对象的封装思想都来源于信息隐藏。信息隐藏能够防止对象内部一些局部的关键值,隐私值暴露给用户,一方面能够防止其他程序员的恶意篡改,也能避免出现不小心改到一些隐私变量值的情况,从而使类对象能够更安全顺利的完成程序员设计的函数。在隐蔽信息的同时,也需要暴露一些接口函数,让其他程序员能够使用到你设计的程序。

在本次作业的接口设计中也十分注意这一点。例如在设计core模块时,考虑到core模块仅需要对外提供相关的接口操作函数,而对于这些函数的具体实现,包括使用的数据结构,都是完全隐蔽的。例如对容器add相关几何对象,需要提供外部的Interface比如addLine addCircle这些函数,但是外部并不知道这些Line以什么形式保存在哪个数据结构中,这就是Information Hiding

class GeometryFactory{
private:
	PointMap points;					    // 交点集合<无理数点,点存在于多少几何对象上>
	LineMap lines;						    // <k&b, <ID, Lines>>
	CircleSet circles;					    // <Circles>
	IdLineMap line_ids;					    // <ID, Line>
	IdCircleMap circle_ids;				            // <ID, Circle>
	int line_counter = 1;					    // Line ID 累加器
	int circle_counter = 0;					    // Circle ID 累加器
	void line_line_intersect(Line &l1, Line &l2);		    // 线线交点
	void line_circle_intersect(Line &l1, Circle &c1);	    // 线圆交点
	void circle_circle_intersect(Circle &c1, Circle &c2);	    // 圆圆交点
	inline void increase_point(Point* p);	//  ..
	inline void decrease_point(Point* p);	//  ..
	void removeLine(Line &l);				    // 移除Line对象
	void removeCircle(Circle &c);				    // 移除Circle对象
	inline bool point_on_line(Point *p, Line &l);
	inline bool point_on_circle(Point *p, Circle &c);
	inline bool point_in_line_range(double x, double y, Line &l);
public:
	GeometryFactory();
	/* Modification */
	int addLine(int type, long long x1, long long x2, long long y1, long long y2);		
	int addCircle(long long x, long long y, long long r);							
        int addObjectFromFile(char* message);				
	void remove(int id);										

	/* Query */
	Line getLine(int id);						
	Circle getCircle(int id);							
	void getPoints(double *px, double *py, int count);								 
	int getPointsCount();								
	
};

例如上述代码为本次core模块的容器类定义,其中对于内部函数,内部数据结构完全是private形式,并不对外开放,而public的函数就是接口对外提供的操作函数,这些操作函数足以体现出容器类的功能。并且调用者调用这些函数时只会对容器类内部变量做安全的操作,而不会特意破坏到内部数据结构。

关于Interface Design,接口的设计是为了让用户能够在不了解内部core的实现逻辑的前提下,仅通过阅读接口函数的声明,就能大概了解core的主要功能并且能够正确的操控core去实现自定义的功能。在我们最初的版本设计中,因为一开始为了能够以面向对象的思想去提供接口形式,因此接口都设计为各种类的形式。这样能够让用户在读到类名和类函数,就大概清楚core的功能。

Loose Coupling模块松耦合,讲究的是模块之间的功能是否能够明确的相互分离,并且又不会造成部分代码的冗余。例如本次作业中涉及到三种模块:计算模块命令行解析模块GUI模块。显然计算模块就应该专注于对几何对象的管理,交点的求解,是其他两个模块的基础。因此在计算模块中要提供对几何对象修改,求解的方法。而后面两个模块都是用户交互模块,他们需要拥有一个共同的功能:处理并解析用户提供的输入。如果这两个模块分别实现解析功能,这样势必会造成代码的冗余,而显然这部分代码是可以复用的。因此为了使复用方便,我们将字符串信息解析模块内嵌在计算模块中,当做向计算模块增加对象的一种方式。

Q:计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

本次作业的core模块框架大部分沿用上一次的设计。首先是对于类的设计,这次作业中需要实现对三种直线类集合对象(直线,线段,射线),以及附加题中的圆提供支持。因此考虑实现两种对数据进行封装的几何对象类:Line类和Circle类。此外还有因为GUI需要描绘所有交点,因此需要增加一个对交点信息进行封装的Point类。

之后就是core模块最主要的类:GeometryFactory容器类。容器类不仅要负责添加,存储几何对象,同时还要支持对交点的求解。因此对于容器类的设计参考了工厂模式,但并不是严格意义上的工厂模式。GeometryFactory类拥有创造直线,圆的方法,即addLineaddCircle方法,用户只需要提供相关参数就可以向容器中添加几何对象。同时也能够求解交点,并生产的getPoints方法。具体类图见下一个问题解答。

其中容器类几个主要函数介绍如下:

GeometryFactory类:

关于主要的数据结构如下:

  • unordered_map<Point*, int> points:管理求解得到的所有交点,其中key是交点的指针,value是该交点的权重。对于权重为0时,将point移出集合(即交点不存在)。关于权重的计算方法见后续解释。
  • map<int, Line> IdLineMap:管理添加的直线,索引为id,值为直线
  • map<int Circle> IdCircleMap:管理添加的圆,索引为id,值为圆

关于该类的对外接口如下:

  • int addLine(int type, long long x1, long long x2, long long y1, long long y2):向容器中添加直线/射线/线段,并计算该直线/射线/线段和其他几何对象的交点,返回对象的id,执行具体步骤:
    • 查看添加该直线是否会造成异常(重合,范围等等)
    • 遍历所有几何对象,求解和该直线的交点,并记录
    • 返回id
  • int addCircle(int a, int b, int r):向容器中添加圆,并计算和其他几何对象的交点,返回对象的id
    • 查看添加该圆是否会造成异常(重合,范围等)
    • 遍历所有几何对象,求解和该圆的交点并记录
    • 返回id
  • int addObjectFromFile(char* message):通过字符串的形式向容器添加集合对象,该函数解析字符串并添加对象,能够抛出格式错误异常
    • 判断该字符串是否符合格式
    • 解析字符串,并添加相应的圆和直线
    • 返回id
  • Line getLine(int id):通过id从容器中获取几何对象
  • Circle getCircle(int id):通过id从容器中获取几何对象
  • getPoints(double *px, double *py, int count):获取所有的交点集合,其中px为x坐标数组,py为y坐标数组,count为点个数。
  • int getPointsCount():获取交点的个数
  • void remove(int id):删去几何对象,根据id判断为圆还是直线,再调用相应的removeLine或者removeCircle函数

关于该类的主要私有函数如下:

  • private void line_line_intersect(Line &l1, Line &l2):求解线线交点
    • 根据算法求解交点*p,算法后续解释
    • 判断求解得到的交点是否在线上(是否在线段,射线的范围内)
    • 若在,则对points[p] += 1,若p不存在在points中,则初始化points[p] = 1
  • private void line_circle_intersect(Line &l1, Circle &c):求解线圆交点,管理逻辑同上
  • private void circle_circle_intersect(Circle &c1, Circle &c2):求解圆圆交点,管理逻辑同上。
  • private void removeLine(Line l):删除直线,执行步骤如下:
    • 遍历所有几何对象(除该直线外),求解交点*p
    • points[p] -= 1,若points[p] == 0,则将该交点从集合中删除。
  • private void removeCircle(Circle c):删除圆,执行步骤同上。

Line 类:

该类为直线封装类,没有特别的成员方法,仅是对数据做封装,具体内容如下:

  • long long a, b, c:直线的参数,根据传入的点坐标计算得到,直线表示为ax + by + c = 0
  • long long x1, y1, x2, y2:传入的点坐标参数
  • long long x_min, x_max, y_min, y_max:直线的范围,该参数主要用于判断直线范围间是否存在重合,其中用最大值100000代表无穷,即直线的x_min = y_min = -100000, x_max = y_max = 100000
  • int type:线的种类,表明是直线还是射线还是线段。
  • Line(long long x1, long long y1, long long x2, long long y2, int type):构造函数,具体步骤如下:
    • 计算直线的a, b, c
    • 计算直线的斜率k和截距b,若k不存在则置k为可能的最大值100000
    • 计算线的范围:x_min, x_max, y_min, y_max,其中使用+-100000表示无穷

Circle 类:

该类为圆封装类,封装信息如下:

  • long long a, b, r:圆的三个参数,代表圆心坐标,半径。

对于几个关键的算法如下解释:

线线交点求解算法:

线线交点的求解,考虑将直线化为ax + by + c = 0的形式求解交点,其中a,b,c的求解公式如下:

  • \(a = y_1 - y_2\)
  • \(b = x_2 - x_1\)
  • \(c = x_1 y_2 - x_2 y_1\)

根据求得的上述参数进行联立,可以得到两条直线的交点公式如下:

\[x = \frac{b_1 \times c_2 - b_2 \times c_1}{a_1 \times b_2 - a_2 \times b_1} \]

\[y = \frac{a_2 \times c_1 - a_1 \times c_2}{a_1 \times b_2 - a_2 \times b_1} \]

线圆交点求解算法:

线圆交点求解算法使用代数法,将直线公式的\(y\)换成\(x\)表示,代入圆公式中,求解一元二次方程来求解x坐标。其中需要考虑直线\(k\)存在和\(k\)不存在两种情况。

  • \(ax + by + c = 0\)表示为\(y = \frac{-c - ax}{b}\)代入圆方程\((x - x_0)^2 + (y - y_0)^2 = r^2\),得到:

\[Ax^2 + Bx + C = 0 \]

\[A = \frac{a^2 + b^2}{b^2}, B = \frac{2y_0ab - 2x_0b^2 + 2ac}{b^2}, C = x_0^2 + y_0^2 + \frac{c^2}{b^2} - r^2 + \frac{2y_0c}{b} \]

  • 其中圆方程的\(\delta = B^2 - 4AC\),因此求得的交点为:

\[x_1 = \frac{-B + \sqrt{B^2 - 4AC}}{2A}, y_1 = \frac{- c - ax_1}{b} \]

\[x_2 = \frac{-B - \sqrt{B^2 - 4AC}}{2A}, y_2 = \frac{- c - ax_2}{b} \]

求解交点过程中,需要判断相切问题。由于代入法在计算到\(\delta\)步骤时,已经接近最后一步,误差较大,因此判断线圆相切时不应该使用\(\delta\)进行判断。因此考虑使用几何法来判断:

  • 假设直线方程为\(ax + by + c = 0\),则圆心到直线的距离为\(distance = \frac{| ax_0 + by_0 + c |}{\sqrt{a^2 + b^2}}\)
  • \(distance == r\),则代表相切,仅计算切点,若\(distance < r\),则代表相交,需要计算两个交点。

圆圆交点求解算法:

圆圆交点求解可以化为线圆交点来进行求解,其中根据定理:两圆方程作差可以得到圆相交弦方程。根据所求的直线方程将问题化为求线圆交点。但需要事先判断两圆是否相交或者相切。同理,相切的判定使用几何法判定。

  • 由于圆有内切和外切两种相切情况,因此方法如下:
  • 计算两个圆心间距平方\(distance^2 = (x_1 - x_2)^2 + (y_1 - y_2)^2\)
  • \(distance^2 == (r_1 - r_2)^2\)\(distance^2 == (r_1 + r_2)^2\),则说明两圆相切,仅计算切点即可
  • \(distance^2 > (r_1 - r_2)^2\)\(distance^2 < (r_1 + r_2)^2\),则说明两圆相交,计算相交弦和其中一个圆的两个交点即可。

关于点在线上的判定:

由于本次作业新增了线段和射线类,则说明根据上述方法求得的交点并不一定真正位于线上,有可能该点只是和线段或者射线位于同一条直线上,但是并不是位于线段或者射线范围内。因此需要在求完交点后进行范围判断

GeometryFactory类里我们实现了一个函数bool point_in_line_range(Line l, Point p),该函数来判断点是否处在线范围上。由于该函数仅被求解交点函数调用,因此传入的点参数必然是一个在同一条直线上的点,因此不用再带入方程验证,仅需要判断范围即可。因此函数具体步骤如下:

  • 若为直线,则返回true
  • 若为线段:
    • l.x1 != l.x2,则返回min(l.x1, l.x2) <= p.x <= max(l.x1, l.x2)
    • l.x1 == l.x2,则返回min(l.y1, l.y2) <= p.y <= max(l.y1, l.y2)
  • 若为射线:
    • l.x1 < l.x2,则返回p.x >= l.x1
    • l.x1 > l.x2,则返回p.x <= l.x1
    • l.x1 == l.x2,则使用l.y来进行判断,逻辑一致。

关于直线重合的判定:

本次作业需要支持对线性对象是否存在重合进行判断。由于Line在构造的时候已经将线的范围进行了设定,因此关于线的重合判定采用以下算法:

  • 对于k,b值相同的线,则显然位于同一直线上:
    • x_min != x_max:如果max(l1.x_min, l2.x_min) < min(l1.x_max, l2.x_max),则存在重合
    • x_min == x_max:如果max(l1.y_min, l2.y_min) < min(l1.y_max, l2.y_max),则存在重合
  • 对于k,b值不同的线,则显然不可能重合

因此对于每一条新加的直线,需要和已有的和其k,b值相同的所有线进行重合判定。因此需要一定的数据结构对相同k,b值的线进行归类。

因此设计了class LineKey,其中拥有两个成员变量k和b,对该对象重写hash函数和equal函数后,使用unordered_map<LineKey, Line> LineKeyMap来对具有相同的kb值的直线进行管理。每一次添加直线时,从LineKeyMap中查找相同kb值的线,来进行重合判定。

Q:阅读有关 UML 的内容:https://en.wikipedia.org/wiki/Unified_Modeling_Language。画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。

Core模块性能改进

Q:计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

对模块喂入一条具有600w级别的交点数据,进行30秒性能检测,结果如下:

观察发现,对于前几的函数是主函数里调用的大范围函数,因此占用率必然高。除去那几个函数后发现,increase_point函数的占用率较高。查看详细报告后,发现其中的代码占用率如下:

发现到对于新增节点的函数竟然占用高达一半以上的占用率。分析得到大概有两点原因:

  • 数据结构使用的是unordered_map,对于map初始化并没有给其预设多大的容量,因此中途当map容量到达极限时,需要对扩充新的容量,对map元素进行复制。而扩充新的容量时务必造成较大的时间花费。
  • points[point]函数的执行效率可能不如points.insert()函数的效率

为了查找具体原因,对这两点问题进行了相应的修改:

  • 在对象初始化时进行map容量预设:
GeometryFactory::GeometryFactory() {
	points.reserve(5000000);
	points.rehash(5000000);
	lines.reserve(500000);
	lines.rehash(500000);
	line_ids.reserve(500000);
	line_ids.rehash(500000);
	circles.reserve(500000);
	circles.rehash(500000);
}
  • points[point]函数修改为points.insert()函数。

对上面两处修改分别进行实验,发现第二点并不是问题的原因,在进行第一点修改后,性能有了进一步的提升:

可以发现increase_point函数的占用率大幅度下降,观察具体代码的占用率发现:

发现insert操作占用率下降了\(20%\),这说明其中的扩大容量,拷贝操作减少了,性能提升了。

Core模块单元测试

Q:计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。

对于单元测试的设计思路,主要考虑以下几点:

  • 对于各个小函数的功能进行小范围的单元测试,测试其基本功能是否达到了预想的效果,如point_in_line_range函数line_line_intersect函数等
  • 在小函数功能测试完成后,对大函数进行单元测试,测试小函数的相互调用以及大函数的逻辑是否有问题,如测试addLineaddCircle函数
  • 使用几个大函数进行交叉测试,测试模块的基础功能,看大函数的交叉调用是否会造成逻辑上的失误。

对于小函数的单元测试,以point_in_line_rangepoint_on_line等为例:

		TEST_METHOD(GeometryFactory_point_in_line_range)
		{
			GeometryFactory *test = new GeometryFactory();
			Point *p = new Point(3.0, 4.0);
			Line l1(3, 4, 5, 8, LIMITED_LINE);
			Line l2(3, 4, -10, -33, SINGLE_INFINITE_LINE);
			Line l3(-3, -4, -6, -8, SINGLE_INFINITE_LINE);
			Line l4(0, 0, 1, 1, DOUBLE_INFINITE_LINE);
			CHECK(test->point_in_line_range(3.0, 4.0, l1), true);
			CHECK(test->point_in_line_range(3.0, 4.0, l2), true);
			CHECK(test->point_in_line_range(3.0, 4.0, l3), false);
			CHECK(test->point_in_line_range(3.0, 4.0, l4), true);
			delete(test);
			delete(p);
		}

		TEST_METHOD(GeometryFactory_point_on_line) 
		{
			GeometryFactory *test = new GeometryFactory();
			Point *p1 = new Point(2.999999999999999001, 3.0);
			Point *p2 = new Point(3.000000000123, 3.0);
			Point *p3 = new Point(4.0, -4.0);
			Line l1(3, 3, 4, 4, DOUBLE_INFINITE_LINE);
			CHECK(test->point_on_line(p1, l1), true);
			CHECK(test->point_on_line(p2, l1), false);
			CHECK(test->point_on_line(p3, l1), false);
			delete p1;
			delete p2;
			delete p3;
			delete test;
		}

		TEST_METHOD(GeometryFactory_point_on_circle)
		{
			GeometryFactory *test = new GeometryFactory();
			Point *p1 = new Point(1.00000000000000003, 0.0);
			Point *p2 = new Point(1.0000000000223, 1.0);
			Point *p3 = new Point(1.0000000000223, 0.0);
			Circle c(0, 0, 1);
			CHECK(test->point_on_circle(p1, c), true);
			CHECK(test->point_on_circle(p2, c), false);
			delete test;
			delete p1;
			delete p2;
			delete p3;
		}

测试思路非常简单,构建点在边界,点在线上,点在线外的情况,调用point_in_line_range,测试功能。后续的函数同理。

对于大函数的测试,主要测试返回值是否预想,对数据结构的改变是否是预想结果。例如调用addLine函数后是否正常返回id,正常插入直线,是否求对了交点,如果有异常是否有抛出。

		TEST_METHOD(add_line_test_1)
		{
			GeometryFactory test;
			CHECK(1,test.addLine(DOUBLE_INFINITE_LINE, 0, 1, 0, 1));
			CHECK(3, test.addLine(DOUBLE_INFINITE_LINE, 0, 2, 0, 4));
			bool catch_flag = false;
			try {
				test.addLine(SINGLE_INFINITE_LINE, 0, -1, 0, -1);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
		}

		TEST_METHOD(add_line_test_2)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(LIMITED_LINE, 0, 2, 0, 2));
			CHECK(3, test.addLine(SINGLE_INFINITE_LINE, 0, -1, 0, -1));
			
			bool catch_flag = false;
			try {
				test.addLine(SINGLE_INFINITE_LINE, 1, 4, 1, 4);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
			catch_flag = false;
			try {
				test.addLine(LIMITED_LINE, 1, -1, 1, -1);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
			catch_flag = false;
		}

		TEST_METHOD(add_line_test_3)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(DOUBLE_INFINITE_LINE, 0, 1, 0, 1));
			bool flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, 0, 0, 1, 1);
			}
			catch (CoordinateCoincidenceException &e) {
				CHECK("Error: coordinate coincident!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, 100000, 0, 0, 0);
			}
			catch (CoordinateRangeException &e) {
				CHECK("Error: coordinate is out of range!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, -100000, 0, 0, 0);
			}
			catch (CoordinateRangeException &e) {
				CHECK("Error: coordinate is out of range!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(5, 1000, 0, 0, 0);
			}
			catch (UndefinedLineException &e) {
				CHECK("Error: undefined line type!", e.what());
				flag = true;
			}
			CHECK(true, flag);
		}

对于大函数的相互调用测试,则先构建容器类对象,然后交叉调用addLineaddCircle,观察是否会抛出意料之外的异常,求得的结果是否正确:

		TEST_METHOD(add_line_add_circle) 
		{
			GeometryFactory test;
			CHECK(1, test.addLine(DOUBLE_INFINITE_LINE, 0, 324, 0, 332));
			CHECK(3, test.addLine(DOUBLE_INFINITE_LINE, 0, -3, 0, 322));
			CHECK(0, test.addCircle(0, 0, 8));
			CHECK(2, test.addCircle(0, 0, 7));
			CHECK(5, test.addLine(DOUBLE_INFINITE_LINE, -32, -33, 32, 22));
		}

		TEST_METHOD(get_line_remove_line)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(DOUBLE_INFINITE_LINE, 0, 3, 0, 3));
			CHECK(3, test.addLine(DOUBLE_INFINITE_LINE, 0, 43, 0, 23));
			Line l = test.getLine(1);
			CHECK((int)l.x1, 0);
			CHECK((int)l.x2, 3);
			CHECK((int)l.y1, 0);
			CHECK((int)l.y2, 3);
			CHECK((size_t)2, test.line_ids.size());
			test.remove(1);
			CHECK((size_t)1, test.line_ids.size());
			bool flag = false;
			try {
				test.remove(33);
			}
			catch (ObjectNotFoundException &e) {
				CHECK("Error: line not found or invalid id!", e.what());
				flag = true;
			}
			CHECK(true, flag);
		}

最终Core模块的各个文件单元测试覆盖率都打到了90%以上,具体如下(其中隐藏了非core模块的main函数文件)

Core模块异常处理设计

计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。

异常类型的设计主要分为两种:有关几何对象的异常,无关几何对象的异常。其中有关几何对象的异常又可以分为:数据异常,直线类异常,圆类异常等。因此最终异常根据分类可以设计出以下情况:

  • 有关几何对象异常
    • 数据异常
      • CoordinateRangeException:坐标超出范围异常,即输入值大于100000,在添加集合对象时检测并触发
    • 直线类异常
      • LineCoincidenceException:存在重合异常,即输入的直线和之前添加的直线有重合,在添加集合对象时检测并触发
      • CoordinateCoincidenceException:直线坐标重合异常,即构成直线的两点相同,在添加集合对象时检测并触发
    • 圆类异常
      • CircleCoincidenceException:圆重合异常,在添加几何对象时检测并触发
      • NegativeRadiusException:圆半径为负数异常,在添加几何对象时检测并触发。
  • 无关几何对象异常
    • ObjectNotFoundException:几何对象不存在异常,即通过id找不到几何对象,在get函数和remove函数中触发
    • WrongFormatException:格式错误异常,即输入的字符串不符合要求的格式,在addObjectFromFile中触发。其中每一行要求的格式为:
      • 每行仅有一个几何对象描述
      • 第一个字符描述集合对象类型,后续n个字符描述几何对象参数
      • 对于每一种几何对象,参数不可多也不可少
      • 不可有除描述用之外的其他字符,不可有多余的空格,即参数之间有且仅有一个空格,行末行首没有多余空格。

对于异常的单元测试,由于大部分异常基本都可以在add函数中检测并抛出,因此在add函数中对异常进行单元测试:

		TEST_METHOD(add_line_test_1)
		{
			GeometryFactory test;
			CHECK(1,test.addLine(DOUBLE_INFINITE_LINE, 0, 1, 0, 1));
			CHECK(3, test.addLine(DOUBLE_INFINITE_LINE, 0, 2, 0, 4));
			bool catch_flag = false;
			try {
				test.addLine(SINGLE_INFINITE_LINE, 0, -1, 0, -1);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
		}

		TEST_METHOD(add_line_test_2)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(LIMITED_LINE, 0, 2, 0, 2));
			CHECK(3, test.addLine(SINGLE_INFINITE_LINE, 0, -1, 0, -1));
			
			bool catch_flag = false;
			try {
				test.addLine(SINGLE_INFINITE_LINE, 1, 4, 1, 4);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
			catch_flag = false;
			try {
				test.addLine(LIMITED_LINE, 1, -1, 1, -1);
			}
			catch (LineCoincidenceException &e) {
				catch_flag = true;
				CHECK("Error: this line has been coincident!", e.what());
			}
			CHECK(true, catch_flag);
			catch_flag = false;
		}

		TEST_METHOD(add_line_test_3)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(DOUBLE_INFINITE_LINE, 0, 1, 0, 1));
			bool flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, 0, 0, 1, 1);
			}
			catch (CoordinateCoincidenceException &e) {
				CHECK("Error: coordinate coincident!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, 100000, 0, 0, 0);
			}
			catch (CoordinateRangeException &e) {
				CHECK("Error: coordinate is out of range!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(DOUBLE_INFINITE_LINE, -100000, 0, 0, 0);
			}
			catch (CoordinateRangeException &e) {
				CHECK("Error: coordinate is out of range!", e.what());
				flag = true;
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addLine(5, 1000, 0, 0, 0);
			}
			catch (UndefinedLineException &e) {
				CHECK("Error: undefined line type!", e.what());
				flag = true;
			}
			CHECK(true, flag);
		}

		TEST_METHOD(add_circle_test1) 
		{
			GeometryFactory test;
			CHECK(0, test.addCircle(0, 0, 3));
			CHECK(2, test.addCircle(0, 0, 8));
			bool flag = false;

			try {
				test.addCircle(0, 0, -4);
			}
			catch (NegativeRadiusException &e) {
				flag = true;
				CHECK("Error: radius of circle is illegal!", e.what());
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addCircle(0, 0, 100000);
			}
			catch (CoordinateRangeException &e) {
				flag = true;
				CHECK("Error: coordinate is out of range!", e.what());
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addCircle(0, -100000, 10000);
			}
			catch (CoordinateRangeException &e) {
				flag = true;
				CHECK("Error: coordinate is out of range!", e.what());
			}
			CHECK(true, flag);

			flag = false;
			try {
				test.addCircle(0, 0, 3);
			}
			catch (CircleCoincidenceException &e) {
				flag = true;
				CHECK("Error: this circle has been added!", e.what());
			}
			CHECK(true, flag);
		}

对于其他的例如get和remove触发的异常,在get和remove函数中进行单元测试:

		TEST_METHOD(get_line_remove_line)
		{
			GeometryFactory test;
			CHECK(1, test.addLine(DOUBLE_INFINITE_LINE, 0, 3, 0, 3));
			CHECK(3, test.addLine(DOUBLE_INFINITE_LINE, 0, 43, 0, 23));
			Line l = test.getLine(1);
			CHECK((int)l.x1, 0);
			CHECK((int)l.x2, 3);
			CHECK((int)l.y1, 0);
			CHECK((int)l.y2, 3);
			CHECK((size_t)2, test.line_ids.size());
			test.remove(1);
			CHECK((size_t)1, test.line_ids.size());
			bool flag = false;
			try {
				test.remove(33);
			}
			catch (ObjectNotFoundException &e) {
				CHECK("Error: line not found or invalid id!", e.what());
				flag = true;
			}
			CHECK(true, flag);
		}

猜你喜欢

转载自www.cnblogs.com/lpxofbuaa/p/12560605.html