软件工程—结对项目博客

1 简介

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

2 PSP 表格

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

3 接口设计方法

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

信息隐藏

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

以下列举了一些信息隐藏原则的应用。

  1. 多层设计中的层与层之间加入接口层。
  2. 所有类与类之间都通过接口类访问。
  3. 类的所有数据成员都是private,所有访问都是通过访问函数实现的。

实践:对于上述例举的内容,我们在前两点做得比较好,将例如下面会讲到的GeometryFactory这个大类下面,会包含Shape类和Point类,而Shape类则又包含Point类,还有就是类内部的存储数据结构对外部是透明的,特别是封装为DLL后外部不知道内部使用的数据结构。

各个类之间通过在头文件中定义的接口函数访问。但是对于第三点,我们在实际操作中遇到的困难,主要由于单元测试。

问题:单元测试时,常需要对某些细节的成员变量进行检查,如果为私有变量,则无法进行访问,而通过访问函数实现,则等价于每一个都要书写。究竟对于这种想检查私有变量的情况,单元测试如何处理呢?


接口设计

良好的接口设计,可以让用户在完全不知道内部实现的情况下,仅依靠组合使用接口即可调动核心模块实现相应的功能。下面的代码展示了核心模块对外的接口,分为修改和查询两类,依靠名字,用户可以一目了然地了解其功能。并且接口的参数和返回值不存在自定义的类,使得内部的组织的密闭性很好。

class GeometryFactory{
private:
	...
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();								
	
};

松耦合设计

经过资料查阅,这篇博客提到藕合度是度量一个代码单元在使用时与其他单元的关系。最理想,最松散的耦合,是一个单元无需其他代码单元特别的配合而可以使用。松藕合一般与高内聚相关。具备高内聚性的代码单元,一般都比较松藕合。而这篇博客,则提到松耦合要付出使系统更加复杂的代价,松耦合意味着更多的开发。

在本次的结对项目中,我们的松耦合主要体现在以下几个方面:

  • GUI界面模块、测试模块与核心计算模块的解耦:这一部分的解耦,是依靠创建DLL动态链接库彻底实现的。
    • 同一个核心计算模块可直接与界面或测试模块的执行文件运行。
    • 当核心模块出现问题时,仅需要修复并生成DLL即可,在保证接口不变化的情况下无需进行其他修改。
    • 在挑战性任务中,我们还与合作小组制定了标准接口,实现能够替换的完全松耦合。
  • 当然,在计算核心内部,我们也有松耦合的思想:
    • 使用Rational有理数类和UnRational无理数类实现数字的运算,两个类的功能完全独立,在后期由于发现有理数不适用,直接替换成无理数就很方面。

4 核心计算模块的设计、组织和实现

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

4.1 设计

本次作业的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:圆的三个参数,代表圆心坐标,半径。

4.2 关键算法解释

线线交点求解算法:

线线交点的求解,考虑将直线化为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值的线,来进行重合判定。


5 UML设计

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

6 核心计算模块性能改进

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%\),这说明其中的扩大容量,拷贝操作减少了,性能提升了。


7 Design By Contract & Code Contract

看 Design by Contract,Code Contract 的内容:

描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

设计和编写的契约模式,是指在设计一个函数或方法时,对于执行开始前的期望条件、执行结束后的结果状态以及断言:

  • Pre-Condition:在函数开始执行前,类成员变量和传入参数所需要满足的条件。
  • Post-Condition:在函数执行完成后,类成员变量和返回结果所需要满足的条件。
  • Invariant:在执行过程中,类成员变量中不应该被改变的内容(上锁)。

Code Contract是微软开发的一个.NET开发包,其功能其实类似我们在大二OO所接触的JML形式化检查工具,这类工具利用语法树、断言等手段提供静态检查(编译成程序时检查)运行时检查(程序运行中检查)等。

契约设计编码的优点在于能够从语法、断言等多种机制上严格保证调用者和被调用的行为符合规定,我认为是一种非常强的防御性编程,能够让很多潜在的问题(特别是函数调用这种不满足代码局部性的行为)得以解决。

而契约设计编码的缺点这主要在于机器契约的逻辑表达存在困难:

  • 书写复杂:经过大二开发经历,我们意识到形式化检查是一种非常严格的逻辑形式,若真的需要程序能够执行检查,有时使用自然语言能够描述的内容需要花费大量时间书写代码的描述,甚至要定义无用的函数接口来专门抽象某一种功能的契约表达。
  • 理解困难:契约机器理解起来容易了,开发人员理解起来就困难了,复杂的契约需要花费大量时间解读,并且还可能存在疏漏。
  • 契约上手比较困难:契约的书写和大一所学的离散数学逻辑表达式很像,对于小白来说是一门新语言,短时间比较难学会。并且契约的约束条件设计也比较考验,写简单了没有作用,写复杂了就可以被当做实现了。

因此,在本次实际的结对编程中,我们没有使用契约编程的工具,但是使用了自然语言对函数设立了契约,比如最典型的分数类Rational,约定分母不能为0,并且要化简为分母大于零的最简分数形式:

// Pre-Condition: dom is not 0.
// Post-Condition: dom must > 0, gcd(num, dom) == 1
Rational::Rational(int64_t num, int64_t dom) {
	if (dom == 0) {
		std::cerr << "Detect zero division, auto turned to 1." << std::endl;dom = 1;
	}

	auto abs_num = abs_(num);
	auto abs_dom = abs_(dom);
	auto GCD = gcd(max_(abs_num, abs_dom), min_(abs_num, abs_dom));
	
	num = num / GCD;
	dom = dom / GCD;

	this->numerator = num;
	this->dominator = dom;
}

再比如上文提到的bool point_in_line_range(Line l, Point p)这个比较函数,其作用是判断点是否在线段\射线\直线上,但是其Pre-Condition就是该点已经经过判断处在l::Line这条线型几何图形所在的直线上了。


8 核心计算模块单元测试

计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 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;
}

调用逻辑测试:对于大函数的测试,主要测试返回值是否预想,对数据结构的改变是否是预想结果。例如调用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函数文件)


9 核心计算模块异常处理设计

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

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

  • 有关几何对象异常

    • 数据异常

      • CoordinateRangeException:坐标超出范围异常,即输入值大于100000,在添加集合对象时检测并触发
    • 直线类异常

      • LineCoincidenceException:存在重合异常,即输入的直线和之前添加的直线有重合,在添加集合对象时检测并触发,测试的逻辑按一下框架进行,对于有相同斜率和截距的线:

        直线 射线 线段
        新增直线 必然重合 必然重合 必然重合
        新增射线 必然重合 三种情况 三种情况
        新增线段 必然重合 三种情况 三种情况
      • CoordinateCoincidenceException:直线坐标重合异常,即构成直线的两点相同,在添加集合对象时检测并触发

    • 圆类异常

      • CircleCoincidenceException:圆重合异常,在添加几何对象时检测并触发
      • NegativeRadiusException:圆半径为负数异常,在添加几何对象时检测并触发。
  • 无关几何对象异常

    • ObjectNotFoundException:几何对象不存在异常,即通过id找不到几何对象,在get函数和remove函数中触发
    • WrongFormatException:格式错误异常,即输入的字符串不符合要求的格式,在addObjectFromFile中触发。其中每一行要求的格式为:
      • 每行仅有一个几何对象描述
      • 第一个字符描述集合对象类型,后续n个字符描述几何对象参数
      • 对于每一种几何对象,参数不可多也不可少
      • 不可有除描述用之外的其他字符,不可有多余的空格,即参数之间有且仅有一个空格,行末行首没有多余空格。

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

测试添加直线的模块(样例)

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);
    test.remove(1);
    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);
}

10. 界面模块的设计(GUI)

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

对于界面模块我们基于Qt-4.15.0开发,并使用Qt自带的QtCreator进行界面的可视化设计,在所有的参考资料中,对我们的开发影响比较大的资料有以下两个;

  1. https://www.qcustomplot.com/index.php/introduction - 图形界面的绘制。
  2. https://www.bilibili.com/video/av29968785 - QtCreator的使用入门。

我们最终的GUI完成效果如下,其具有以下几方面的特点:

  • 使用勾选框的可同时删除多个几何部件
  • 自适应的图形界面显示
  • 操作LOG日志:成功添加与失败添加、错误提示。
  • 输入体验:不允许输入非法格式数据、提供Line型几何对象类型的选择。

10. 1 界面设计

用户界面共分为两个部分,第一部分是主界面,第二部分是自适应画图的界面,两者都使用QMainWindow类进行实现,但不同的是,前者我们使用了QCreator自带的设计师功能进行了可视化的界面设计,而后者则是参考了一个名叫QCustomPlot的第三方Qt包人工进行书写的。

主窗口

Qt的设计师中,拖动各种形式的部件进行窗口化设计,我们主要用到的有一下几种部件:

  1. pushBotton:按键,所有与用户交互所执行的功能都是通过clicked()的槽函数延伸开的。
  2. SpinLine:输入框,通过限制范围来方式非法输入。
  3. Text Editor:编辑框,在本程序中用于显示交点数量的结果。
  4. label: 界面上的一些文字。
  5. listview:构建log框和object框的容器。

事件触发:我们所设计的界面中,所有的事件都是由click所触发的,因此对于每个按钮,仅需要右键点击后选择-转到槽-clicked()即可自动建立按钮与函数之间的槽连接,在所划定的函数中写对应的功能代码,即可进行运行。

获取文本:对于主窗口中的一些值的获取,在qmake和构建后,即可通过使用以下代码获取:

// 当点击“添加线类图形”后,使用一下代码获取线的类型和输入框中的内容。
type = ui->comboBox_line_type->currentText();
x1 = ui->spin_line_x1->text();
x2 = ui->spin_line_x2->text();
y1 = ui->spin_line_y1->text();
y2 = ui->spin_line_y2->text();

ListView与删除功能

ListView是一个大白框——容器,通过setModel()的方法,我们可以将对应的实质部件加入其中,我们使用了QStandardItemModel加入这个容器,以下是一些代码的节选片段。

// 主界面初始化时,将listView的model进行设置
item_model = new QStandardItemModel(0, 1);
ui->objectView->setModel(item_model);

// 当新增一个线型的部件到Object框里去时,使用的函数
void SuperWindow::add_to_item(int id, int x, int y, int r)
{
    QStandardItem *item = new QStandardItem(QString("C\t%0\t%1\t%2").arg(x).arg(y).arg(r));	// 新建Item(一行)
    item->setEditable(false);	// 内容不可变
    item->setCheckable(true);	// 增加勾选框

    item_model->appendRow(item);// 加入到ListView.ItemModel中
    item_id[item] = id;			// 建立Item与部件id之间的映射。

    // report(log)
    string message("[√] Add Circle C " + to_string(x) + " " +
                   to_string(y) + " " + to_string(r) + " successfully!");
    report(message);
}

// 当点击删除按钮后,删除勾选的项目时
void SuperWindow::on_pushBotton_DeleteObject_clicked()
{
    // 扫描是否勾选
    foreach (QStandardItem *item, item_model->findItems("*", Qt::MatchWildcard)) {
        // 该部件被勾选,需要被删除
        if (item->checkState()) {
            // delete item;
            int id = item_id[item];			// 查询该行Item所对应的部件的id。
            try {
                this->core->remove(id);
                // get name
                string object = item->text().toStdString();
                // remove plot graph
                this->plot->remove_object(id);
                item_model->removeRow(item->index().row());
                report(ret);
            } catch (exception e) {
                QErrorMessage error(this);
                error.showMessage(tr(e.what()));
            }
        }
    }
}

绘图接口

对于绘图部分,我们没有“白手起家”仅依靠Qt的函数进行构建,而是参考了第三方的工具包QCustomPlot进行开发,其利用cpp和h文件将Qt的功能进一步封装实现,被封装在一个MainWindow的类中,我们在使用时仅需要调用其show函数即可进行展示。

同时,对于画图功能,我们还新增了自适应界面,由于边界是在变化,而且最大边界的范围不好预估,因此我们采取的是每次画图时,将所有的部件拿出来进行重新绘图。对于边界的划定,我们使用的一下几个内容进行确认:

  • 所有交点的坐标。
  • 对于线型几何对象时,所指定的两个点的坐标。
  • 对于圆型几何对象,使用x-r, x+r, y-r, y+r四个参数进行确认。

边界通过取min(), max()完成,并保证上述的点坐标均被包含在之内,而后我们在生成的边界框上画一条透明的对角线,运用槽函数即可实现边界的自适应显示。

// 使用槽函数进行自适应界面显示。
connect(ui->customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->xAxis2, SLOT(setRange(QCPRange)));
connect(ui->customPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->yAxis2, SLOT(setRange(QCPRange)));
/*
 * Plot transparent box to fit entire box.
*/
int MainWindow::plot_transparent_box()
{
    cout << x_max << " " << y_max << " " << x_min << " " << y_min << endl;
    ui->customPlot->graph(1)->setPen(QColor(0, 0, 0, 0));
    ui->customPlot->graph(1)->setLineStyle(QCPGraph::lsNone);
    QVector<double> vx, vy;
    vx.push_back(x_max);vy.push_back(y_max);
    vx.push_back(x_min);vy.push_back(y_min);
    ui->customPlot->graph(1)->setData(vx, vy);

    ui->customPlot->graph(1)->rescaleAxes();
    ui->customPlot->replot();

    return 0;
}

当点击显示按钮时,会执行以下四部分功能:

void SuperWindow::on_pushButton_show_clicked()
{
    // 1 重设范围
    plot->reset_scale();
    // 1.1 用交点更新范围
    int count = core->getPointsCount();
    double* point_x = new double[u_int(count)];
    double* point_y = new double[u_int(count)];
    core->getPoints(point_x, point_y, core->getPointsCount());
    this->plot->update_scale_by_intersects(point_x, point_y, core->getPointsCount());
    // 1.2 用线和圆更新范围
    for(auto it = plot->id_graph.begin(); it != plot->id_graph.end(); it++){
        update_scale_by_object(...);
    }
    
    // 2适当放宽范围
    plot->smooth_scale(); 

    // 3.1 根据新的范围绘制几何部件
    for(auto it = plot->id_graph.begin(); it != plot->id_graph.end(); it++){
		plot(...);
    }
    // 3.2 绘制交点
    plot->plot_intersects(point_x, point_y, count);

    delete[] point_x;
    delete[] point_y;

    // 4. 绘画透明框,限制显示范围
    plot->plot_transparent_box();
    plot->show();
}

11. 界面模块与计算模块的对接

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

对于gui模块和core模块之间的对接,是我们本次结对编程所遇到的一个困难,虽然结对编程以领航员—驾驶员的形式展开,同一时刻仅有一位编程人员,但在远程环境下,有关系统环境和编译环境的差异是无法避免的。整体来说,分为*.dll*.lib文件的导出、QtCreator导入外部库和程序的打包三步。

11.1 DLL与LIB文件的生成

首先,在VS2017上完成了核心计算模块的编写和测试后,我们在VS2017上新建了一个DLL动态链接库的项目,用于生成模块文件。

将原工程的代码与头文件迁入后,在*.h头文件中外部可见的程序接口加入如下的函数签名,并进行项目的生成,在项目文件夹下即可看到libdll文件,其中,lib文件作为外部库需要在界面模块的项目中引入,而dll文件是作为可执行程序的链接文件。将生成文件与没有附加导出签名的头文件放在一起形成文件夹,即可进入在QtCreator的IDE中导入改模块进行运行的过程。


11.2 在IDE中导入动态链接库

对于QtCreator,其项目中提供了导入外部库的工具,在项目名上右键-添加库,选择外部库,而后选择库文件点击确认即可导入,在确认后,.pro文件会显示项目组织变化的部分以让你确定保存(因为其自动化添加的关于外部库的内容可能是不正确的)。

在网络的教程中,也有手动地向.pro文件加入项目组织的方法,代码如下。其中LIBS指定了*.lib*.dll文件的位置,-l参数和后面紧跟的文件名之间不能有空格,而INCLUDEPATHDEPENDPATH则指定了外部库的依赖地址,有了这两句以后,便可以在原本的工程文件里通过#include操作将接口包含进来进行调用。

unix|win32: LIBS += -L$$PWD/lib/ -lGeometryCore

INCLUDEPATH += $$PWD/lib
DEPENDPATH += $$PWD/lib

11. 3 界面交互程序的导出

对于QtCreator生成好的可执行程序,我们测试过其在系统环境变量中如果不加入相应Qmakebin路径下是无法运行的,因此需要对文件进行打包,关于打包的具体操作流程可以参考windeployqt,即可将可执行程序所依赖的Qt动态链接库文件进行批量的导出。


11. 4 坑

之前说过,这部分是我们所要的较困难的一部分,其在于中间走过了很多弯路,在此做一些总结。

问题:链接库与界面工程的编译环境不同

表现:Undefined Reference for .....

QtCreator默认使用MinGW编译器对文件进行编译,而外部链接库的文件则是用MSVC2017生成,一种编译器所生成的外部库是无法作为其他编译器的项目调用的。

在生成时编译器所报告的错误并不明显,因此耗费的大量的时间查阅StackOverflow的博客才得出问题。由于QtCreator-5.14.0仅支持MSVC2017的版本,而合作者的电脑装载的是MSVC2019的版本,因此发现问题后还需要手动配置编译器,所幸虽然QtCreator发出了版本不适配的警告,但MSVC2019MSVC2017是兼容的,因此成功地解决了此问题。

问题:接口定义规范

表现:核心模块返回STL标准容器的函数在被界面模块调用时会出现崩溃。(例如vector getPoints())

对于这点我们我们能够较快地定位到位置,并猜测可能是因为使用了较为高级的STL容器导致的问题,但是对于接口返回一些我们自定义的几何类,则没有出现该问题。
关于其原理我们至今不是特别清楚,但在后续的解耦合附加题中,为了解耦的有效性,我们使用了纯C接口的方式完成核心模块的调用。

问题:导入lib时.pro文件设置的问题

d后缀:在上文中的导入流程图第三张图中可以看到,存在一句为debug版本添加后缀-d,这一点要和核心模块导出的内容形式做到一致,对于没有d的则不要添加。

LIB路径:由于IDE自动检测路径,但并不一定正确,因此需要手工确认,否则include包含外部库的头文件。


12. 结对过程描述

描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。

我们使用了腾讯会议和TIM进行结对编程,主要使用腾讯会议完成驾驶员和领航员的模式,当环境配置等问题出现时,使用TIM的远程控制进行。


13 有关结对编程的思考

看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。

13.1 结对编程的优点与缺点

结对编程的核心思想,就是把个人的一些好的开发方法发挥到极致,因此其优点应该是这些开发方法的具体内容,而缺点则是引入的副产品:

优点

  • 提升设计质量和代码质量:两位合作者在设计和编写代码时,两双眼睛的角度必然比一双眼睛开阔,并能够提供不间断的复审和提醒,使得写出的代码的因为细节造成的bug更少,一次成型率更高。
  • 更均衡的压力和责任释放:这一点是我在结对编程中所具体体会到的,在做一项任务时,如果是一个人做压力会大很多,因为必须凭一己之力保证正确性和性能。结对编程中,每当共同完成的程序遇到问题时,驾驶员和领航员从规则上讲都有责任,从而减轻个人压力,提升后续的生产力。
  • 理性克制感性:程序是一种符合严格逻辑的工程,需要依靠尽可能的理性进行完成,但是孔子都说过君子慎独——人在独自的时候感性因素会更多,这不利于工程开发,因此两人存在的环境更能够保持理性。
  • 学习经验和技术:结对编程时两个人是同步的,对于对方掌握的工具技术,能够像看直播的形式,从整体到细节上透彻地学习到。

缺点

  • 开发效率降低:结对编程模式的进入需要磨合期和两人性格的适应,在短时间内(比如本次作业),加上远程限制,初试结对编程的效率会比个人开发的效率更低。

13.2 团队成员分析

YZN LPX
优点 学习新工具能力强。 责任心强,擅长把握整体进度和推进。
代码鲁棒性强。 沟通能力出色,与其他小组交流和共享对拍测试。
设计严格细致。 编码强,实现速度快。
缺点 时间意识不强,容易拖整体进度。 在设计与接口编写上重视程度不高。

14 附加题 - 松耦合测试

学号 CnblogProfile GitHubRepo
17231162 (本小组) @PX_L †Repository
17373321 (本小组) @youzn99
17231145 (合作小组) @FuturexGO *Repository
17373331 (合作小组) @MisTariano

14.1 界面效果展示

我们与进行了互换dll测试,实现了GUI和测试模块都能够使用对方的内核模块进行运行,由于正常功能不太能看出差异,因此我们选择使用异常处理的方式来展示交换过计算核心后的内容。

使用我们的gCore模块搭建的dll的部分错误展示:

  • 添加一条非法直线:
    • 我们的Core:“Error:coordinate coincident”
    • 对方的Core:“Needs two points to define a line”

  • 添加两条重合直线:
    • 我们的Core:“Error:this line has been coincident”
    • 对方的Core:“This line overlaps with another line”

  • 添加文件内容格式错误(存在不符合参数要求的直线L 0 0 1 1 1):
    • 我们的Core:弹出Error:Something wrong的窗口,并在log目录中显式“Get the Wrong Format Input”。
    • 对方的Core:弹出Error:Something wrong的窗口,并在log目录中显式“Nees two points to define a line”。


14.2 纯C接口设计

C++STL容器作为接口时存在错误的风险。

在没有合作前的接口的设计中,我们便已经尝到了使用高级容器例如vector所存在的风险,据了解,这种风险情况并不是在每一组中都有发生,但是为了接口的鲁棒性,我们基于合作双方的接口,给接口穿上了一层新外套——标准接口,这套接口使用纯C的方式书写,并且计算核心内部的类和结构体对外部是不可见的,外部均通过容器的方式对内部数据进行访问。

接口的具体定义如下:

#ifndef GEOMETRY_STDINTERFACE_H
#define GEOMETRY_STDINTERFACE_H

struct gPoint {
    double x;
    double y;
};

struct gShape {
    char type;
    int x1, y1, x2, y2;
};

struct gFigure {
    unsigned int figureId = 0;
    gPoint *points = 0;  // only available after updatePoints()
    gShape *shapes = 0;  // only available after updateShapes()
	gPoint upperleft = { -5, 5 };
	gPoint lowerright = { 5, -5 };
};

enum ERROR_CODE {
    SUCCESS,
    WRONG_FORMAT,
    VALUE_OUT_OF_RANGE,
    INVALID_LINE, INVALID_CIRCLE,
    LINE_OVERLAP, CIRCLE_OVERLAP,
};

struct ERROR_INFO {
    ERROR_CODE code = SUCCESS;
    int lineNoStartedWithZero = -1;
    char messages[50] = "";
};

gFigure* addFigure();
void deleteFigure(gFigure *fig);
void cleanFigure(gFigure *fig);
ERROR_INFO addShapeToFigure(gFigure *fig, gShape obj);
ERROR_INFO addShapeToFigureString(gFigure *fig, const char *desc);
ERROR_INFO addShapesToFigureFile(gFigure *fig, const char *filename);
ERROR_INFO addShapesToFigureStdin(gFigure *fig);
void removeShapeByIndex(gFigure *fig, unsigned int index);
void updatePoints(gFigure *fig);		// fig容器的point指针将指向交点数组。
void updateShapes(gFigure *fig);		// fig容器的shape指针将指向形状数组。
int getPointsCount(const gFigure *fig);
int getShapesCount(const gFigure *fig);

#endif //GEOMETRY_STDINTERFACE_H

其中标准接口使用gFigure结构体来特定标识一个容器类对象,在后续对对象的各种操作中,都是基于对gFigure结构体进行操作。此外接口还提供了两种封装结构gShapegPoint,来表示点和几何形状。

在定义新接口后,我们通过对老接口函数的特定操作,来完成新接口的各种要求,具体代码见github项目的dev-combine分支


14.3 测试模块效果与性能测试

由于两组同学使用的编译环境并不相同(我们使用windows+MSVC,另一组使用MacOS+MinGW),因此直接使用对方构建的dll库会出现程序不能加载dll的问题。因此需要从对方github仓库clone代码,然后进行dll生成。

使用对方的dll后,命令行程序运行结果(包括异常报告)

img

和我们的dll运行结果进行对比:

img

对比后发现,对于字符串异常处理,我们的模块处理比较粗糙,仅是进行WrongFormat报错,而对方的报错信息会告知,Line需要什么样的参数,这样方便用户检错;而对于异常种类的划分,对方模块仅进行了3中大类型异常信息返回,而我们模块的异常信息返回更加丰富一些。

对于性能测试,进行600w级别的点数据进行测试,结果如下:

img

可以发现,对方组的程序在600w级别的性能上比我们要好的多。具体细节了解到,对方组的数据结构比较简洁,并没有大规模的使用STL的各种容器,而我们在维护数据的时候太过于依赖STL的标准容器,导致在STL容器在维护过程会出现各种不必要的拷贝耗费时间。

此外,我们存储的点是使用new方法进行存储的,而new构造一般会比一般构造要耗费更多的时间,这也导致我们在性能上会更差一些。


14.4 关于松耦合设计的思考

安全性与鲁棒性:相比新的标准接口,我们的接口的安全性和可用性都比较低。首先我们的旧接口参数有STL的容器类,甚至传了引用参数,而不同环境下的这些实现都是不同的,因此在可用性上会稍差。并且旧接口在导出的时候必须要导出所有的内部类,因为之间具有依赖关系,这样就暴露了内部实现,安全性降低。因此在接口设计时,用类C接口会比较好。

接口功能的设计:双方开发的模块需要解耦互用是在现实中一种常见的问题,而且“解耦的需求”往往并不是从一开始就进行的,因此必然存在双方的接口存在差异。这也是我们和合作小组所遇到的问题,因此,在设计接口时,我们遵循两条规则——外套+取并集

  • 外套是指在已经实现好的接口之上再封装一层。
  • 并集则是指的是不是所有接口我们都要使用,某些接口可以被其他接口经过组合后实现。例如,我们的界面在读取文件时,会按行解析而后传递给计算模块,而合作小组则直接将文件名传递给计算模块,因此在实现加载文件的功能时,双方调用的接口是不同的。

15 关于PSP时间记录的思考

相比上次个人项目,这次PSP开发所耗费的时间就比原本预计时间差很多了,纵观整个项目的开发过程,我认为造成这种情况的原因和解决方案有:

原因 解决方案
中途因为有理数类无法满足需求而迭代调整为Double双精度函数,重写了模块和单元测试。 代码未动,设计先行。设计自任务布置下来后便开始着手准备,公式、图表等抽象表示先设计出来。在有设计思路后,最后给团队留下1-2天的”冷静期“,反思设计所存在的问题。
DLL动态链接库文件的编译环境配置 对于目的性任务,最好使用自己擅长的工具;若对于时间紧张的任务需要新工具的学习,应该咨询和商定后慎重决定。
远程结对编程时共同成本大,作息有差异。 在结对编程前做好原则性的约定和计划,并允许计划存在一定的弹性。
posted @ 2020-03-24 18:55  youzn99  阅读(371)  评论(2编辑  收藏  举报