118
社区成员
这个作业属于哪个课程 | https://bbs.csdn.net/forums/ssynkqtd_06 |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/topics/618087255 |
这个作业的目标 | 完成文件读取小练习 |
其他参考文献 | 无 |
PSP | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
Planning | 5 | 5 |
- Estimate | 5 * 60 | 5 * 60 |
Development | 3 * 60 | 3 * 60 |
- Analysis | 15 | 15 |
- Design Spec | 0 | 0 |
- Design Review | 0 | 0 |
- Coding Standard | 0 | 0 |
- Design | 5 | 5 |
- Coding | 2 * 60 | 2 * 60 |
- Code Review | 40 | 40 |
- Test | 60 | 60 |
Reporting | 60 | 60 |
- Test Report | 40 | 40 |
- Size Measurement | 0 | 0 |
- Postmortem & Process Improvement Plan | 20 | 20 |
Total | 6 * 60 | 6 * 60 |
项目在 Linux 平台下开发,windows 平台的二进制文件是静态链接交叉编译的,如果提供的二进制文件有问题请告知
项目给出的是 json 格式的结构化数据,手写 parser 并不合理。使用 picojson 库对 json 进行解析
程序的主要流程为:
故将程序分为以下 4 个部分
main process,负责输入输出 / 调用各模块完成功能
json parser,用于读取指定文件,并转为 json object
entities,负责构造 / 维护程序所需的数据结构
cmd handler,负责解析指令 / 返回参数与处理函数
使用 clang-format 进行代码格式化,基于 google style 修改;
具体 .clang-format config 在 $(project)/codesytle.md 中。
该模块负责解析指令并提取参数,返回处理函数,有以下定义:
// handler 类型定义
using cmd_handler_t = std::function<
expected<string, err_msg_t>(const char*, vector<athlete_t>&, vector<event_t>&)>;
// parser
auto parse_cmd(string cmd) -> expected<tuple<cmd_handler_t, string>, err_msg_t>;
// example handler which can process 'players' command
auto show_players(
const char* args,
vector<athlete_t>& athletes,
vector<event_t>& events
) -> expected<string, err_msg_t>;
读取文件并解析为 picojson::object
auto open_and_parse_json(const string& prefix, const string& path)
-> expected<picojson::value, err_msg_t>;
设计了 event_t / result_t / athelet_t 三个实体类,提供 from_json 函数进行构造。
逻辑上应该使用 concept 对三个类型进行约束,提供统一的构造 / 输出接口,这样可以简化 handler 的设计为:
expected<string, err_msg_t>(const char*, vector<T>& Args...)>;
并使 T 服从于上述 concept,但由于提供 detail 功能,使得对象之间存在组合后进行输出的操作,并不能简单这样设计,因此便干脆偷懒,直接不做约束并将 cmd_handler_t 写死了。
去除了异常处理相关的代码,仅展示整体逻辑结构
auto main(int argc, char** argv) -> int {
// check the command line arguments
auto opt = getopt(argc, argv);
auto [input_path, output_path] = opt.value();
// get the json objects from the files and make object from them
auto athletes = open_and_parse_json("./datas/", "athletes.json")
.and_then([](auto&& v) { return athlete_t::from_json(v); });
auto events = open_and_parse_json("./datas/", "event.json")
.and_then([](auto&& v) { return event_t::from_json(v, "./datas/"); });
string cmd;
std::fstream input, output;
input.open(input_path, std::ios::in);
output.open(output_path, std::ios::out);
// process each line of the input file
while (std::getline(input, cmd)) {
// parse the command
auto cmd_handler = parse_cmd(cmd);
// destruct the handler and the args from the tuple
auto [handler, args] = cmd_handler.value();
// use the handler to process the command
auto res = handler(args.c_str(), *athletes, *events);
// process the result
res.map([&](const string& s) { output << s; });
}
input.close();
output.close();
return 0;
}
auto parse_cmd(string cmd) -> expected<tuple<cmd_handler_t, string>, err_msg_t> {
// the command pattern and the handler
static const vector<tuple<string, cmd_handler_t>> cmd_handlers = {
static const vector<tuple<string, cmd_handler_t>> cmd_handlers = {
{"^players$", show_players},
{"^result (.*) detail$", show_result_detail},
{"^result (.*)", show_result},
};
};
// find the handler that matches the command
for (const auto [pattern, handler] : cmd_handlers) {
std::smatch match;
if (std::regex_search(cmd, match, std::regex(pattern))) {
// get the args, and return the result
auto args = match.size() == 1 ? "" : match[1].str();
return std::make_tuple(handler, args);
}
}
// failed to find a matching handler
return unexpected<err_msg_t>("invalid command");
}
这里的 pattern 是写死的,但更好的做法是提供一个注册 handler 的接口,作业中所说的 "core lib" 向该模块注册所有的 command pattern 与 handler,这样指令解析的模块就不会有变动了。
程序完成需求的部分并没有性能开销特别大的地方,主要的开销就在于磁盘的读写与 json 解析了。因此尽可能减少这部分开销即可。考虑到程序本身提供的是批处理读取指令的方式,要达到上述目标就需要做到无需读取的文件就不读不解析,已经读取过的文件就不用再读再解析。从这个角度出发,使用懒加载并缓存这样的思路十分自然。
对于 event_t 提供了一个额外的函数,即 event_t::get,在首次调用时,会读取文件并缓存结果,在之后的调用时,就将直接返回,从而做到了上述要求。
auto event_t::get(event_stage_t stage)
-> expected<shared_ptr<vector<result_t>>, err_msg_t> {
if (std::all_of(results.begin(), results.end(),
[](const auto& p) { return p.second->size() == 0; })) {
return init_results()
.map([this, stage]() { return results[stage]; })
.map_error([](const err_msg_t& e) { return e; });
}
return results[stage];
}
此外,该程序并不改变输入文件的状态,因此十分便于并行化,只需在输出时做好同步即可。
由于没有使用 IDE 进行开发,所以测试是手写的,放在 src/test 文件夹下,并使用 makefile 进行管理。使用 arch=[linux, win] make test 即可进行测试。
对于每个单独的模块,分别进行了测试,以 cmd parse 部分,进行了如下测试:
#include "../include/CMDHandler.h"
auto main() -> int {
//! test cmd match
auto cmd = "players";
auto [handler, args] = parse_cmd(cmd).value();
assert(args == "");
cmd = "result 100m";
auto [handler2, args2] = parse_cmd(cmd).value();
assert(args2 == "100m");
cmd = "result 100m detail";
auto [handler3, args3] = parse_cmd(cmd).value();
assert(args3 == "100m");
cmd = "result 100m details";
auto [handler4, args4] = parse_cmd(cmd).value();
assert(args4 == "100m details");
return 0;
}
对于程序整体,设计了如下测试用例:
测试的输入和输出放置在 src/test/input | output 下
异常处理没有使用 try - catch 风格,尝试使用了将在 c++ 23 进入标准库中的 std::expected (项目用 tl::expected 库替代)。cpp 在对于一个抛出的异常时,会进行栈解旋,直到找到能处理该异常的函数(或是到最顶层后 terminate);这就允许了在调用一个可能抛出异常的函数时可以不处理。而 expected 要求必须显式地处理错误。
此外 expected 提供了一系列链式调用的函数,使得代码简洁且可读性更高。
以读取 json 并转为 entity 为例:
auto events = open_and_parse_json("./datas/", "event.json")
.and_then([](auto&& v) { return event_t::from_json(v, "./datas/"); })
.map_error([](auto&& e) {
cout << "Error: " << e << endl;
exit(1);
})
.value();