软件工程作业——结对编程实践

  1. 在文章开头给出教学班级和可克隆的 Github 项目地址(例子如下)。(1')
项目 内容
这个作业属于 2020春季计算机学院软件工程(罗杰 任健)
这个作业的要求是 结对项目作业
我的教学班级 006
结对队友博客 favourLZH
结对项目的GitHub地址 IntersectDualProj
我对这项作业的目标是 提高个人程序开发素质,在与同伴的结对编程中不断配合、成长,
一同写出高性能程序

作业要求

本次作业为个人项目中求解交点的增量扩展,主要目的是为了让同学们通过身体力行了解以下三点:软件需求的变更,封装,接口与松耦合,以及错误处理。

  1. 软件需求的变更
    在个人项目的基础上额外支持两个几何对象:线段和射线
  2. 封装
    把求解交点的功能能独立出来,成为一个独立的模块(class library, DLL,或其它),这样命令行和 GUI 的程序都能使用同一份代码
    我们称之为计算核心“core 模块”,这个模块至少可以可被用于以下地方:
    1. 命令行测试程序中用于提供核心功能;
    2. 在单元测试框架下验证模块正确性;
    3. 与数据可视化部分结合使用提供核心功能
  3. 接口
    我们知道软件并非只有计算核心,实际的软件是交付给最终用户的软件,除了计算核心外,还需要有一定的界面和必要的辅助功能。那么这个 core 模块和使用它的其他模块之间需要如何进行交流呢?答案是通过一定的 API(Application Programming Interface),通常也可称作接口。通过明确模块之间的接口,则模块 A 对于模块 B 的认知则只有 B 提供的接口,而与 B 的实现无关。
  4. 松耦合
    系统的组件对于其它的组件只具有很少的认知或者没有认知
  5. 错误与异常处理
    对于一个真实的软件来说,来自用户的非法输入是难以避免的。如果程序的输入出现了错误,比如命令行参数是其他字符,或者有多个无意义参数等等,要怎样才能告诉函数的调用者“你错了”?又该如何方便地告诉函数的调用者“哪里错了”?在这种时候,我们一般会定义各种异常(Exception),让模块在碰到各种异常情况的时候,能给调用者充分的错误信息。
    在本次作业中,将要求同学们进行错误处理,体会一个真实软件在错误处理上的相关考虑。

作业完成功能划分

  • 助教小哥哥小姐姐非常友善的为我们的代码提交规定了几个step(这样也方便审核~
    • 所以我们的软件开发各步骤的顺序和内容就确定如下了
  1. 基本功能实现及测试:单元测试、并要求得到90%以上覆盖率
  2. 拓展功能后封装:看书相关内容,并说明命令行、GUI接口设计的理由,并构造测试
  3. 支持异常处理:异常处理设计及其测试
  4. 增加界面模块:开发UI模块
  5. 松耦合:不同小组间交换核心模块与界面模块测试

PSP

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

14.在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。(0.5')

  • PSP
PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
- Estimate - 估计这个任务需要多少时间 10 10
Development 开发
- Analysis - 需求分析 (包括学习新技术) 1. 5
2. 45
3. 45
4. 30
5. 30
总计:155
1. 75
2. 30
3. 25
4. 120
5. 120
总计:370
- Design Spec - 生成设计文档 20 40
- Design Review - 设计复审 (和同事审核设计文档) 30 10
- Coding Standard - 代码规范 (为目前的开发制定合适的规范) 20 15
- Design - 具体设计 1. 15
2. 30
3. 20
4. 45
5. 30
总计:140
1. 15
2. 30
3. 30
4. 45
5. 30
总计:150
- Coding - 具体编码 1. 20
2. 30
3. 60
4. 90
5. 30
总计:230
1. 90
2. 75
3. 75
4. 120
5. 30
总计:390
- Code Review - 代码复审 60
上述5步骤各10min
30
- Test - 测试(自我测试,修改代码,提交修改) 1. 40
2. 60
3. 60
4. 90
5. 30
总计:230
1. 120
2. 45
3. 120
4. 120
5. 30
总计:435
Reporting 报告
- Test Report - 测试报告 15 15
- Size Measurement - 计算工作量 15 15
- Postmortem & Process Improvement Plan - 事后总结, 并提出过程改进计划 15 15
合计 940 1495

开发

  • 按照作业要求实现的功能,开发顺序如下

Step 1 基本功能实现及测试

需求分析与学习

  • 这次结对编程,我们采用队友个人项目的程序作为基础程序,

    • 原因是,上次她用了更多的时间去编写和测试,完成了附加题,代码完整度和性能都应该比我的更好,所以采用
  • 而我在阅读她的代码过程中,对代码做出了以下几点改进

    1. 计算类、图形类、基础函数类分离
    2. 在程序中加入一些TODO标记:(1)命令行错误处理;(2)直线boundary以融入线段和射线;(3)运行时间分析;(4)更换部分数据结构以优化性能
  • 并且下载学习了覆盖性测试的软件OpenCppCoverage Plugin

设计

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

  • 一共6个类(除开def,内有通用函数,比如double精度比较)
软件工程作业——结对编程实践_第1张图片
  • 类与类之间关系
graph LR A[Point] --> B[Line] A --> C[Circle] A --> D[Calcalator] B --> D C --> D D --> E[IOinterface] D --> F[Exception] E --> F E --> G[GUI调用] E --> H[cmd调用]
  • 关键部分说明,由于交点计算的关键函数实现都在上一次作业博客中说明了,这次不做特别阐述,着重说明新增功能的拓展。
  • Calculator 类
    • 这是由于新增射线和射线需要做比较多改进的地方
    • 其中Line与line的交点计算的预判(用到叉乘的方法) 参见 博客,无法预判的内容比如射线,则先计算交点,再判断是否在射线上
    • 直线和射线与圆的问题处理,计划先算出交点,再判断点是否在line上
    • 线段与圆关系进行预判,比如线段两点都在圆内,来一定程度的降低时间复杂度
    • 圆与圆的交点无需改动
class Calculator {
public:
	Calculator();
	inline double xmult(Point v1, Point v2);
	double xmult(Point o, Point a, Point b);
	//判断点是否在line上 在line上则return true
	bool pOnLine(Point p, Line l);
	// 圆内 return true;  相切/相离  return false;
	bool pInCircle(Point p, Circle c);
	bool isParallel(Line l1, Line l2);//判断两线是否平行 (并捕捉 重叠的异常)	 
	int haveIntersection(Line l1, Line l2, set& nodeSet);
	int haveIntersection(Circle c, Line l, set& nodeSet);
	int haveIntersection(Circle c1, Circle c2, set& nodeSet);
	//计算全部交点
	int countAllinsect(vector lVec, vector cVec, set &nodeSet);
};
  
  • Line类
    • 其中设计新增射线和线段的功能
// 'L' -> line;
// 'R' -> radio;
// 'S' -> segment;
class Line
{
public:
	Line();
	Line(char newType, double x1, double y1, double x2, double y2);
	char getType();
	double getA();
	double getB();
	double getC();
  double getSlope();
	Point  getP1();
	Point  getP2();

private:
	char type;
	Point p1;
	Point p2;
	double A;
	double B;
	double C;
  double slope;
};
  • Point类 & Circle类的设计沿用上一次作业设计

测试

  1. 计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。(6')
  • 由于openCppCoverage在VS中下载缓慢,可以选择在marketplace中下载,目前还没有发现openCppCoverage插件可以用于检测单元测试覆盖率的功能,只能用它来检测整体代码覆盖率,得出覆盖率如下

软件工程作业——结对编程实践_第2张图片

  • 在网上查找,VS的单元测试覆盖率可能需要旗舰版才能完成,所以目前没有得出单元测试的结果,但是我们自己的单元测试编写的有层次有条理,我们对单元测试有100%覆盖的信心

  • 整体测试架构

    • 根据设计的几大类,采用bottom-up的方式进行测试程序的编写
  • 对于每个类的每一个函数都进行了细密的测试

    • 比如下面展示的对于直线类的测试,细致到每一个函数
TEST_CLASS(LineTest) {
public:
	TEST_METHOD(lineTestBasic) {
		Line l1('L', 1, 1, 2, 2);
		Line l2('R', 0, 2, 1, 0);
		Line l3('S', 1, 0, 5, 0);

		// test getType
		Assert::IsTrue(l1.getType() == 'L');
		Assert::IsTrue(l2.getType() == 'R');
		Assert::IsTrue(l3.getType() == 'S');

		// test get abc
		Assert::IsTrue((l1.getA() == 1 && l1.getB() == -1) ||
			(l1.getA() == -1 && l1.getB() == 1)
			&& l1.getC() == 0);
		Assert::IsTrue((l2.getA() == -2 && l2.getB() == -1 && l2.getC() == 2) ||
			(l2.getA() == 2 && l2.getB() == 1 && l2.getC() == -2));

		// test get p1 p2;
		Point p1(1, 1);
		Point p2(1, 0);
		Point p3(5, 0);
		Assert::IsTrue(l1.getP1() == p1);
		Assert::IsTrue(l2.getP2() == p2);
		Assert::IsTrue(l3.getP2() == p3);

	}
};
  • 对于求交点的重要复杂部分,我们的测试也做的更细致
    • 比如直线相交的测试,我们对于几种直线间的情况比如相交、平行、重叠,以及三种直线的情况(线段、射线、直线)都做了非常细致的测试
// test parallel
		TEST_METHOD(LinePrl)
		{
			Calculator* calc = new Calculator();
      // 三种线段
			char line = 'L';
			char radio = 'R';
			char segment = 'S';
			Line lTD(line, 1, 3, 2, 3);
			Line rTD(radio, 2, 5, 4, 5);
			Line sTD(segment, 51, 6, 24, 6);
			Calculator* cal = new Calculator();
			Assert::IsTrue(cal->isParallerl(lTD, rTD));
			Assert::IsTrue(cal->isParallerl(lTD, sTD));
			Assert::IsTrue(cal->isParallerl(rTD, sTD));

			Line l1(line, 3, 3, 5, 5);
			Line r1(radio, 6, 5, -100, -101);
			Line s1(segment, 0, 1, 100, 101);
			Assert::IsTrue(cal->isParallerl(l1, r1));
			Assert::IsTrue(cal->isParallerl(l1, s1));
			Assert::IsTrue(cal->isParallerl(r1, s1));
			Assert::IsFalse(cal->isParallerl(l1, sTD));
			Assert::IsFalse(cal->isParallerl(r1, sTD));
			Assert::IsFalse(cal->isParallerl(s1, rTD));
		}

Step 2 拓展功能后封装

接口设计

  1. 看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
  • 针对Information hiding原则(参考),我们对step1中的程序做了如下改进:

    • 外部无法访问原则,所有容器访问改为迭代器访问,而不是数组访问
    • 人机交互逻辑,集中到一个单独的类、包或者子系统中,这为我们的UI接口设计确定了理论基础
    • 具体数字常量化/宏,我们对于精度的设定就采用了宏定义的方式#define EPS (1e-8) 这样在修改具体精度时,只用一处修改即可
    • 信息访问限制,我们在对点、线、圆等对象有严格的访问限制,所有的内部属性为private,在构造时即确定内部属性,之后只能读取,不能修改
  • Interface Design

    • 我们的接口设计遵从高内聚,低耦合的设计思路:单一模块只实现一个功能,模块和模块之间的依赖尽可能小甚至没有,模块间的接口也是考虑全局而精简设计
    • 比如在Calculator类的设计中,我们充分解析了交点计算过程中的关键计算流程,设计出以下7个分层次的计算方法,分别对点在线上、点在圆内、平行判断、交点计算(线与线、线与圆、圆与圆、汇总计算)进行各模块的编写
    • 使得整体复杂的计算过程充分解耦,降低计算过程的代码编写复杂度,减少bug出现的机会,提高计算交点过程的整体可靠性。
// Calculator.h	
  //判断点是否在line上 在line上则return true
	bool pOnLine(Point p, Line l);
	// 圆内 return true;  相切/相离  return false;
	bool pInCircle(Point p, Circle c);
	// TODO: add slope class
	bool isParallel(Line l1, Line l2);//判断两线是否平行 (并捕捉 重叠的异常)	 
	int haveIntersection(Line l1, Line l2, set& nodeSet);
	int haveIntersection(Circle c, Line l, set& nodeSet);
	int haveIntersection(Circle c1, Circle c2, set& nodeSet);
	//计算全部交点
	int countAllinsect(vector lVec, vector cVec, set &nodeSet);
  • Loose Coupling,这次附加题是对这个原则的一个理论实践
    • 学习之后,总结为如下几点:
      1. 接口和抽象类的正确使用
      2. 增加中间层
      3. 减少对象之间的依赖
    • 这个部分和interface design所要求的内容有一些重合,不过非常值得学习和实践的是增加中间层这部分内容
    • 上学期通过数据库理论的学习,我明白了数据库从应用层到中间层再到底层逻辑,其中三层任何一层的修改,都不会涉及到其他层次的变动,这是因为中间两级镜像层发挥了巨大的作用
    • 而我们这次的计算核心模块和GUI以及命令行的对接,也要有中间转换层,来实现系统的核心模块和用户交互层的彻底解耦。下面的代码就是我们核心模块面向GUI和cmd的中间层设计。
    • 内部计算实现彻底隐藏,只注明外部调用接口规范,这个内容会在GUI设计部分详细说明。
// IOinterface.h
IMPORT_DLL int guiProcess(std::vector>* points, std::string msg);
IMPORT_DLL void cmdProcess(int argc, char* argv[]);

UML

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

  • 使用VS类图导出的UML如下
    • 其中cmd和GUI接口部分使用函数编写,不在类图中。其主要根据UI指令,构造并维护各种图形集合,调用Calculaor类计算交点并返回给用户,并处理上述过程的异常抛出。
      软件工程作业——结对编程实践_第3张图片

性能分析

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

  • 由于这次作业主要是有上次作业的基础的,所以整体性能改进相比起上次“女娲补天”式的提升,这次只是小修小改。记录几个修改:

    1. 直线平行判断优化,我们将直线的slope属性存储在直线class中,判断平行时直接调用判断相等,而不用多次计算斜率
    2. 部分小函数宏定义优化,对于double相等以及大小比较的小型但是多次调用的函数,我们将其设定为宏定义,减少函数调用的时间消耗
    3. 以及一些小的修改,根据函数调用的局部性原理,将分支语句中更多调用的语句提前之类
    4. 学习了助教发的sweep line提示文档,对比其中内容和自行编写的代码,由于sweep line似乎只能优化线段的内容,作业时间有限,也就没有做这个部分的优化。
  • 最终性能分析图片如下

    • 占用性能最多的函数还是求直线交点函数,该函数中交点集合的插入部分是占用CPU最多的数据操作语句
      软件工程作业——结对编程实践_第4张图片

Design by contract & Code contract

7.看 Design by Contract,Code Contract 的内容:

http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

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

  • design by contract & code constract
    • 设计即要确保交付性能,而且类的完成者和使用者、函数的调用者和被调用者等等,各种API的开发和调用,均需根据contract开发,也就是说明外界保证什么(传入什么参数),内部向外界提供什么(返回什么结果),在处理过程中维护什么样的不变性
    • 以上理解,参考wiki中的叙述

Similarly, if the method of a class in object-oriented programming provides a certain functionality, it may:

  • Expect a certain condition to be guaranteed on entry by any client module that calls it: the method's precondition—an obligation for the client, and a benefit for the supplier (the method itself), as it frees it from having to handle cases outside of the precondition.
  • Guarantee a certain property on exit: the method's postcondition—an obligation for the supplier, and obviously a benefit (the main benefit of calling the method) for the client.
  • Maintain a certain property, assumed on entry and guaranteed on exit: the class invariant.
  • The contract is semantically equivalent to a Hoare triple which formalises the obligations. This can be summarised by the "three questions" that the designer must repeatedly answer in the contract:
    • What does the contract expect?
    • What does the contract guarantee?
    • What does the contract maintain?
  • 以及参考Code Contracts for .NET的描述

The contracts take the form of pre-conditions, post-conditions, and object invariants. Contracts act as checked documentation of your external and internal APIs. The contracts are used to improve testing via runtime checking, enable static contract verification, and documentation generation.

  • 我认为以上原则在团队或者结对编程过程中是非常有助于团体开发效率的,按照constract开发并交付
    • 但问题在于这种开发方式在面对大的需求变化时候重构时候可能过于死板,constract的细密度和维护设计都需要额外的开销
  • 而我们的作业在实际设计中,尤其是GUI接口的设计,也最遵循了这一原则
    • 接口如下
    • gui提供直线和圆的msg信息和返回交点的容器,guiProcess函数保证返回交点数目并把交点写入容器,guiProcess在运行过程中使用异常处理的方法保证了直线、圆、线段等内部特性的稳定
    • 同理cmdprocess需要参数个数和参数字符数组,向输出文件返回交点数目,运行过程中维持直线、圆、线段等内部特性
// IOinterface.h
IMPORT_DLL int guiProcess(std::vector>* points, std::string msg);
IMPORT_DLL void cmdProcess(int argc, char* argv[]);

Step 3 支持异常处理

注意事项
当前阶段,题目中描述了若干对于输入的保证,规定了合法输入的含义。在后续的异常处理中,所有关于输入的保证均会被去除

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

异常处理设计

  • 几种异常
  1. 命令行输入异常(参数、文件名)
  2. 输入文件异常(输入曲线不符合格式,输入线段数目,“乱码”输入)
  3. 直线异常
  4. 曲线异常

异常处理测试

1.1. 命令行输入——参数异常

intersect.exe -n 
intersect.exe -i input.txt -o output.txt -h

1.2. 命令行输入——文件名异常

intersect.exe -i test.txt -o output.txt
intersect.exe -i input.txt -o out.txt
  • 根据以上测试,得到异常处理测试结果

软件工程作业——结对编程实践_第5张图片)

2.1. 输入文件内容——输入曲线不符合格式

## 1. 直线输入错误
R 0 43 9 -3 98

# 2. 输入几何对象参数含有前导0
S 09 12 45 89

# 3.  多个字母
S S 3 2 1 

# 4. 只有数字
3 1 5 2 76

# 5. 字母数字乱序
5 L 1 4 6

# 6. -后不接数字
L - - - -

# 7. 错误数字
L 98-736 92 0 82

2.2. 输入线段数目异常

# 1. 输入线段 < 1
0
-94

# 2. 输入线段与实际线段数不同
1
L 0 10 8 83
R 91 46 2 0

4
L 56 28 82 4
R 19 41 34 56
C 56 168 5 

2.3.曲线输入文件无法打开

3.1. 直线不符合标准

## 1. 输入两点重合
L 0 1 0 1 

## 2. 输入数字超范围
R 100000 0 0 0
L -100000 4897 278 1
S -100005 3784 942 61

3.2.有无穷多交点

#1.  正确情况
3
S 1 1 3 3
S 5 5 100 100
R 0 0 -55 -55


# 2. 异常
2
S 0 1 7 8
R -4 -3 -3 -2

2
S 0 1 7 8
L 9 10 100 101

2
R -4 -5 0 -1
L -99 -100 -50 -51

2 
S 1 0 3 0
S 2 0 4 0 

2
S 1 0 3 0
S 2 0 3 0 

2 
S 1 0 3 0
S 1 0 5 0 

2
S 1 0 3 0
S 0 0 5 0

4.1. 圆不符合标准

## 1. 输入圆半径小于1
C 0 0 0

C 84 72 -23

## 2. 输入数字超范围
C -100000 4897 278
  • 对于以上的样例,分别写了测试样例,得到测试结果如下:

软件工程作业——结对编程实践_第6张图片

Step 4 增加界面模块

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

这次的GUI我们是用Qt实现的。Qt的良好封装机制使得Qt的模块化程度非常高,可重用性较好,对于用户开发来说比较方便。 Qt提供了一种称为signals/slots的安全类型来替代 callback,使得各个元件之间的协同工作变得十分简单。

(1)界面设计

  • 我们先通过集成在 Qt Creator 中的 Qt Designer 对窗体进行可视化设计。
    • 最终界面如下:

软件工程作业——结对编程实践_第7张图片

  • 为了方便用户操作、减少用户记忆负担、减少用户错误信息,我们的设计做了如下改良:

    • 关文件导入:我们实现的“..."按钮可直接浏览文件夹选择文件,无需手动输入路径。
    • 添加操作:由于几何对象的参数要求为在(-100000,100000)之间的不含前导零的整数,我们设置了SpinBox,其限制了正确的参数形式,避免手动输入带来的参数格式错误问题。对于几何体的类型,我们实现了下拉选框。
    • 删除操作:为了减少用户的记忆负担,我们在listWidget中实时呈现了当前几何图形。为了避免删除操作的错误信息和比较过程的繁琐,我们设置了复选框。用户选择列表中的几何图形,点击“删除几何图形”即可。这样既方便了用户,也减少了我们异常处理的负担。
    • 求解交点:点击“求解交点”按钮,则会计算出所有交点的左边,并在下方显示求解的交点数。
    • 绘制:在用户完成全部对当前几何图形的修改和求解交点后,点击“绘制几何图形和交点”按钮,我们将统一更新画布。这样避免了用户频繁的阶段性操作带来的无用计算。
  • 界面使用注意点:

    • 在点击求解交点后,方有交点的数据,才能绘制出交点。
    • 在点击绘制几何图形和交点后,才会更新当前几何图形的绘制。

(2)主要代码说明

  • Qt使用了信号和槽来代替回调函数,实现对象间的通信。当一个特定的事件发生时,信号会被发送出去。Qt的窗体部件(widget)拥有众多预先定义好的信号。槽,则是对一个特定的信号进行的反馈。我们这次的实现主要是创建窗体部件(widget)的子类并添加自定义槽,以便对感兴趣的信号进行处理。

  • 我们实现的类中,有两个重要的属性vector figuresvector points,分别存放当前几何对象当前交点

  • a.文件导入

//点击"..."按钮,浏览文件夹
void IntersectGUI::on_findFileButton_clicked() 
{
	filePath =
		QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Save path"), QDir::currentPath()));  //文件路径
	if (!filePath.isEmpty())
	{
		if (ui.fileBox->findText(filePath) == -1)
			ui.fileBox->addItem(filePath);//在comboBox中显示文件路径
	}
}
//点击"输入文件"按钮,导入文件数据
void IntersectGUI::on_infileButton_clicked()
{
	QFile* file = new QFile;   //申请一个文件指针
	file->setFileName(filePath);   //设置文件路径
	bool ok = file->open(QIODevice::ReadOnly);
	if (ok)
	{
	……//读入文件并将文件中的数据处理后存入figures中	
		}
		file->close();
	}
}
  • b.求解交点
    • 点击“求解交点”按钮,将当前几何体的数据转换成相应的接口处的数据,调用dll中的函数,计算交点并返回。具体接口设计,下一部分详细介绍。
int IntersectGUI::on_calcPButton_clicked()
{
	points.clear();
	std::string input;
	size_t n = figures.size();
	……//将figures中的几何体数据转换成相应的接口中的数据input
	int cnt = 0;
	//cnt = guiProcess(&points,figures);
	try {
		cnt = guiProcess(&points, input);
	}
	catch (std::exception e) {
		QString dlgTitle = QString::fromLocal8Bit("计算出现错误");
		QMessageBox::critical(this, dlgTitle, e.what());
		return 0;

	} {
	}
	ui.lineEdit->setText(QString::number(cnt));//反馈交点数
	return cnt;
}
  • c.图形绘制
    • 这一部分,我们重写了paintEvent()方法。点击“绘制几何图形和交点”的按钮时,调用update()函数,重新绘制。
void IntersectGUI::paintEvent(QPaintEvent*)
{
	init_canvas(); //初始化画布 (底色和坐标轴)
	if (figures.size() != 0) {
		for (vector::iterator iter = figures.begin(); iter != figures.end(); ++iter) {
			draw_figures_from_str(*iter);//绘制几何图形
		}
		draw_points();//绘制交点
	}
}

void IntersectGUI::on_drawFigureButton_clicked()
{
	update();
}

//将不同的string数据绘制成不同的几何图形
void IntersectGUI::draw_figures_from_str(string str)
{
	QStringList list = (QString::fromStdString(str)).split(" ");
	QString type = list.at(0);
	……
	if (type == QString::fromLocal8Bit("L")) {
		draw_line(x1, y1, x2, y2);
	}
	else if (type == QString::fromLocal8Bit("S")) {
		draw_seg(x1, y1, x2, y2);
	}
	else if (type == QString::fromLocal8Bit("R")) {
		draw_ray(x1, y1, x2, y2);
	}
	else {
		draw_circle(x1, y1, r);
	}
}

  • 由于Qt中的drawLine()方法,只能绘制两点间的线段。所以在实现绘制直线和射线的时候,我们计算了当前线与画布边界的交点。代码简单,但是很长,在这里就不展示了。
  1. 界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')

(1)接口数据格式介绍

  • 计算模块与界面模块的对接,用到了此接口:
    • msg存放的是当前几何图形的信息,数据格式与文件中读取的格式相同。
    • points存放求解的交点。
#ifdef IMPORT_DLL
#else
#define IMPORT_DLL extern "C" _declspec(dllimport)
#endif
IMPORT_DLL int guiProcess(std::vector> * points, std::string msg);
/* string in msg
4
C 3 3 3
S 2 4 3 2
L -1 4 5 2
R 2 5 -1 2
*/

(2)GUI导入dll的方式如下:

#pragma comment(lib,"calcInterface.lib")
_declspec(dllexport)  extern "C"  int guiProcess(std::vector> * points, std::string msg);

(3)具体代码实现

  • GUI相关的代码只在求解交点处调用了dll的guiProcess()方法。
int IntersectGUI::on_calcPButton_clicked()
{
	points.clear();
	std::string input;
	size_t n = figures.size();
    //转换数据
	input += std::to_string(n) + "\n";
	for (size_t i = 0; i < n; i++) {
		input += figures.at(i) + "\n";
	}
	
	int cnt = 0;
	try {
		cnt = guiProcess(&points, input);
	}
	catch (std::exception e) {
		...
	} {
	}
	ui.lineEdit->setText(QString::number(cnt));//界面反馈交点总数
	return cnt;
}
  • IntersectGUI类中的vector> points属性存放求解的交点。在调用guiProcess前,清空当前points中的元素,传入points的引用。将figures中的数据转换为接口对应类型的数据传入。guiProcess()会将求解的交点写入points中,并返回交点数。
  • guiProcess()代码如下:
int guiProcess(std::vector>* points, 
	std::string msg) {
	try {
		vector lVec;
		vector cVec;
        //将msg中的几何信息解析并存入lVec和cVec中
		istringstream input(msg);
		fileExcHandler(input, lVec, cVec);
        //计算交点
		set pointSet = getAllIntersect(lVec, cVec);
        //将交点信息写入points中
		for (auto iter = pointSet.begin(); iter != pointSet.end(); iter++) {
			Point p = (Point)* iter;
			points->push_back(make_pair(p.getX(), p.getY()));
		}
        //返回交点总数
		return (int)pointSet.size();
	} catch (fileException& fileError) {
		cout << fileError.what() << endl;
	}
	catch (lineException& lineError) {
		cout << lineError.what() << endl;
	}
	catch (CircleException& circleError) {
		cout << circleError.what() << endl;
	}
	return -1;
}

(4)对接相关功能实现

  • 未点击“求解交点”按钮时,绘制的几何图形。

软件工程作业——结对编程实践_第8张图片

  • 点击“求解交点”按钮后,再绘制

软件工程作业——结对编程实践_第9张图片

  • 绘制交点并返回交点数。

Step 5 松耦合测试

  • 和我们(A组)进行松耦合对接的是1602108817373439的结对编程(B)小组
  • 松耦合开发过程如下:
    • dll导出,即新建dll导出项目,在pch.h 文件中导入头文件,并在每一个*.cpp文件中include "pch.h文件,之后生成dll即可
    • 之后与GUI和cmd程序分别对接,其中有一个特别需要注意的问题,也是我们最初对接出现的问题,就是dll导出的编译器需要和导入文件的编译器相同,主要是和GUI对应的编译器相同。否则就会出现无法导入的情况,我们两个小组在都采用的VS IDE上的release x64版本的编译器之后,导出的dll可以互相调用。
  • 下面展示松耦合实际测试(测试展示中两个组的dll文件名不同,但之后同一命名为calcInterface上传到GitHubdev-combine分支中)

cmd松耦合

  • cmd松耦合测试主要代码
  typedef int (*pGui)(vector>* points, string a);
	typedef void (*pCmd)(int argc, char* argv[]);
  HMODULE hDLL = LoadLibrary(TEXT("calcInterface.dll"));
	vector>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);
			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);
	}
  • A组dll导入B组cmd程序

软件工程作业——结对编程实践_第10张图片

  • B组dll导入A组cmd程序

软件工程作业——结对编程实践_第11张图片

GUI的松耦合

  • GUI导入dll主要函数如下
#pragma comment(lib,"calcInterface.lib")
_declspec(dllexport)  extern "C"  int guiProcess(std::vector> * points, std::string msg);
  • A组dll导入B组GUI程序

软件工程作业——结对编程实践_第12张图片

  • B组dll导入A组GUI程序

软件工程作业——结对编程实践_第13张图片

结对编程

  1. 描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。(1')
  2. 看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。(5')

结对编程时间表

时间 事项
3.12晚 讨论结对编程总体规划
3.12-3.14 独立进行需求分析、学习和设计的工作,辅以资源交流
3.14下午 讨论代码设计,确定最终设计和实际结对编程的各种方法和规范
3.14晚-3.23上午 进行合作编程,完成所有功能及其测试
3.23-3.24 完成博客撰写

远程结对编程工具与经验

  • liveShare进行远程合作,其中左边栏为远程桌面通信栏,中间代码区域为我审核step1编写的测试样例以及队友核心代码编程,右边代码区域为队友正在编写step1代码核心功能
    • 一个注意事项:在一次远程liveShare结束之后,再开启新连接,需要关闭会话再次开启会话生成新的共享链接才能连上,不然会一直连接不上...(我们第一个晚上连接不上简直崩溃。)
  • GitHub上将结对队友设置为contributor,一起贡献整个项目
  • 腾讯会议,可以进行屏幕共享和实时聊天,方便代码复核
  • 微信电话、语音和聊天,一起共同制定每日计划、完成每日总结,一同攻克难关
  • 部分截图如下:

软件工程作业——结对编程实践_第14张图片

结对过程中的优缺点

  • 参考了关于结对编程的介绍博客之后,总结结对编程的优缺点如下:
  • 优点
    1. 一人编码一人审核,互相监督,提高了编程效率,减少了犯错几率
    2. 两人一起开发、设计、编写,可以互相学习取长补短
    3. 在面对困难时,两人一起开发,可以互相借鉴思路,加速问题的解决
  • 缺点
    1. 两人合作需要有相对久的磨合期,而且磨合之后能否达成良好的合作也未可知
    2. 对于一下相对基础的软件开发,结对编程的意义可能不能体现

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

  • 队友
    • 优点
      1. 学习能力强,负责这次GUI开发工作,在短时间里写出了一个精美的GUI界面,并完成了全部功能
      2. 富有巧思编码能力强,向量的计算引入、GUI的设计,都是队友的功劳
      3. 做事认真负责而且耐心,追求完美,测试的编写、readme和博客的写作,队友都做到尽善尽美
    • 缺点
      1. 做事情时常缺少规划,计划的事儿有时候延期完成
    • 优点
      1. 对项目能提出计划,并且能按照计划完成任务
      2. 擅于与人沟通,与GUI对接的API是我与另一位同学商议确定
      3. 乐于学习新知识新技术,乐于尝试liveShare、OpenCppCoverage、VS类图等工具,辅助结对开发
    • 缺点
      1. 编程能力略弱,对于C++和VS编程平台了解不够

源代码管理 - 警告完全消除

软件工程作业——结对编程实践_第15张图片

你可能感兴趣的:(软件工程作业——结对编程实践)