北航软件工程 2022 结对项目
项目 | 内容 |
---|---|
这个作业属于哪个课程 | 2022年北航敏捷软件工程社区-CSDN社区云 |
这个作业的要求在哪里 | 结对编程项目-最长英语单词链-CSDN社区 |
我在这个课程的目标是 | 见此 |
这个作业在哪个具体方面帮助我实现目标 | 掌握在团队内进行沟通和协作的方法,具备团队协作软件开发的实践经验,具有在团队协作中提升和改进个人软件开发技能和团队软件开发能力的能力 |
-
在文章开头给出教学班级和可克隆的 Github 项目地址。(1')
-
教学班级:周五班
-
解决方案地址:kevintsq/LongestWordChain: Pair Programming Homework for a Software Engineering Course (github.com)
-
总览:解决方案下包含 4 个项目,功能如下所示:
项目名 功能 Core
核心计算模块(以下均简称 Core
)CLI
解决方案的命令行接口 GUI
-Avalonia解决方案的图形用户界面接口 UnitTest
单元测试模块
-
-
【独立】在开始实现程序之前,在下述 PSP 表格记录下你估计将在程序的各个模块的开发上耗费的时间。(0.5')
PSP2.1 Personal Software Process Stages 实际耗时(分钟) Planning 计划 30 · Estimate · 估计这个任务需要多少时间 30 Development 开发 1440 · Analysis · 需求分析 (包括学习新技术) 30 · Design Spec · 生成设计文档 30 · Design Review · 设计复审 (和同事审核设计文档) 30 · Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 · Design · 具体设计 30 · Coding · 具体编码 930 · Code Review · 代码复审 60 · Test · 测试(自我测试,修改代码,提交修改) 300 Reporting 报告 180 · Test Report · 测试报告 150 · Size Measurement · 计算工作量 15 · Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 15 合计 1650 -
【独立】看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。(5')
-
Information Hiding:信息隐藏是将计算机程序中最有可能发生变化的设计决策隔离开来的原则,从而保护程序的其他部分在设计决策发生变化时不被广泛修改。这种保护包括提供一个稳定的接口,保护程序的其余部分不受实现(其细节可能会改变)的影响。
-
如:由于作业要求接口由动态链接库以 C 样式暴露,只能使用裸指针,不便于后续操作,因此设计
Launcher
类专门用于信息隐藏,将使用的裸指针转换为相应容器,传给Solver
类的方法。由于Launcher
类的存在,Solver
类无需关心和其它模块交互的接口是什么样的,只用实现内部逻辑即可。反之,调用者无需关心Solver
类是如何求解的,只需按要求准备参数即可。// [DllExport("gen_chain_word", CallingConvention = CallingConvention.Cdecl)] public static unsafe int gen_chain_word(char** words, int len, char** result, char head, char tail, bool enable_loop) { Solver solver = new Solver(); List<string> results; results = solver.SolveGenerateMostOrLongest(Utility.ConvertCharArrayToStringList(words, len), head, tail, enable_loop, true); if (results.Count > MAX_RESULT_AMOUNT) { return -1; } else { for (int i = 0; i < results.Count; i++) { result[i] = (char*)Marshal.StringToHGlobalUni(results[i]); } return results.Count; } }
-
-
Interface Design:接口用于模块之间的交互。精心设计的接口可以避免“抽象泄露”,提升编码和运行效率,给人良好的开发和使用体验。
-
如:课程组定义的接口,虽然有其弊端,但由于被设计为由动态链接库以 C 样式暴露,理论上扩展性较强,在 CLI 和 GUI 程序中可以复用。如在 CLI 程序中调用如下:
switch (type) { case OperationType.All: reNum = Core.Launcher.gen_chains_all(forWords, wordNum, forResults); break; case OperationType.Unique: reNum = Core.Launcher.gen_chain_word_unique(forWords, wordNum, forResults); break; case OperationType.Longest: reNum = Core.Launcher.gen_chain_char(forWords, wordNum, forResults, head, tail, loop); break; case OperationType.Most: default: reNum = Core.Launcher.gen_chain_word(forWords, wordNum, forResults, head, tail, loop); break; }
-
-
Loose Coupling:松耦合是一种连接各模块的方法,使这些模块在可行的最小范围内依赖。
- 如:课程组定义的接口,传的直接是以单词为单位的数组,而不是文件句柄、文件名或完整字符串等。由于单词的来源有很多,不一定来自文件,输出的方式也有很多,不一定是
stdout
或固定的文件,这种设计可以将模块间的依赖降到最低。
- 如:课程组定义的接口,传的直接是以单词为单位的数组,而不是文件句柄、文件名或完整字符串等。由于单词的来源有很多,不一定来自文件,输出的方式也有很多,不一定是
-
-
计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(7')
Core 模块包含如下五个类:
CircleDetected.cs
:由于前端进行了大部分异常处理(见下文),可以保证输入数据符合处理需求。本类定义了 Core 模块唯一可能涉及的异常:要求无环路而实际出现环路。Component.cs
:本类进行了数据抽象。题目需求本质上是图上的最长路问题,单词建模为节点,接龙关系建模为边,路径建模为边的集合,如是最终建立本题的图抽象结构。Launcher.cs
:本类根据题目需求第二阶段对接口封装的需求,定义并暴露了要求的数个接口,同时将输入的非托管内存中的内容转移到托管内存中,进而传递给Solver
,进行实际的计算和处理。Solver.cs
:本类是 Core 模块的核心,所有的计算任务逻辑在本类中编写。Utility.cs
:本类定义了所用到的辅助函数,包括上述从非托管内存中复制字符串的方法。
首先对图结构进行说明:
Graph
类除包含上述节点、边的抽象容器外,还额外定义了:StartNode
:抽象的图起始点,具有到每个单词节点的出边。EndNode
:抽象的图终点,具有从每个单词节点出发的入边。BuildGraphForAllWords(List<string>)
:接收输入单词,并按照上述节点和边抽象建立图的方法。
以下,对
Solver
类进行更详细的说明:Solver
类包含一个图类型成员graph
,对于传入的单词列表,在进行处理之前会首先进行建图。Solver
类包含如下方法:-
JudgeCircle()
是判定环路的算法,在建好的图上尝试进行拓扑排序,并同时判定回路。 -
FindPath()
是输出图中所有路径的算法,由于要求全部可能路径,本函数的算法逻辑是简单的搜索。 -
SolveGenerateAll()
包裹了完成输出所有路径需求的各步骤,首先完成图的建立,随后进行环路判定,对于存在环路的图给出提示,随后调用FindPath()
,搜索并获得全部可能路径。 -
FindPathUnique()
是输出图中首字母不相同的最长路径的算法,在搜索时记录已用的首字母进行剪枝,将搜索深度控制在26 层。 -
SolveGenerateUnique()
包裹了完成输出首字母不相同的最长路径需求的各步骤,与前文类似不再赘述。 -
FindPathMostOrLongest()
是求解最长路的通用算法,最多单词或最多字母本质上是一回事,唯一的不同在于对于边权值的定义不同:对于最多单词的情况,边权值等于 1,边与边无取舍上的偏向——只要能够前进到下一个节点即可;对于最多字母的情况,边权值等于下一单词的长度,对应最长路的优化目标为单词长度。 -
SolveGenerateMostOrLongest()
包裹了完成输出最长路需求的各步骤,与前文类似。然而,对于 DAG (无环路)和非 DAG 上的求解方案选择不同,分别调用前文的两个函数,以对 DAG 进行针对性优化。
-
【独立】阅读有关 UML 的内容,画出 UML 图显示计算模块部分各个实体之间的关系(画一个图即可)。(2’)
-
计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由 VS 的性能分析工具自动生成),并展示你程序中消耗最大的函数。(3')
通过对最长路径搜索、允许环路条件(主要性能测试场合)的性能分析,我们发现在搜索时复制并记录既有路径的暴力写法耗费了大量的时间和空间。
该方法耗费大量空间,并在内存换页等压力的作用下,导致了极大的时间浪费,在数据量较大的状况下尤为明显。
由于该需求中,只要求记录最长路径,并无需记录所有路径,我们改进写法以减少该开销及花费,进行修改后,在大数据量状况下获得了较好的结果。
-
【独立】看 Design by Contract,Code Contract 的内容,并描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。(5')
- Design by Contract:要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。
- 优点:
- 严格限定了程序的行为,无需程序员自己思考这些先验条件、后验条件和不变式,使编码更轻松。
- 先验条件、后验条件和不变式正好也是单元测试的条件,使单元测试更容易。
- 缺点:
- 要求写“契约”的人考虑周全,否则程序员编码可能产生疑问,或背错误的“契约”误导。
- 写“契约”可能花费大量时间,可能还不如直接编码效率高。
- 作业体现:
- 我们在编写相关函数前后对先验条件、后验条件和不变式等进行了讨论,如错误情况的返回值、非托管内存在何处分配或释放等问题。
- 优点:
- Code Contract:.NET 早期版本(5+ 版本不再支持)引入的类似断言检查(Assertions)的语法,在语言层面支持“契约式设计”的功能。
- 优点:
- 包含“契约式设计”的所有优点,同时在语言层面的支持使其成为代码的一部分,让程序更稳定。
- 缺点:
- 包含“契约式设计”的大部分缺点,同时如果编译器在 Release 版本不对其优化,可能降低代码运行效率。
- 作业体现:
- 由于我们代码运行在 .NET 6 Runtime 上,无法使用该功能,仅使用“契约式设计”。
- 优点:
- Design by Contract:要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。
-
计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到 90% 以上,否则单元测试部分视作无效。(6')
主要测试 CallCoreByOptions
函数,测试数据的构造以尽量多地覆盖代码为目的。
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
namespace CLI.Tests
{
[TestClass()]
public class CallWrapperTests
{
[TestMethod()]
public void CallCoreByOptionsTest()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\1.txt",
OperationType.All, '\0', '\0', false);
var results = new HashSet<string>();
foreach (string line in toWrite)
{
results.Add(line.Trim());
}
var real = new HashSet<string>()
{
"6",
"woo oom",
"moon noox",
"oom moon",
"woo oom moon",
"oom moon noox",
"woo oom moon noox"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest2()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\2.txt",
OperationType.Most, '\0', '\0', false);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"algebra",
"apple",
"elephant",
"trick"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest3()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\3.txt",
OperationType.Unique, '\0', '\0', false);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"apple",
"elephant",
"trick"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest4()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\4.txt",
OperationType.Longest, '\0', '\0', false);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"pseudopseudohypoparathyroidism",
"moon"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest5()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\5.txt",
OperationType.Most, 'e', '\0', false);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"elephant",
"trick"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest6()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\6.txt",
OperationType.Most, '\0', 't', false);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"algebra",
"apple",
"elephant"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest7()
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\7.txt",
OperationType.Most, '\0', '\0', true);
var results = new HashSet<string>();
for (int i = 1; i < toWrite.Count; i++)
{
results.Add(toWrite[i].Trim());
}
var real = new HashSet<string>()
{
"table",
"element",
"teach",
"heaven"
};
Assert.IsTrue(real.SetEquals(results));
}
[TestMethod()]
public void CallCoreByOptionsTest8()
{
try
{
List<string> toWrite =
CallWrapper.CallCoreByOptions(
@"..\..\..\..\TestCases\7.txt",
OperationType.Most, '\0', '\0', false);
}
catch (Exception ex)
{
Assert.IsTrue(ex.GetType() == typeof(Core.CircleDetected));
}
}
}
}
-
计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(5')
Core
模块仅预期产生以下三种异常:- 文本内含有单词环
- 内存不足
- 返回单词个数超过作业规定的上限
由于文本内含有单词环的情况由
Solver
在多处发现,定义异常类CircleDetected
,在发现时抛出,由调用者负责接收。样例如下:Element Heaven Table Teach Talk
对于内存不足,由于在 C# 中所有为引用类型的变量都可能发生,因此仅在最外层接收异常。触发该异常要求以 32 位方式编译,并使用较长的单词表。样例由 CJQ 提供,请见这里,太长不便贴出。若使用 64 为方式编译并开启交换文件,基本不会触发该异常。
对于超限,由于接口要求由动态链接库以 C 样式暴露,而标准 C 不包含异常功能,因此不定义异常类,而是将接口返回值设为
-1
表示异常。由于
CLI
模块调用该接口判定返回值只有一处,为便捷起见不建异常类,而是直接抛含消息的标准异常。CLI
模块可能遇到的其它异常如下所示。由于使用Mono.Options
模块初步解析命令行,并在main
函数内进一步判断,简便起见不再手动定义异常类。异常 触发样例(程序简写为 cli
)无参数 ./cli
无选项 ./cli test.txt
无文件名 ./cli -n
多个文件 ./cli -n test.txt input.txt
错误参数 ./cli -e test.txt
只给了 -h
、-t
或-r
但没给其它选项./cli -h a test.txt
( -n
或-m
) 与 (-h
、-t
或-r
) 复合使用./cli -n -r test.txt
-n
、-m
、-w
、-c
复合使用./cli -n -m test.txt
-h
或-t
的参数不是字母,或者数量超过一个./cli -n -h 123 test.txt
错误文件后缀 ./cli -n test
文件不存在 ./cli -n hahaha.txt
非法文件 ./cli -n foo.txt
(如foo.txt
是个目录)文件名非法 ./cli -n :.txt
没有写入 solution.txt
的权限./cli -n test.txt
(如当前在 C 盘根目录下,未以管理员身份运行)注:
- 我们允许重复选项,如
-n -n
,甚至-h a -h b
。这种情况下字母是 a 还是 b 取决于Mono.Options
的实现,用户应主动避免。 - 我们允许在所有参数后出现任意数量的
--
,这样其后的文件名可以以选项开头。这是由Mono.Options
库决定的,该库的处理符合一般惯例。 - 对于不以
-
开头的文件,我们不要求扩展名必须是.txt
。对于二进制文件,ASCII 码不在字母范围内的字节均被视为分隔符。
-
界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。(5')
本项目的界面模块选择基于 MVVM 设计模式的 AvaloniaUI 框架开发,结果如下:
以下做分类介绍:
Utilities
:包含了与 CLI 模块相同的,用于进行预解析和 Core 模块调用的相关类。ViewModels
:定义了 GUI 的视图模型,从而对视图构件和用户输入数据、操作逻辑等进行解耦。Views
:按照框架开发文档,采用类似 xml 文件的形式,进行了 GUI 模块的视图构件定义和组织结构设计。
以任意一个构件为例简单解说视图的开发流程:
<ComboBox SelectedIndex="{Binding GenerateType}" Margin="0,5"> <ComboBoxItem>全部单词链</ComboBoxItem> <ComboBoxItem>首字母不重复的单词链</ComboBoxItem> <ComboBoxItem>字母数最多的单词链</ComboBoxItem> <ComboBoxItem>单词数最多的单词链</ComboBoxItem> </ComboBox>
以上定义了生成模式的输入下拉框,并将其绑定到视图模型中的 GenerateType 属性上。
public int generateType = 0; public int GenerateType { get => generateType; set => this.RaiseAndSetIfChanged(ref generateType, value);}
以上在视图模型中进行了视图和数据的双向绑定。
public void Generate() { OperationType type = OperationType.All; //... switch (generateType) { case 0: type = OperationType.All; break; case 1: type = OperationType.Unique; break; case 2: type = OperationType.Longest; break; case 3: type = OperationType.Most; break; default: type = OperationType.All; break; } //... List<string> toWrite = CallWrapper.CallCoreByOptions(inputFile, type, head, tail, canLoop); //... }
以上定义了用户点击按钮后进行单词链计算并输出的方法。该方法按照绑定的
GenerateType
的值,选择生成模式,并传递给 Core 模块进行生成。 -
界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。(4')
对于 CLI 模块:
Program.cs
是主程序入口,为输入输出分配非托管内存,调用 Mono.Options 库进行输入选项的解析,并一次调用Parser
类进行输入文本的预解析、通过CallWrapper
调用 Core 模块中的相关方法,最后进行输出。Parser.cs
是解析器类,该类按照输入的文件名,解析输入文件并获得单词列表。CallWrapper.cs
类根据传入的选项,按照符合 Core 模块接口定义的方式调用其中的相关方法。
对于 GUI 模块:
GUI 模块以类似上述 CLI 模块的方式完成了解析和调用。对于用户输入的处理,如上节所述,GUI 模块将用户的各项选择和输入信息进行抽象和规范化,以下拉菜单、勾选框和输入框的形式展现,并通过视图模型与格式化的输入数据进行绑定,最后按照用户的需求调用
Core
模块完成计算。上节已对其实现方法进行了充分的介绍,不再一个个赘述。 -
描述结对的过程,提供两人在讨论的结对图像资料(比如 Live Share 的截图)。关于如何远程进行结对参见作业最后的注意事项。(1')
-
【独立】看教科书和其它参考书,网站中关于结对编程的章节,例如:http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html ,说明结对编程的优点和缺点。同时描述结对的每一个人的优点和缺点在哪里(要列出至少三个优点和一个缺点)。(5')
- 结对编程
- 优点:
- 技术上,结对编程融合了两个人的技术水平和开发经验,能够提高项目水平;
- 从开发人员的角度上来看,结对编程帮助结对的两位程序员互相学习互相促进;
- 开发以外,结对编程提供了更积极的心理体验,使程序员可以更加集中于问题解决,产出更高效的解决方案;
- 最后,结对编程意味着一套结合高效沟通、敏捷修改的开发模式,拓宽了编程与开发的内涵和边界。
- 缺点:
- 结对编程使两人同时开发,“原教旨主义的”结对编程,客观上缺失了两人合作分析需求、分别开发的协作方法论,可能降低总体效率。
- 结对编程带来更高的工作专注度的同时,也带来了更高的精神压力。
- 优点:
- 熊安杰(合作者)
- 优点:
- 具有较好的学习能力
- 层次化、模块化理念清晰,代码风格好
- 能够快速检索新方法新方案并应用
- 缺点:
- 对目标平台及开发语言缺乏了解
- 优点:
- 谭思齐(本人)
- 优点:
- 细心、考虑周到,能够覆盖各类错综复杂的开发需求
- 技术功底好,对目标平台和程序语言都有扎实的基础
- 审美水平高、代码风格好
- 缺点:
- 开发时讲求完美,最终完成得比较极限
- 优点:
- 结对编程
-
【独立】在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。(0.5')
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 120 |
· Estimate | · 估计这个任务需要多少时间 | 120 |
Development | 开发 | 2040 |
· Analysis | · 需求分析 (包括学习新技术) | 240 |
· Design Spec | · 生成设计文档 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 15 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 15 |
· Design | · 具体设计 | 30 |
· Coding | · 具体编码 | 1440 |
· Code Review | · 代码复审 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 |
Reporting | 报告 | 300 |
· Test Report | · 测试报告 | 240 |
· Size Measurement | · 计算工作量 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 |
合计 | 2460 |