软件工程实践第二次作业——个人实战
这个作业属于哪个课程 | 软件工程实践-2023学年-W班社区-CSDN社区云 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战-CSDN社区 |
这个作业的目标 | 本次作业将聚焦于世界游泳锦标赛跳水赛事项目 |
其他参考文献 | 无 |
一、gitcode 项目
Wishrem / Project-java · GitCode
二、PSP 表格
以下时间并不够准确,因为并没有严格按照这份表格的内容进行。
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 8 |
• Estimate | • 估计这个任务需要多少时间 | 10 | 8 |
Development | 开发 | 480 | 488 |
• Analysis | • 需求分析 (包括学习新技术) | 120 | 92 |
• Design Spec | • 生成设计文档 | 30 | 48 |
• Design Review | • 设计复审 | 20 | 23 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 27 |
• Design | • 具体设计 | 30 | 22 |
• Coding | • 具体编码 | 180 | 114 |
• Code Review | • 代码复审 | 30 | 20 |
• Test | • 测试(自我测试,修改代码,提交修改) | 20 | 142 |
Reporting | 报告 | 120 | 120 |
• Test Repor | • 测试报告 | 10 | 18 |
• Size Measurement | • 计算工作量 | 10 | 12 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 100 | 96 |
合计 | 610 | 616 |
三、解题思路描述
(一)功能1:输出所有选手信息
这个比较简单,只需要将之前处理好的数据载入,然后按国籍为首要关键字升序、选手的名(Last Name)为次要关键字升序排序输出即可,并按如下输出格式输出
Full Name:HART Alexander
Gender:Male
Country:Austria
-----
Full Name:LOTFI Dariush
Gender:Male
Country:Austria
-----
...
-----
Full Name:DICK Elaena
Gender:Female
Country:Canada
-----
(二)功能2:输出决赛每个运动项目结果
基础功能
这个也比较简单,将之前处理好的数据载入,然后按格式输出决赛信息即可
Full Name:MULLER Jette
Rank:1
Score:51.60 + 52.00 + 51.75 + 46.80 + 46.80 = 248.95
-----
Full Name:ROLLINSON Amy
Rank:2
Score:46.00 + 42.90 + 50.70 + 54.00 + 46.80 = 240.40
-----
...
-----
Full Name:SANTIAGO Dominique
Rank:12
Score:42.00 + 18.20 + 35.70 + 34.50 + 32.55 = 162.95
-----
附加功能
可以将刚刚基础功能的 获取 Rank 和 获取分数输出样式 复用,然后按照相应的格式输出
Full Name:FUNG Katelyn
Rank:1 | 4 | 2
Preliminary Score:60.20 + 56.00 + 57.60 + 54.00 + 70.40 = 298.20
Semifinal Score:46.20 + 30.80 + 68.80 + 61.50 + 67.20 = 274.50
Final Score:58.80 + 54.60 + 57.60 + 54.00 + 72.00 = 297.00
-----
...
-----
Full Name:SANTIAGO Dominique
Rank:14 | 11 | 11
Preliminary Score:39.20 + 21.00 + 41.85 + 31.20 + 13.05 = 146.30
Semifinal Score:42.00 + 22.50 + 17.55 + 42.90 + 39.15 = 164.10
Final Score:42.00 + 28.50 + 20.25 + 41.60 + 44.95 = 177.30
-----
(三)执行流程
四、接口设计和实现过程
(一)输出所有选手信息
1. 接口设计
该接口可以拆分出如下功能:读取文件返回结构化信息、汇总信息按需求输出
可以拆出两个类:Player 和 PlayerList
Player 是一个记录数据的类,其中记录了选手的 fullName、gender 和 country 信息
PlayerList 是一个管理 Player 的类,它向外暴露 输出所有选手信息 的接口
PlayerList:
1)汇总信息按需求输出
public void savePlayers(String filename, boolean append);
filename: 保存文件的文件地址
append:是否追加保存
2) 读取文件返回结构化信息
private ArrayList<Player> getPlayers();
2. 实现过程
在实现过程中,我剔除了 getPlayers
这个私有方法,因为该方法只供该类内部使用,所以可以在初始化就完成,不必增加。
同时在入读 players 时完成了排序的预处理,以下是 Comparator
类的实现:
private static class PlayerComparator implements Comparator<Player> {
@Override
public int compare(Player o1, Player o2) {
// compare country name
int result = o1.country.compareTo(o2.country);
if (result != 0) {
return result;
}
// compare player last name
return o1.lastName.compareTo(o2.lastName);
}
}
如此处理后, savePlayers
的实现只要按照格式输出即可。
(二)输出某个比赛结果
该接口可以拆出如下功能:读取某一场赛事的文件并返回结构化信息、根据提供的比赛信息返回文件名或 Null、汇总信息按需求输出
可以拆出两个类:Performance 和 Competition
Performance 是一个记录数据的类,其中记录了每个选手的表现数据,包括 playerFullName、scores(按 dive 顺序)、rank 的信息
Competition 是一个记录数据和管理 Performance 的类,其中记录了比赛的 performances 和 type 的信息
1. 接口设计
1)读取某一场赛事的文件并返回结构化信息
public Competition(String competitionName);
2)根据提供的比赛信息返回文件名或Null
private ArrayList<String> getCompetitionFilenames(String competitionName);
3)汇总信息按需求输出
public void saveCompetition(String filename, boolean detail, boolean append)
2. 实现过程
在后面的性能改进之后,此部分有较多的改动
- 将
Competition
重新命名为了CompetitionList
并且输入改为了(String dataDir)
。 saveCompetition
变更成save(String competitionName, String filename, boolean detail, boolean append)
。getCompetitionFilenames
被移除。- 增加了
getOrderList
用于获取第一次比赛的排名。 - 增加了
getPerformances
用于获取某一个项目的所有选手的表现。
public void save(String competitionName, String filename, boolean detail, boolean append) {
Map<CompetitionType, List<Performance>> performances = getPerformances(competitionName);
try (FileWriter writer = new FileWriter(filename, append)) {
// 没有获取到则输出 N/A
if (performances == null) {
writer.write("N/A\n");
writer.write("-----\n");
return;
}
// 如果需要 detail
if (detail) {
saveDetial(performances, writer);
} else {
saveFinalCompetition(performances, writer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
saveFinalCompetition
的内容较为简单不进行说明,以下是 saveDetail
的代码:
private void saveDetial(Map<CompetitionType, List<Performance>> performances, FileWriter writer)
throws IOException {
List<String> orderList = getOrderList(performances); // 获取第一次比赛时候的排名顺序
StringBuilder rankBuilder = new StringBuilder("Rank:");
String rankDelimiter = " | ";
StringBuilder scoreBuilder = new StringBuilder();
Performance performance = null;
for (String playerFullName : orderList) {
writer.write("Full Name:" + playerFullName + "\n"); // 首先打印名字
for (CompetitionType type : orderedTypes) { // orderedTypes = {Preliminary, SemiFinal, Final};
performance = getPlayerPerformance(performances, type, playerFullName);
// 因为有的比赛项目只有 final,或者只有 final 和 preliminary,没获取到则跳过
if (performance != null) {
rankBuilder.append(performance.rank).append(rankDelimiter);
scoreBuilder.append(type.name + " Score:").append(performance.scoresDetail).append("\n");
}
}
// 此处可以用 setLength 优化,不用重新映射。
String rankStr = rankBuilder.toString().substring(0, rankBuilder.length() - rankDelimiter.length());
String scoreStr = scoreBuilder.toString();
writer.write(rankStr + "\n");
writer.write(scoreStr);
writer.write("-----\n");
rankBuilder = new StringBuilder("Rank:");
scoreBuilder = new StringBuilder();
}
}
五、程序性能改进
这里使用 JProfiler 进行程序性能分析,测试输入文件为 profile-input.txt
,其内容如下
players
result women 3m springboard
result women 3m springboard detail
注:以上为精简内容,在实际文件中,每一行都出现了 20 次,为了减小波动带来的误差。
测试命令为
java -jar DWASearch.jar profile-input.txt output.txt -p
(一)性能分析
未优化之前的各部分函数用时占比:
1. gitcode.competition.Competition.load 29.7%
这部分是由于反复读取同一个 women_3m_springboard.json
文件导致的,其中开销最大的是 IO 流。
其中 gitcode.competition.Competition.contains
也是反复读取 list.txt
,两者可以一起改进。
2. gitcode.competition.Competition.saveDetail 15.8% & gitcode.competition.Competition.saveFinalCompetition 15.2 %
这两个函数都是由于 JAVA 标准库的 String.format
方法而导致缓慢,以下是 formatScores
方法的代码
private String formatScores(float[] scores) {
StringBuilder sb = new StringBuilder();
float sum = 0;
for (float score : scores) {
sb.append(String.format("%.2f", score)).append(" + ");
sum += score;
}
sb.delete(sb.length() - 3, sb.length());
sb.append(" = ").append(String.format("%.2f", sum));
return sb.toString();
}
(二)代码改进
1. gitcode.competition.Competition.load
这部分做了较大的改进,将结构改了一番,思路是这样的:原先的每一个项目的所有比赛的操作都需要将对应文件读入,这里就可以将读入的文件缓存起来,将 Competition
类变更为一个管理类 CompetitionList
,再通过 getPerformances
的方法接管每一个项目的所有比赛的获取。
具体的 diff 可以查看这个链接: pref: optimized the initialization of competition (1a740529) · 提交 · Wishrem / Project-java · GitCode
改进后的性能:
2. gitcode.competition.Competition.save
由于是频繁调用,所以将该部分的格式化字符串在载入对应比赛项目时提前格式化后缓存。
具体的 diff 可以查看这个链接:pref: optimized the performance of saving competition (9d3ad3a6) · 提交 · Wishrem / Project-java · GitCode
改进后的性能:
六、单元测试展示
(一)工具说明
- Apache Maven:用于注入 JaCoCo 插件和自动化测试。
- JaCoCo:用于输出可视化的代码测试覆盖率。
- JUnit 5:用于编写单元测试。
- Coverage Gutters(VSCode 插件):用于将 JaCoCo 输出的文件直接在 VSCode 编辑器中直接展示覆盖率的情况。
(二)构造测试思路
理论上最好的做法是给每一个方法都定制一个测试集,然而此次作业时间有限,部分输入固定的方法并没有单独的测试集而是在一个集成的方法中一起测试。
思路:
- 先进行黑盒测试:根据输入的域 D i n D_{in} Din ,将其拆分为若干个互斥域 A A A,在域 A A A 选一个元素作为该域的代表输入,并给定一个期望输出,如:
@Test
public void testEquals() throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
Performance performance = peConstructor.newInstance("Dariush LOTFI & Tazman ABRAMOWICZ",
new float[] { 10.1f, 20.30f, 30.3f }, 1);
Performance performance2 = peConstructor.newInstance("Dariush LOTFI & Tazman ABRAMOWICZ",
new float[] { 10.1f, 20.30f, 30.3f }, 1);
assertEquals(performance, performance2);
performance2 = peConstructor.newInstance("Dariush LOTFI & Tazman ABRAMOWICZ",
new float[] { 10.1f, 20.30f, 30.3f }, 2);
assertNotEquals(performance, performance2);
performance2 = peConstructor.newInstance("Tazman ABRAMOWICZ",
new float[] { 10.1f, 20.30f, 30.3f }, 1);
assertNotEquals(performance, performance2);
performance2 = peConstructor.newInstance("Dariush LOTFI & Tazman ABRAMOWICZ",
new float[] { 10.1f, 20.30f, 30.3f, 40.4f }, 1);
assertNotEquals(performance, performance2);
assertNotEquals(performance, new Object());
}
名字、成绩和排名不同应该认为是不同的表现。
- 后进行白盒测试:查看方法内使用的调用的其他方法,针对设计,如:
@Test
public void testInvalidInitialization()
throws InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException {
assertThrows(IllegalArgumentException.class,
() -> throwInernalException(() -> peConstructor.newInstance("&", new float[] {}, 0)));
assertThrows(IllegalArgumentException.class,
() -> throwInernalException(() -> peConstructor.newInstance(" & ", new float[] {}, 0)));
}
根据内部会抛出的错误进行白盒测试。
(三)覆盖率提升
覆盖率低的原因是因为先进行黑盒测试时,并不清楚内部一些 if 等条件语句的分支,还有 IO 流的异常抛出难以实现。
改进之后:
具体的 diff 可以查看这两个链接:
-
style: improve code coverage rate (f340cad0) · 提交 · Wishrem / Project-java · GitCode
-
test: improved test coverage for util and player (c318a3e6) · 提交 · Wishrem / Project-java · GitCode
六、心路历程与收获
(一)流程改进
对于 PSP 表格的填写顺序和内容,个人有其他顺序(只是通过这次项目实践所得出的,后续会继续更新迭代出一套自己的开发流程):
- 需求分析:将需求内容拆分成若干需要实现的功能,遇到不确定的实现方法应当询问客户,并将客户的要求补充道需求中,重复该步骤直至没有异议,并生成需求文档。
- 统一愿景:与成员一起明确该项目的期望和目标,有助于在后续的取舍中做出符合愿景的选择。
- 计划安排(若事先不知道具体要实现哪些需求,就不可能提前安排出时间)
- 技术选型:更具需求进行调研和实验,选择出符合要求的技术。
- 具体设计:组建出整一个架构,具体到每一函数的定义和代码规范,写出符合函数定义的测试案例,产出技术文档。
- 设计复审
- 代码实现:这部分包括了白盒测试。
- 代码复审
- 性能优化:该部分是可选的,没有超出预期就可以不用进行优化。
- 项目部署
- 工作总结
N.B. 里程碑式的流程推进,除非有特别重要的要求,否则不允许返回到上一阶段。该流程遵循敏捷开发,所以在需求分析阶段就可以将功能直接的依赖关系拆分成多个可迭代的版本,这样也有助于客户早些看到效果,对不满意的点还可以直接修改,同时也有可能直接到达客户的期待提前结束。
(二)对于语言的看法
- 好的语言是只适用于一部分人群,并不是具有极强的泛用性。Python 语法简单易于快速实践并非具有极强的泛用性,其便于调用其他由 C++ 或 Fortran 写好的高性能库,可以做快速的开发和实验验证,所以该语言也在数据科学领域特别流行。
- 为什要特别专精语言?虽然优秀的人可以做到需要什么语言就学习什么语言,但是大多公司并不会接纳这样的员工,因为学习新语言并达到充分运用语言特性(这里不是指一些徒有其表的炫技)所需的时间成本太大。对于一个项目的开发,最大的开支在于人员的培训和雇佣,所以市场会更青睐专精语言的人才。