这个作业属于哪个课程 | 2302软件工程社区 |
---|---|
这个作业要求在哪里 | 软件工程第二次作业 |
这个作业的目标 | 完成世界游泳锦标赛官网的跳水项目数据搜集,并实现一个数据统计的 Console 程序 |
其他参考文献 | 无 |
Gitcode项目地址
https://gitcode.net/ozline/project-c
PSP 表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
• Estimate | 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 600 | 410 |
• Analysis | • 需求分析(包括学习新技术) | 120 | 50 |
• Design Spec | • 生成设计文档 | 20 | 20 |
• Design Review | • 设计复审 | 60 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
• Design | • 具体设计 | 120 | 20 |
• Coding | • 具体编码 | 360 | 120 |
• Code Review | • 代码复审 | 120 | 60 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 100 |
Reporting | 报告 | 210 | 100 |
• Test Repor | • 测试报告 | 80 | 40 |
• Size Measurement | • 计算工作量 | 60 | 40 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 60 | 20 |
合计 | 820 | 520 |
解题思路描述
问题1
对于问题一,我们需要实现参赛选手的信息读取,我们按以下顺序分析
- 分析
athletes.json
文件,找到我们需要提取的内容 - 构建项目基建
- 在项目基建的基础上,利用
nlohmann/json
库实现 json 解析 - 将输出文件输出到指定位置
问题2
问题二在问题一的基础上更加复杂了一些,它需要我们做如下内容
- 根据比赛文本,在
event.json
中找到指定的比赛 id - 根据比赛 id,在
results
文件夹中找到指定的比赛具体信息 - 分析具体的信息,找到我们需要的 key
- 编写代码,整合信息并类似问题 1 的解决过程解决问题 2
接口设计和实现的过程
设计如下接口
#ifndef UTILITIES_H
#define UTILITIES_H
#include <vector>
#include "participant.h"
#include "result.h"
#include "nlohmann/json.hpp"
#include <cstring>
// 验证比赛名称合法性
bool checkNameVaility(const std::string& name);
// 输出 N/A 到文件中
void writeNAToFile(const std::string& filename);
// 输出 Error 到文件中
void writeErrorToFile(const std::string& filename);
// 读入参赛选手到向量中
void readParticipantsFromFile(const std::string& filename, std::vector<Participant>& participants);
// 从向量中输出参赛选手
void writeParticipantsToFile(const std::string& filename, const std::vector<Participant>& participants);
// 找到比赛 id
std::string findResultsId(const std::string& filename, const std::string& gender, const std::string& disciplineName);
// 读入比赛具体结果到向量中
void readEventResultFromFile(const std::string& filename, std::vector<EventResult>& eventResult);
// 输出比赛结果到向量中
void writeEventResultsToFile(const std::string& filename, const std::vector<EventResult>& eventResult);
#endif // UTILITIES_H
在这之后,我们单独设计participant
和eventResult
两个类,这样封装起来具体的内容
#ifndef PARTICIPANT_H
#define PARTICIPANT_H
#include <string>
struct Participant {
std::string fullName;
std::string gender;
std::string country;
};
std::string genderToString(int genderCode);
bool compareParticipants(const Participant& a, const Participant& b);
#endif // PARTICIPANT_H
#ifndef RESULT_H
#define RESULT_H
#include <string>
struct EventResult {
std::string fullName;
std::string rank;
std::string score;
};
#endif // RESULT_H
关键代码展示
- 校验比赛合法性
bool checkNameVaility(const std::string& name) {
std::string list[10] = { "women 1m springboard",
"women 3m springboard",
"women 10m platform",
"women 3m synchronised",
"women 10m synchronised",
"men 1m springboard",
"men 3m springboard",
"men 10m platform",
"men 3m synchronised",
"men 10m synchronised"
};
for (const auto& str : list) {
if (toLower(name) == toLower(str)) {
return true;
}
}
return false;
}
- 查找比赛 id、根据 id 查找比赛详情、输出比赛详情
std::string findResultsId(const std::string& filename, const std::string& gender, const std::string& disciplineName) {
std::ifstream input_file(filename);
if (!input_file.is_open()) {
ErrorHandle::print("无法打开文件: " + filename);
}
json events;
input_file >> events;
std::string res = "";
for (const auto& sport : events["Sports"]) {
for (const auto& discipline : sport["DisciplineList"]) {
std::string jsonGender = toLower(discipline["Gender"].get<std::string>());
std::string jsonDisciplineName = toLower(discipline["DisciplineName"].get<std::string>());
if (jsonGender == gender && jsonDisciplineName == disciplineName) {
res = discipline["Id"].get<std::string>();
break;
}
}
if (res != "") {
break;
}
}
input_file.close();
return res;
}
void readEventResultFromFile(const std::string& filename, std::vector<EventResult>& eventResult) {
std::ifstream input_file(filename);
if (!input_file.is_open()) {
ErrorHandle::print("无法打开文件: " + filename);
}
json data;
input_file >> data;
auto& heats = data["Heats"];
auto& results = heats[0];
for (const auto& result : results["Results"]) {
std::string scores = "";
std::string totalPoints = "";
// 获取分数(相加形式)
for (const auto& dive : result["Dives"]) {
if (scores != "") scores += "+ ";
scores += dive["DivePoints"].get<std::string>() + " ";
totalPoints = dive["TotalPoints"].get<std::string>();
}
scores += "= " + totalPoints;
// 推入结果
eventResult.push_back({
result["FullName"].get<std::string>(),
std::to_string(result["Rank"].get<int>()),
scores
});
}
input_file.close();
}
void writeEventResultsToFile(const std::string& filename, const std::vector<EventResult>& eventResult) {
std::ofstream output_file(filename, std::ios::app);
if (!output_file.is_open()) {
ErrorHandle::print("无法打开文件: " + filename);
}
for (const auto& result : eventResult) {
output_file << "Full Name: " << result.fullName << std::endl;
output_file << "Rank: " << result.rank << std::endl;
output_file << "Score: " << result.score << std::endl;
output_file << "-----" << std::endl;
}
output_file.close();
}
性能改进
-
我们使用
ffstream
和sstream
两个库来改进传统的文件读取 IO 效率低的问题 -
在 json 解析部分,我们选择使用高性能的
nlohmann/json
库来实现 json 解析 -
在工程项目部分,我们使用 vcpkg 高性能包管理器来管理包
单元测试
在src/tests
中我编写了十个测试样例,以及十个正确的程序输出
我使用了脚本来自动化完成单元测试内容
异常处理
我们封装了一个error.h
和error.cpp
一个简易的错误包装,并进行适当的输出
/**
* @file error.cpp
* @brief 错误封装
* @author ozline
* @date 2024-03-03
*
*/
#include "error.h"
#include <iostream>
#include <cstdlib>
namespace ErrorHandle {
void print(const std::string& message) {
std::cerr << "Error: " << message << std::endl;
}
}
这样可以保证输出的规范性
心得体会
从作业一到作业二,难点主要在于 json 的解析,单元测试的编写和性能提升。
细致来说,我本人第一次尝试使用 cpp 构建工程项目,CMake、Makefile 和 shell 脚本的学习也得到了进一步的加强
但是整个项目结构仍然有一定可优化的地方,期待未来更进一步的提升