项目 | 内容 |
---|---|
课程: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
类拥有创造直线,圆的方法,即addLine
和addCircle
方法,用户只需要提供相关参数就可以向容器中添加几何对象。同时也能够求解交点,并生产的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\)
根据求得的上述参数进行联立,可以得到两条直线的交点公式如下:
线圆交点求解算法:
线圆交点求解算法使用代数法,将直线公式的\(y\)换成\(x\)表示,代入圆公式中,求解一元二次方程来求解x坐标。其中需要考虑直线\(k\)存在和\(k\)不存在两种情况。
- 将\(ax + by + c = 0\)表示为\(y = \frac{-c - ax}{b}\)代入圆方程\((x - x_0)^2 + (y - y_0)^2 = r^2\),得到:
- 其中圆方程的\(\delta = B^2 - 4AC\),因此求得的交点为:
求解交点过程中,需要判断相切问题。由于代入法在计算到\(\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
函数等 - 在小函数功能测试完成后,对大函数进行单元测试,测试小函数的相互调用以及大函数的逻辑是否有问题,如测试
addLine
和addCircle
函数 - 使用几个大函数进行交叉测试,测试模块的基础功能,看大函数的交叉调用是否会造成逻辑上的失误。
对于小函数的单元测试,以point_in_line_range
和point_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);
}
对于大函数的相互调用测试,则先构建容器类对象,然后交叉调用addLine
和addCircle
,观察是否会抛出意料之外的异常,求得的结果是否正确:
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);
}