这个作业属于哪个课程 | 2302软件工程 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——文件读取 |
这个作业的目标 | 完成对世界游泳锦标赛跳水项目相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
其他参考文献 | 《腾讯c++代码规范》、单元测试和回归测试、源代码管理、工程师的能力评估和发展、《构建之法》 |
目录
0. Gitcode项目地址
1. PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 预估耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
• Estimate | • 估计这个任务需要多少时间 | 45 | 60 |
Development | 开发 | ||
• Analysis | • 需求分析 (包括学习新技术) | 150 | 300 |
• Design Spec | • 生成设计文档 | 60 | 60 |
• Design Review | • 设计复审 | 45 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 45 | 45 |
• Design | • 具体设计 | 80 | 90 |
• Coding | • 具体编码 | 900 | 1000 |
• Code Review | • 代码复审 | 45 | 45 |
• Test | • 测试(自我测试,修改代码,提交修改 | 100 | 350 |
Reporting | 报告 | ||
• Test Repor | • 测试报告 | 45 | 30 |
• Size Measurement | • 计算工作量 | 20 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 45 |
合计 | 1565 | 2075 |
2. 解题思路描述
刚拿到题目时候当然是一头雾水,仔细思考之后,我大概理清了整个解题思路:
- 需求分析:理解整个项目要求,包括功能需求和非功能需求。明确需要实现的功能模块,比如读取运动员信息、查询比赛成绩等。
- 技术选型:确定项目所需的技术栈,我将选择 C++ 作为编程语言,选择适合处理 JSON 数据的库。
- 学习相关知识:
- 复习 C++ 编程语言的相关基础知识和文件操作相关知识。
- 学习如何处理 JSON 数据,了解 JSON 格式和相关的 C++ JSON 库。
- 查找资料(大部分是在CSDN上进行查找):
- 在线搜索 C++ 文件操作和 JSON 库的相关文档和教程。
- 查阅类似项目的代码示例或教程,了解如何将 C++ 和 JSON 结合使用。
- 设计程序架构:根据需求和学习的知识,设计项目的整体架构,包括模块划分等。
- 编码实现:根据我整个设计的架构和需求,开始编写代码实现各个功能模块。
- 调试和测试:逐步完成各功能模块后进行调试和测试,确保系统能够正常运行并符合预期。
- 优化和改进:根据测试结果,对程序进行优化和改进,提高系统性能和稳定性。
在整个过程中,不断思考问题、查找资料、学习新知识是非常重要的!!同时,遇到问题时可以通过查阅文档、阅读代码示例、请教舍友、同学、老师等方式来解决,确保项目顺利完成。
3 关于JSON数据的处理
3.1 json数据的爬取
由于之前对json了解较少,因此直接从网页上爬取json对我来说有点困难(在一开始的时候),所以一开始就直接使用了助教直接爬取下来的json数据。数据保存在example/datas
中
3.2 json数据处理与解析
step1:格式化json文件
将json文件拷贝到json编辑器网站中进行格式化(我用的是JSON EDITOR online),这时候就可以将又乱又长的json文件变得好看一些了!
变成
step2:分析json数据就比如若我们要实现功能1(输出所有选手信息),那么我们打开
Participations
这一项,会发现我们所需要的数据就在以下框框中。因此所构成的树形结构就是
json
->>Participation
->>Gender / PreferredLastName / PreferredFirstName / NAT同理,对于功能2(输出决赛每个运动项目结果),我们同样选取其中一个比赛项目的json文件,整体结构如下(其中
DisciplineName
表示的是该项目的名称,Heats
中则保存的是整个赛事的流程):
展开Heats,我们便可以发现
Name
表示的是比赛的阶段(Preliminary表示初赛,Semifinal表示半决赛,Final表示决赛阶段);
Results
表示所有运动员比赛的结果;
TotalPoints
表示每个运动员的总分;
Dives
表示的是每一次(这里是每一次跳水)的情况;
ResultId
表示运动员的id(若某个比赛项目是双人项目,则没有这个属性); Rank
表示排名;
FullName
表示运动员的全名。
step3:解析json数据
对于解析json数据,我使用的是nlohmann json
1、首先,在我的C++代码中包含了nlohmann json库的头文件。
#include <nlohmann/json.hpp>
2、读取JSON文件:使用nlohmann json库提供的函数来读取JSON文件内容并解析为JSON对象。例如,可以使用json::parse函数将JSON文件内容解析为JSON对象:
ifstream ifs("datas/results/7b2f14f7-5e6b-44e0-8bb4-f78591e9545b.json");
json data = json::parse(ifs);
3、访问JSON数据:一旦将JSON文件内容解析为JSON对象,就可以通过索引、迭代器或键名来访问JSON数据的各个部分。
4.接口设计和实现过程
整个项目包含了三个文件:DWASearch.cpp
、athlete_results.cpp
和 athlete_results.h
。该项目用于处理运动员比赛结果数据,并根据用户输入的命令来输出对应的比赛结果信息。
4.1 模块接口设计
- athlete_results.h:定义参赛选手个人信息的结构体PlayerInfo和参赛选手比赛细节信息的结构体Detail_Competitors。声明athlete_results类,包括输出所有选手信息、输出比赛结果详情等函数。
- athlete_results.cpp:实现athlete_results类中各个函数的具体功能,包括输出选手信息、输出比赛结果详情等。
- DWASearch.cpp:包含main函数,处理命令行参数,调用athlete_results类中的函数进行具体的查询和结果输出。
4.2 实现过程
1. DWASearch.cpp
DWASearch.cpp
是整个程序的入口,主要负责解析用户输入的命令,并调用athlete_results.cpp
中相应的函数来处理比赛结果信息。- 根据用户输入的命令,调用不同的函数来输出对应的比赛结果信息。
- 将处理后的结果输出到指定的文件中。
2. athlete_results.cpp
athlete_results.cpp
包含了处理比赛结果数据的具体逻辑,定义了各种操作函数来处理选手信息和比赛结果。- 实现了提取选手信息、按照规则排序选手、处理不同比赛项目的结果信息等功能。
- 定义了多个函数来处理不同类型比赛项目的结果信息,如
result_women_1m_Springboard
、result_women_1m_Springboard_Detail
等。
3. athlete_results.h
athlete_results.h
是对外提供接口的声明文件,包含了所有公开的函数接口和结构体定义。- 包括函数声明、结构体定义以及可能需要的常量定义等内容。
- 通过
athlete_results.h
文件,其他模块可以引用其中定义的接口和数据结构,实现模块间的通信和数据交换。
4.3 独到之处
- 通过使用结构体和类,将相关的函数和数据进行了封装,提高了代码的可读性和可维护性。
- 使用了JSON库nlohmann::json来处理JSON数据,简化了数据的解析和处理流程。
- 对于不同比赛项目的结果输出,采用了统一的设计思路,并通过传递不同的参数来实现不同比赛项目的结果输出,减少了代码重复度。
5. 关键代码展示
5.1 选手排序问题
- 单人项目的选手排序按照国籍和姓名进行排序,首先比较国籍,如果国籍不同则按照国籍升序排列,如果国籍相同,则按照姓氏升序排列。
- 双人项目的队友排序通过对队员姓名进行分割,并按照姓氏从小到大进行排序。
5.1.1 单人项目,两个对手排序
// 按照国籍和姓名排序
bool athlete_results::compareParticipants(const PlayerInfo& a, const PlayerInfo& b) {
if (a.country != b.country) {
return a.country < b.country; // 按照国籍升序
} else {
// 如果国籍相同,则按照姓升序
return a.lastName < b.lastName;
}
}
5.1.2 双人项目,两个队友排序
//如果遇到双人项目,选手姓名格式命名为'A & B'按照选手名(Last Name)从小到大排序
void athlete_results::Double_Conpetitiors(string& names, char delimiter){
std::vector<std::string> splitNames;
// 去除分隔符两边的空格
names = trim(names);
// 按指定分隔符分割字符串
std::istringstream iss(names);
std::string name;
while (std::getline(iss, name, delimiter)) {
splitNames.push_back(trim(name)); // 将分割后的人名加入数组,并去除空格
}
// 按关键字从小到大排序
std::sort(splitNames.begin(), splitNames.end(), [](const std::string &a, const std::string &b) {
std::string keyA = extractKey(a);
std::string keyB = extractKey(b);
return keyA < keyB;
});
// 更新原始变量names为排序后的结果
names.clear();
bool first = true;
for (const auto& name : splitNames) {
if (!first) {
names += " & ";
}
names += name;
first = false;
}
return ;
}
5.2 如何获取运动员身份信息
- 通过读取JSON文件中的选手信息,将选手的姓名、姓氏、性别和国籍等信息提取出来,并存储在
PlayerInfo
结构体中。 - 使用
std::stable_sort
函数按照国籍和姓名对选手信息进行排序。
// 功能1:输出所有选手信息
void athlete_results:: Print_Players(std::ofstream& outputFile) {
// 读取JSON文件
std::ifstream ifs("datas/athletes.json");
json data = json::parse(ifs);
vector<PlayerInfo>players;
for(int j=0;j<data.size();j++)
for(int i=0;i<data[j]["Participations"].size();i++){
PlayerInfo player;
player.fullName = removeQuotes(data[j]["Participations"][i]["PreferredLastName"]) + " " + removeQuotes(data[j]["Participations"][i]["PreferredFirstName"]);
player.lastName = removeQuotes(data[j]["Participations"][i]["PreferredLastName"]);
player.gender = (data[j]["Participations"][i]["Gender"] == 0 ? "Male" : "Female");
player.country = removeQuotes(data[j]["Participations"][i]["NAT"]);
players.push_back(player);
}
// 按照国籍和姓名排序
std::stable_sort(players.begin(), players.end(), compareParticipants);
// 输出选手信息
``````
}
}
5.3 如何获取比赛结果
- 遍历包含比赛结果的JSON数据,提取选手姓名、名次、总分和每次比赛得分等信息,并输出比赛结果信息。
- 对选手姓名进行分割,处理双人项目的选手名字格式。
void athlete_results:: Print_Results(const json& finalResults, std::ofstream& outputFile) {
for (const auto& result : finalResults) {
if (!result["FullName"].is_null() && !result["Rank"].is_null() && !result["TotalPoints"].is_null() && !result["Dives"].is_null()) {
string fullName = result["FullName"].get<string>();
Double_Conpetitiors(fullName,'/');
string rank = to_string(result["Rank"].get<int>());
string totalScore = result["TotalPoints"].get<string>();
string scoreStr;
for (const auto& dive : result["Dives"]) {
if (!dive["DivePoints"].is_null()) {
scoreStr += dive["DivePoints"].get<string>() + " + ";
}
}
scoreStr = scoreStr.substr(0, scoreStr.size() - 3) + " = " + totalScore;
//输出比赛结果信息
```
}
}
}
5.4 如何获取比赛的详细结果
- 根据比赛项目的不同场次(如有1场、2场或3场比赛),分别调用不同的函数处理比赛结果信息,将结果存储在Detail_Competitors结构体中,并按照排名进行排序。
void athlete_results:: Print_Results_Detail(ofstream& outputFile, json& data) {
vector<Detail_Competitors> results;
int race_count = data["Heats"].size(); // 记录该项目分为几场比赛
if (race_count == 1) {
First_Competiton(data, results);
} else if (race_count == 2) {
Second_Competiton(data, results);
} else if (race_count == 3) {
Third_Competiton(data, results);
}
sort(results.begin(), results.end(), Rank_Comparation);
//输出详细的比赛结果信息
```
}
5.5 对于不同比赛项目有几场比赛的处理(以有3场比赛为例)
//有3场比赛
void athlete_results:: Third_Competiton(json& data,vector<Detail_Competitors>& results){
json preliminaryResults = data["Heats"][2]["Results"];
json semifinalResults = data["Heats"][1]["Results"];
json finalResults = data["Heats"][0]["Results"];
for (const auto& player : preliminaryResults) {
Detail_Competitors competitor;
competitor.fullName=player["FullName"];
Double_Conpetitiors(competitor.fullName,'/');
competitor.rank=to_string(player["Rank"].get<int>());
competitor.Rank_First=player["Rank"].get<int>();
competitor.Preliminary_Event="*";
competitor.Semifinal_Event="*";
competitor.Final_Event="*";
competitor.Preliminary_Event= Detail_Scores(competitor,player);
bool issemifinal=true;
for(const auto& semresult : semifinalResults){
if(semresult["FullName"]==competitor.fullName){
competitor.rank=competitor.rank+" | "+to_string(semresult["Rank"].get<int>());
competitor.Semifinal_Event= Detail_Scores(competitor,semresult);
issemifinal=false;
break;
}
}
bool isfinal=true;
for(const auto& finalresult : finalResults){
if(finalresult["FullName"]==competitor.fullName){
competitor.rank=competitor.rank+" | "+to_string(finalresult["Rank"].get<int>());
competitor.Final_Event= Detail_Scores(competitor,finalresult);
isfinal=false;
break;
}
}
if(issemifinal)
competitor.rank=competitor.rank+" | *";
if(isfinal)
competitor.rank=competitor.rank+" | *";
results.push_back(competitor);
}
}
5.6 对于输入指令的处理
- 通过解析输入的指令,根据指令的不同调用相应的函数处理并输出对应的结果。
- 如果输入的指令不符合已定义的规则,则输出错误信息。
std::istringstream iss(command);
std::string word;
iss >> word; // 获取第一个单词
if (command == "players")
{
athlete_results::Print_Players(ofs);
}
else if(command=="result women 1m springboard")
{
athlete_results::result_women_1m_Springboard(ofs);
}
else if(command=="result women 1m springboard detail")
{
athlete_results::result_women_1m_Springboard_Detail(ofs);
}
//其他项目类似指令处理
```
else if (word == "result")
{
ofs << "N/A" << "\n";
ofs << "-----" << "\n";
}
else {
ofs << "Error" << "\n";
ofs << "-----" << "\n";
//return 1;
}
}
5.7 算法总结
以上算法主要涉及对JSON数据的解析、字符串处理、排序和条件判断等操作,以实现运动员信息的提取、比赛结果的输出和指令的处理。
6. 性能改进
6.1 减少文件的读写次数
在优化后的代码中,文件的打开和关闭操作只进行了一次,而不是每次处理命令都重新打开和关闭文件。
std::ifstream ifs(inputFile);
std::ofstream ofs(outputFile);
6.2 减少字符串的比较次数
通过使用command.substr(0, 6)
提取出命令的前6个字符,并与"result
"进行比较,避免了在循环内频繁比较整个命令字符串。
if (command.substr(0, 6) == "result") {
// 处理特定格式的结果命令
} else {
// 处理其他情况
}
6.3 避免不必要的重复计算
将命令与处理函数的映射表commandMap
设为静态变量,避免了重复创建和销毁的开销。
static const std::unordered_map<std::string, std::function<void(std::ofstream&)>> commandMap = {
// 命令与处理函数的映射关系
};
通过这些优化,可以提高代码的性能和效率,减少不必要的资源消耗。
【注】优化前运行时间约为1.427s;
优化后时间减少约为0.4s,为0.99s。
7. 单元测试
7.1 测试数据介绍
对于构造测试数据,包括以下几个步骤:
-
边界条件测试: 确保测试数据覆盖了输入参数的边界条件,包括最小值、最大值、边界值等。
-
常规情况测试: 一般情况下的测试数据,包括典型输入和预期输出。
-
异常情况测试: 测试程序对异常输入的处理,例如空输入、非法输入等。
-
特殊情况测试: 针对特殊场景构造的测试数据,例如特定业务逻辑下的输入和输出。
7.2 测试过程
单元测试我使用了 Microsoft 的 Visual Studio 的 C++ 单元测试框架。主要功能是对DWASearch.exe 的程序进行多个测试,并比较程序输出的结果文件与预期正确答案文件是否一致来进行测试验证。
1. Files_Comparation 函数:
- 该函数用于比较两个文件的内容是否完全一致(输出文件以及正确答案文件)。
- 通过逐行读取两个文件的内容,如果发现有不一致的行,则返回 false,表示文件内容不同;否则返回 true,表示文件内容相同。
2. UnitTest1 命名空间:
- 包含了多个测试方法,每个方法对应一组测试用例。
- 每个测试方法内部:
- 构造了调用 DWASearch.exe 程序所需的参数数组 t,包括程序路径、输入文件和输出文件。
- 调用 main 函数执行 DWASearch.exe 程序。
使用Assert::AreEqual
来断言程序输出的结果文件是否正确,从而判断测试是否通过。
3. 测试用例:
- 代码中包含了多个测试用例(Test_1 到 Test_10),
测试用例的设计是为了覆盖不同的输入情况,以确保被测试的程序在各种情况下都能正确输出结果。
总体来说,利用单元测试框架对 DWASearch.exe 程序进行了自动化测试,验证其输出是否符合预期结果。部分测试代码以及测试结果如下:
7.3 覆盖率的测试
为了评估测试覆盖率,可以使用各种代码覆盖率工具,例如OpenCppCoverage、gcov等。以下是通过7.2单元测试后代码覆盖率截图,基本上实现了覆盖率95%+:
7.4 如何优化覆盖率
若覆盖率一开始比较低,那么想要优化可以考虑以下几点:
- 增加测试用例:覆盖更多的分支和边界条件,尽可能覆盖所有可能的情况。
- 调整测试策略:根据代码的复杂度和重要性,优先编写测试用例,确保关键代码得到充分覆盖。
- 使用覆盖率工具:借助代码覆盖率工具分析覆盖情况,找出未覆盖的部分,并有针对性地补充测试用例。
- 定期回顾和更新:随着代码的修改和迭代,测试用例也需要不断更新和补充,以保证覆盖率的有效性。
通过以上优化措施,可以提高测试覆盖率,确保软件质量和稳定性。
8. 异常处理
8.1文件打开失败处理
在主函数 main
中,对输入文件和输出文件的打开操作后,都进行了状态检查,并在打开失败时输出错误信息并返回非零值,表示程序异常终止。
std::ifstream ifs(inputFile);
if (!ifs.is_open()) {
std::cerr << "Error opening input file." << "\n";
return 1;
}
std::ofstream ofs(outputFile);
if (!ofs.is_open()) {
std::cerr << "Error opening output file." << "\n";
return 1;
}
8.2 命令处理异常处理
在 processCommand
函数中,如果传入的命令不在预设的命令映射中,会输出错误信息到输出文件并继续处理下一个命令。
auto it = commandMap.find(command);
if (it != commandMap.end()) {
it->second(ofs);
} else if (command.substr(0, 6) == "result") {
ofs << "N/A" << "\n";
ofs << "-----" << "\n";
} else {
ofs << "Error" << "\n";
ofs << "-----" << "\n";
}
8.3 文件关闭
在主函数结束时,使用 ifs.close()
和ofs.close()
关闭输入和输出文件,确保文件资源正确释放。
9. 心得体会
在开发这个项目时,我学到了很多关于JSON数据处理、文件操作、字符串处理和排序算法的知识。通过这个项目,我对C++的文件输入输出、字符串处理等方面有了更深入的理解,并学会了如何利用现有的第三方库(比如nlohmann/json)来处理JSON数据。
除此之外,我也体会到了模块化设计的重要性。将功能拆分成不同的函数和类,能够使代码更加清晰易懂,便于维护和扩展。同时,通过合理地组织代码结构,可以更好地实现代码的复用和降低耦合度。
在开发过程中,我也遇到了一些挑战,比如对JSON数据结构的理解、异常处理和错误消息的输出等问题。通过不断查阅文档、学习和调试,我逐渐解决了这些问题,并且积累了更多的编程经验。
总的来说,这个项目让我获益良多,不仅加深了我对C++语言的理解,还提高了我的问题解决能力和编程技能。希望未来能有更多这样的项目实践机会,让我不断进步!