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

经常写代码的时候,写着写着就发现,有两个函数,很像。不完全一样,如果一样的话,那就直接合并成一个函数就行了。比如,这次给某个业务改写入 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 文件夹/代码仓库。特别合适的场景是多个部分的代码逻辑/处理方式需要对齐的。比如上游调用下游的接口协议,本来就是需要对齐的,放一起,有改动就互相能感知到,不需要额外的通知。

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

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

相关推荐
Bigfish_coding1 小时前
前端转agent-【python】-12 LangChain 入门实战:RAG + LCEL 链式调用
人工智能
云技纵横1 小时前
@Transactional 失效的 7 种场景:第 5 种最难排查
后端
用户6757049885021 小时前
你知道 Go 结构体和结构体指针调用的区别吗?一文带你彻底搞懂!
后端·go
程序员cxuan2 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
用户6757049885022 小时前
面试官问“装饰器模式”,这样回答薪资多要 3000!
后端
tntxia2 小时前
Geo Scene域名修改引起的一些问题
后端
用户298698530142 小时前
Java 实现 Word 文档加密与权限解除
java·后端
AskHarries2 小时前
多 Agent 与任务队列:什么时候需要拆分任务
程序员
饼干哥哥2 小时前
扣子3.0测评:我让 Codex 和 Claude Code 住同一个桌面,结果它们打架了!
人工智能·开源·代码规范