游戏之作: Restful命令处理对象

我们的产品有几十条Restful命令, 凭心而林, 命令太多, 不正交. 更大的问题是命令的api更杂乱.
对上层代码而言, 屏蔽Restful命令差异的方式当然是封装接口函数. 但是为每一个函数封装一个接口, 又有大量的样板代码. 虽然前期我是这么做的, 但是随着接口越来越多, 也越来越让人感到厌烦.
比如, 下面是一条命令的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool KaryoServices::modifyCaseInfo(const QString &case_id, const QVariantMap &info)
{
QJsonObject rsp;

QJsonObject param = QJsonObject::fromVariantMap(info);
TRACE() << param;

QString path = QString("/api/case/%1").arg(case_id);
try {
rsp = _impl->_api->putSync(path, QJsonDocument(param));
}
catch (const std::exception& e)
{
_impl->_errorString = QString(tr("modify case info failed: %1")).arg(e.what());
TRACE() << _impl->_errorString << "path: " << path;
emit errorMsg(_impl->_errorString);
return false;
}

if(rsp["code"].toInt() != 200)
{
_impl->_errorString = QString(tr("modify case info error: %1")).arg(rsp["message"].toString());
TRACE() << _impl->_errorString << "path: " << path;
emit errorMsg(_impl->_errorString);
}
TRACE() << "修改病历信息成功" << path;
return true;
}

换一个命令, 其实现也都是的大同小异.

当然, 一种做法是将从try开始的部分都提取到一个公共函数里面. 我们对查询命令也是这么做的. 那么, 有没有一种更好的做法呢?

今天又新增了一条修改命令, 突发奇想, 将每条命令实现为一个函数对象如何? 这样才像是C++的样子嘛:-).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
enum class ApiCmdType
{
modifyCase,
modifySlide,
modifyMitosis,
};

template <ApiCmdType cmd>
class DbModifier
{
public:
template<typename... Ts, ApiMethodType=ApiMethodType::putMethod>
DbModifier(KaryoServices* service, Ts... args)
: _service{service}
{
QString result ;
if constexpr (cmd==ApiCmdType::modifyCase)
{
result = "api/case/";
}
else if constexpr (cmd==ApiCmdType::modifySlide)
{
result = "api/slide/";
}
else if constexpr (cmd==ApiCmdType::modifyMitosis)
{
result = "api/mitosis/";
}
auto h = [](const auto& x){ return QString("%1/").arg(x); };
_path = (result + ... + h(args));
}
DbModifier& operator() (const QString& name, QVariant value){
_params.insert(name, value);
return *this;
}
tl::expected<bool, QString> operator()()
{
QJsonObject rsp;
QJsonObject param = QJsonObject::fromVariantMap(_params);
try {
rsp = _service->api()->putSync(_path, QJsonDocument(param));
}
catch (const std::exception& e) {
QString errmsg = QString("command %1 failed: %2").arg(_path).arg(e.what());
return tl::unexpected(errmsg);
}
if(rsp["code"].toInt() != 200)
{
QString errmsg = QString("command %1 failed: %2").arg(_path).arg(rsp["message"].toString());
return tl::unexpected(errmsg);
}
return true;
}
const QString path() const{
return _path;
}
const QVariantMap& data() const{
return _params;
}
private:
KaryoServices* _service;
QString _path;
QVariantMap _params;
};

这里的operator()纯属给自己找不自在, 完全可以起一个正常点的函数名”-).

下面是使用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 更新case信息:
TRACE() << "开始更新Case信息...";
karyolib::services::DbModifier<karyolib::services::ApiCmdType::modifyCase> caseModifier(_services, _impl->_context._case_id);
auto ret = caseModifier("id", _impl->_context._case_id)
("sufferer", result.value("name"))
("age", result.value("age"))
("age_unit", result.value("ageUnit"))
("sex", result.value("gender"))
("identity", result.value("identity"))
("case_no", result.value("caseNo"))
("summary", result.value("diagnosis"))
("admission_no", result.value("admissionNo"))
("bed_no", result.value("bedNo"))
("department", result.value("department"))
();
TRACE() << QString("更新case信息%1").arg(ret.has_value() ? "成功":"失败:"+ret.error());

这里主要利用了C++17的const if和variadic两个特性, 个人认为这是C++17中最有价值的特性. 其实, 相对来说, C++20的特性对我们这种普通人来说, 反倒没有太多惊艳的地方: 最惊艳的conceptmodule, 前者我几乎不用元编程, 后者在Qt下, 想用也几乎用不了.