文章分享——相似函数处理方法

经常写代码的时候,写着写着就发现,有两个函数,很像。不完全一样,如果一样的话,那就直接合并成一个函数就行了。比如,这次给某个业务改写入 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 和它类似

  1. A 被多个其他函数依赖,改动 A 需要验证的地方很多
  2. B 虽然和 A 类似,但其实是属于整个仓库另一个小模块。如果把 A 和 B 抽出一个公共函数,需要引入其他的依赖
  3. 从目前往长期看,除了 B 不会有其他函数和 A 类似了

也就是说,重复的范围有限,但是从语义/修改成本来看,并不合适现在去优化

泛化

如果发现函数的处理逻辑是一模一样的,唯一不同的是入参的类型,优先考虑泛化。比如,需要一个指针

r 复制代码
func ToPtr[T any](t T) *T {
    return &t
}

增加一个入参

这种适合两个/多个函数的不同,是在处理流程中的固定某一段,就可以通过增加一个参数,来区别不同的场景。增加一个怎么样的参数,也是可以考虑的。

  1. 增加一个 bool型参数。明确知道只有两种情况
  2. 增加一个枚举型参数。只有有限种可能情况
  3. 增加一个函数型参数。无法枚举/预测有多少种情况,给调用方充分的发挥余地

比如说,假设我们把之前那段构造单个请求的代码抽成一个函数

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;
}
  1. 新增的函数与这个函数对比,只有要不要移除 VERSION 的区别,就尝试增加一个 bool 参数

  2. 除了 VERSION 还有 DOC_ID 也可以选择移除/不移除,可以尝试增加一个枚举型参数

  3. 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 文件夹/代码仓库。特别合适的场景是多个部分的代码逻辑/处理方式需要对齐的。比如上游调用下游的接口协议,本来就是需要对齐的,放一起,有改动就互相能感知到,不需要额外的通知。

也有一些函数其实,没有对齐需求的。就不适合放在这种公共库/模块中。不然,就会发生一个模块改了代码,另一个模块挂了。

关注公众号"字节跳动数据库",获取更多技术干货!

相关推荐
码事漫谈5 分钟前
本体论,彻底懂
后端
创新技术阁27 分钟前
FastapiAdmin 系统日志体系与核心配置参数详解
前端·后端·fastapi
掘金者阿豪29 分钟前
SQL Server 迁到金仓,真正麻烦的不是数据,而是这些 T-SQL
后端
eaglewgs32 分钟前
关于AI书写测试用例,谈一下我的思考
人工智能·测试开发·ai·测试用例·agent·测试经验·agent测试开发
熊野君35 分钟前
第 4 章 技术产品经理核心能力模型
大数据·人工智能·产品经理
问天_观心36 分钟前
大模型微调学习(一)
开发语言·人工智能·学习·语言模型·github
百胜软件@百胜软件39 分钟前
AI赋能零售,迈向智能零售时代丨黄飞获邀担任2026年度上海市专业技术人才知识更新工程急需紧缺人才培养项目讲师
人工智能·百度·零售
财复视界41 分钟前
光智科技从“光学元件”到“稀散金属材料平台”的进化逻辑
大数据·人工智能·科技
阿里嘎多学长41 分钟前
2026-09-03 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Kyrie_kk1 小时前
Java--IO--Path文件访问
java·后端