软件工程第二次作业--文件读取

182000214廖文焘 2024-02-20 20:38:00
这个作业属于哪个课程https://bbs.csdn.net/forums/ssynkqtd_06
这个作业要求在哪里https://bbs.csdn.net/topics/618087255
这个作业的目标完成文件读取小练习
其他参考文献

目录

  • 作业基本信息
  • 解题思路描述
  • 数据如何处理
  • 如何解耦
  • 代码规范
  • 接口设计和实现过程
  • command parse and handle
  • json parse
  • entities
  • 关键代码展示
  • main process
  • cmd parser
  • 性能改进
  • 单元测试
  • 异常处理
  • 心得体会

作业基本信息

PSP预估耗时(分钟)实际耗时(分钟)
Planning55
- Estimate5 * 605 * 60
Development3 * 603 * 60
- Analysis1515
- Design Spec00
- Design Review00
- Coding Standard00
- Design55
- Coding2 * 602 * 60
- Code Review4040
- Test6060
Reporting6060
- Test Report4040
- Size Measurement00
- Postmortem & Process Improvement Plan2020
Total6 * 606 * 60

项目在 Linux 平台下开发,windows 平台的二进制文件是静态链接交叉编译的,如果提供的二进制文件有问题请告知

解题思路描述

数据如何处理

项目给出的是 json 格式的结构化数据,手写 parser 并不合理。使用 picojson 库对 json 进行解析

如何解耦

程序的主要流程为:

  1. 读取 json 文件
  2. 转为对应数据结构
  3. 读取指令
  4. 解析指令并处理
  5. 输出到指定文件

故将程序分为以下 4 个部分

  1. main process,负责输入输出 / 调用各模块完成功能

  2. json parser,用于读取指定文件,并转为 json object

  3. entities,负责构造 / 维护程序所需的数据结构

  4. cmd handler,负责解析指令 / 返回参数与处理函数

代码规范

使用 clang-format 进行代码格式化,基于 google style 修改;

具体 .clang-format config 在 $(project)/codesytle.md 中。

接口设计和实现过程

command parse and handle

该模块负责解析指令并提取参数,返回处理函数,有以下定义:

// 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>;

json parse

读取文件并解析为 picojson::object

auto open_and_parse_json(const string& prefix, const string& path)
    -> expected<picojson::value, err_msg_t>;

entities

设计了 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 写死了。

关键代码展示

main process

去除了异常处理相关的代码,仅展示整体逻辑结构

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;
}

cmd parser

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;
}

对于程序整体,设计了如下测试用例:

  1. 单条指令输入
    • players
    • result * detail
    • result *
  2. 多条连续指令
  3. invalid command
    • test-cmd
    • player
  4. invalid arguments
    • result * details
    • result detail
    • result hello
  5. 存在 event 但不存在 result

测试的输入和输出放置在 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();

心得体会

  1. 虽然代码写成了 shi 山,但是对 modern cpp 多了一些了解,在实践中更好地体会到了所有权与完美转发等新的特性。
  2. 测试十分重要,尽早测试,尽可能完全地测试是十分必要的。否则等调用链越来越长,而未经测试的代码越来越多,调试的难度也会越来越大。
  3. 模块化十分重要,一方面解耦程序,使它更加灵活而可修改可维护,另一方面为测试也提供了便利。
...全文
125 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

109

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧