经常写代码的时候,写着写着就发现,有两个函数,很像。不完全一样,如果一样的话,那就直接合并成一个函数就行了。比如,这次给某个业务改写入 ES 代码,在构造写入 ES 的数据时候,有一个样例是这样的
ini
private BulkRequest getIndexBulkRequest(List<JSONObject> dataList, String index) {
BulkRequest bulkRequest = new BulkRequest(index);
dataList.forEach(jsonObject -> {
String docId = jsonObject.getString(DOC_ID);
long version = jsonObject.getLongValue(VERSION);
// 移除
jsonObject.remove(DOC_ID);
jsonObject.remove(VERSION);
IndexRequest indexRequest = new IndexRequest();
indexRequest.id(docId);
indexRequest.source(jsonObject.toJSONString(), XContentType.JSON);
indexRequest.version(version);
indexRequest.versionType(VERSION_TYPE);
indexRequest.timeout(TimeValue.timeValueMillis(Constant.TIME_OUT));
bulkRequest.add(indexRequest);
// 放回去
jsonObject.put(DOC_ID, docId);
jsonObject.put(VERSION, version);
});
return bulkRequest;
}
然后,这个函数有一些变种,比如,version 要不要移除;请求类型除了 index 还可以是 upsert/update;冲突的情况下重试次数也有不同的配置等。上面是封成一个函数的,还有很多类似的逻辑,散乱在不同的函数中。当我要去改的时候,我需要仔细的对比,我写的函数的功能,是不是和原来是一样的。工作量很大。这里就总结一下,在这种代码类似的情况下,有可能的处理方法
不管它
很多人可能会笑,不管它也能算方法吗 ? 真的就有适用这种方法的情况,比如,已经存在一个函数 A,需要新增的函数 B 和它类似
- A 被多个其他函数依赖,改动 A 需要验证的地方很多
- B 虽然和 A 类似,但其实是属于整个仓库另一个小模块。如果把 A 和 B 抽出一个公共函数,需要引入其他的依赖
- 从目前往长期看,除了 B 不会有其他函数和 A 类似了
也就是说,重复的范围有限,但是从语义/修改成本来看,并不合适现在去优化
泛化
如果发现函数的处理逻辑是一模一样的,唯一不同的是入参的类型,优先考虑泛化。比如,需要一个指针
r
func ToPtr[T any](t T) *T {
return &t
}
增加一个入参
这种适合两个/多个函数的不同,是在处理流程中的固定某一段,就可以通过增加一个参数,来区别不同的场景。增加一个怎么样的参数,也是可以考虑的。
- 增加一个 bool型参数。明确知道只有两种情况
- 增加一个枚举型参数。只有有限种可能情况
- 增加一个函数型参数。无法枚举/预测有多少种情况,给调用方充分的发挥余地
比如说,假设我们把之前那段构造单个请求的代码抽成一个函数
ini
private Request getRequest(JSONObject jsonObject, String index) {
String docId = jsonObject.getString(DOC_ID);
long version = jsonObject.getLongValue(VERSION);
// 移除字段
jsonObject.remove(DOC_ID);
jsonObject.remove(VERSION);
IndexRequest indexRequest = new IndexRequest();
indexRequest.id(docId);
indexRequest.source(jsonObject.toJSONString(), XContentType.JSON);
indexRequest.version(version);
indexRequest.versionType(VERSION_TYPE);
indexRequest.timeout(TimeValue.timeValueMillis(Constant.TIME_OUT));
// 放回字段,先不考虑这段
// jsonObject.put(DOC_ID, docId);
// jsonObject.put(VERSION, version);
return indexRequest;
}
-
新增的函数与这个函数对比,只有要不要移除 VERSION 的区别,就尝试增加一个 bool 参数
-
除了 VERSION 还有 DOC_ID 也可以选择移除/不移除,可以尝试增加一个枚举型参数
-
jsonObject 里面有很多字段,调用方还想移除其他字段,可以尝试增加函数型参数,让调用方自己传入一个函数来处理 jsonObject 的内容
增加 hook 配置
适合多个函数的不同,是在处理流程中的不同段。比如说,之前写过一个运行任务的模块。其中 On***() 函数,都可以认为是 hook 的内容。就是每个任务,在开始时、失败时、成功时处理的方式都不太一样。
go
func (t *Task) Run(ctx context.Context) {
err := t.OnStart(ctx) // 任务运行前
if err != nil {
logs.CtxError(ctx, "[task] task on start failed, taskId=%d, err=%v", t.ID, err)
return
}
err = t.runSteps(ctx)
if err != nil {
_ = t.OnFailure(ctx) // 任务运行失败
logs.CtxError(ctx, "[task] run task failed, taskId=%d, err=%v", t.ID, err)
return
}
if t.CurStep == nil {
_ = t.OnSuccess(ctx) // 任务运行成功
return
}
}
抽出一个配置
不符合上面列举的情况,但确实是相似的。就是说没啥规律。就比如说文章开头的例子。它在很多地方有小修改的地方,但是,又不是固定的某个/某几个处理流程里面的不同。我的做法,就是抽一个配置出来,然后,在 buildOp 里面根据配置生成不同的文档内容。把相似的代码和这段代码的一个复杂度,简化成配置 + 固定到一个更小的范围内
ini
static class BulkOption {
final OpType opType;
final VersionHandling versionHandling;
final boolean docAsUpsert;
final int retryOnConflict;
private BulkOption(OpType opType, VersionHandling versionHandling, boolean docAsUpsert, int retryOnConflict) {
this.opType = opType;
this.versionHandling = versionHandling;
this.docAsUpsert = docAsUpsert;
this.retryOnConflict = retryOnConflict;
}
/** 对应 ElasticsearchWriter#bulkIndex:remove VERSION,不设外部版本号 */
static BulkOption bulkIndex() {
return new BulkOption(OpType.INDEX, VersionHandling.DISCARD, false, 0);
}
// ....
}
private BulkOperation buildOp(JSONObject jsonObject, String index, BulkOption option) {
String docId = jsonObject.getString(DOC_ID);
jsonObject.remove(DOC_ID);
long version = 0;
if (option.versionHandling == VersionHandling.EXTERNAL) {
version = jsonObject.getLongValue(VERSION);
jsonObject.remove(VERSION);
} else if (option.versionHandling == VersionHandling.DISCARD) {
jsonObject.remove(VERSION);
}
// KEEP: VERSION 保留在 jsonObject 中,直接写入文档
Map<String, Object> doc = new HashMap<>(jsonObject);
// 还原 jsonObject
jsonObject.put(DOC_ID, docId);
if (option.versionHandling == VersionHandling.EXTERNAL) {
jsonObject.put(VERSION, version);
}
final String fDocId = docId;
final String fIndex = index;
final long fVersion = version;
final boolean fDocAsUpsert = option.docAsUpsert;
final int fRetryOnConflict = option.retryOnConflict;
switch (option.opType) {
case INDEX:
if (option.versionHandling == VersionHandling.EXTERNAL) {
return BulkOperation.of(op -> op.index(idx -> idx
.index(fIndex).id(fDocId).document(doc)));
}
return BulkOperation.of(op -> op.index(idx -> idx.index(fIndex).id(fDocId).document(doc)));
case UPDATE:
// ...
return BulkOperation.of(op -> op.update(updateOp));
case DELETE:
if (option.versionHandling == VersionHandling.EXTERNAL) {
return BulkOperation.of(op -> op.delete(del -> del
.index(fIndex).id(fDocId).version(fVersion).versionType(VersionType.External)));
}
return BulkOperation.of(op -> op.delete(del -> del.index(fIndex).id(fDocId)));
default:
throw new IllegalStateException("Unknown OpType: " + option.opType);
}
}
抽出一个公共库/模块
当公共的函数,涉及多个模块/多个仓库的时候。就可以抽出一个公共模块,比如常见的 common/utils 文件夹/代码仓库。特别合适的场景是多个部分的代码逻辑/处理方式需要对齐的。比如上游调用下游的接口协议,本来就是需要对齐的,放一起,有改动就互相能感知到,不需要额外的通知。
也有一些函数其实,没有对齐需求的。就不适合放在这种公共库/模块中。不然,就会发生一个模块改了代码,另一个模块挂了。
关注公众号"字节跳动数据库",获取更多技术干货!