软件工程实践第二次作业——个人实战

软件工程实践第二次作业——个人实战

这个作业属于哪个课程软件工程实践-2023学年-W班社区-CSDN社区云
这个作业要求在哪里软件工程实践第二次作业——个人实战-CSDN社区
这个作业的目标本次作业将聚焦于世界游泳锦标赛跳水赛事项目
其他参考文献

一、gitcode 项目

Wishrem / Project-java · GitCode

二、PSP 表格

以下时间并不够准确,因为并没有严格按照这份表格的内容进行。

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划108
• Estimate• 估计这个任务需要多少时间108
Development开发480488
• Analysis• 需求分析 (包括学习新技术)12092
• Design Spec• 生成设计文档3048
• Design Review• 设计复审2023
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)3027
• Design• 具体设计3022
• Coding• 具体编码180114
• Code Review• 代码复审3020
• Test• 测试(自我测试,修改代码,提交修改)20142
Reporting报告120120
• Test Repor• 测试报告1018
• Size Measurement• 计算工作量1012
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划10096
合计610616

三、解题思路描述

(一)功能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. 接口设计

该接口可以拆分出如下功能:读取文件返回结构化信息、汇总信息按需求输出

可以拆出两个类:PlayerPlayerList

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、汇总信息按需求输出

可以拆出两个类:PerformanceCompetition

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. 实现过程

在后面的性能改进之后,此部分有较多的改动

  1. Competition 重新命名为了 CompetitionList 并且输入改为了 (String dataDir)
  2. saveCompetition 变更成 save(String competitionName, String filename, boolean detail, boolean append)
  3. getCompetitionFilenames 被移除。
  4. 增加了 getOrderList 用于获取第一次比赛的排名。
  5. 增加了 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%
未优化之前的load函数时间占比

这部分是由于反复读取同一个 women_3m_springboard.json 文件导致的,其中开销最大的是 IO 流。

其中 gitcode.competition.Competition.contains 也是反复读取 list.txt,两者可以一起改进。

2. gitcode.competition.Competition.saveDetail 15.8% & gitcode.competition.Competition.saveFinalCompetition 15.2 %
save方法下调用的方法的时间占比

这两个函数都是由于 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

改进后的性能:
改进后load方法的性能

2. gitcode.competition.Competition.save

由于是频繁调用,所以将该部分的格式化字符串在载入对应比赛项目时提前格式化后缓存。

具体的 diff 可以查看这个链接:pref: optimized the performance of saving competition (9d3ad3a6) · 提交 · Wishrem / Project-java · GitCode

改进后的性能:
改进后的性能

六、单元测试展示

(一)工具说明

  1. Apache Maven:用于注入 JaCoCo 插件和自动化测试。
  2. JaCoCo:用于输出可视化的代码测试覆盖率。
  3. JUnit 5:用于编写单元测试。
  4. Coverage Gutters(VSCode 插件):用于将 JaCoCo 输出的文件直接在 VSCode 编辑器中直接展示覆盖率的情况。

(二)构造测试思路

理论上最好的做法是给每一个方法都定制一个测试集,然而此次作业时间有限,部分输入固定的方法并没有单独的测试集而是在一个集成的方法中一起测试。

思路:

  1. 先进行黑盒测试:根据输入的域 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());
}

名字、成绩和排名不同应该认为是不同的表现。

  1. 后进行白盒测试:查看方法内使用的调用的其他方法,针对设计,如:
@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 可以查看这两个链接:

  1. style: improve code coverage rate (f340cad0) · 提交 · Wishrem / Project-java · GitCode

  2. test: improved test coverage for util and player (c318a3e6) · 提交 · Wishrem / Project-java · GitCode

六、心路历程与收获

(一)流程改进

对于 PSP 表格的填写顺序和内容,个人有其他顺序(只是通过这次项目实践所得出的,后续会继续更新迭代出一套自己的开发流程):

  1. 需求分析:将需求内容拆分成若干需要实现的功能,遇到不确定的实现方法应当询问客户,并将客户的要求补充道需求中,重复该步骤直至没有异议,并生成需求文档。
  2. 统一愿景:与成员一起明确该项目的期望和目标,有助于在后续的取舍中做出符合愿景的选择。
  3. 计划安排(若事先不知道具体要实现哪些需求,就不可能提前安排出时间
  4. 技术选型:更具需求进行调研和实验,选择出符合要求的技术。
  5. 具体设计:组建出整一个架构,具体到每一函数的定义和代码规范,写出符合函数定义的测试案例,产出技术文档。
  6. 设计复审
  7. 代码实现:这部分包括了白盒测试。
  8. 代码复审
  9. 性能优化:该部分是可选的,没有超出预期就可以不用进行优化。
  10. 项目部署
  11. 工作总结

N.B. 里程碑式的流程推进,除非有特别重要的要求,否则不允许返回到上一阶段。该流程遵循敏捷开发,所以在需求分析阶段就可以将功能直接的依赖关系拆分成多个可迭代的版本,这样也有助于客户早些看到效果,对不满意的点还可以直接修改,同时也有可能直接到达客户的期待提前结束。

(二)对于语言的看法

  1. 好的语言是只适用于一部分人群,并不是具有极强的泛用性。Python 语法简单易于快速实践并非具有极强的泛用性,其便于调用其他由 C++ 或 Fortran 写好的高性能库,可以做快速的开发和实验验证,所以该语言也在数据科学领域特别流行。
  2. 为什要特别专精语言?虽然优秀的人可以做到需要什么语言就学习什么语言,但是大多公司并不会接纳这样的员工,因为学习新语言并达到充分运用语言特性(这里不是指一些徒有其表的炫技)所需的时间成本太大。对于一个项目的开发,最大的开支在于人员的培训和雇佣,所以市场会更青睐专精语言的人才。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值