1 从一个接口说起
1.1 初始接口
假设现在有一个创建订单接口:
xml
public OrderCreateResultDTO createOrder(OrderCreateDTO order)
- 创建订单对象
xml
public class OrderCreateDTO {
private String bizColumnA;
private String bizColumnB;
private String bizColumnC;
private String bizColumnD;
}
- 订单响应对象
xml
public class OrderCreateResultDTO {
private String orderId;
private String bizColumnA;
private String bizColumnB;
private String bizColumnC;
private String bizColumnD;
}
1.2 发生变化
现在业务发生变化,产品经理提出需求:创建订单时不需要C和D字段,新增E和F字段。如果你是开发人员,你会怎么设计这个接口?有一种比较朴素的思路是直接删除C和D字段,新增E和F字段:
- 创建订单对象
xml
public class OrderCreateDTO {
private String bizColumnA;
private String bizColumnB;
private String bizColumnE;
private String bizColumnF;
}
- 订单响应对象
xml
public class OrderCreateResultDTO {
private String orderId;
private String bizColumnA;
private String bizColumnB;
private String bizColumnE;
private String bizColumnF;
}
1.3 发现问题
我们想一想上述方案是否可行呢?答案是不行,有以下三方面原因:
原因一是接口契约性:接口是本服务暴露给外部使用的,相当于上下游签了合同,如果随意修改合同,那么合同严肃性荡然无存。
原因二是版本兼容性:我们知道APP是有版本号的,假设APP版本1使用的是初始接口,这时APP版本2由于新业务需要使用新接口,但是你不能直接把初始接口改的面目全非,因为APP版本1有很多用户在用,如果只考虑新版本,那老版本将会报错。
原因三是时间窗口:我们知道APP发布版本是需要审核的,即使这一版本是强更,也会有一个时间窗口新功能和老功能是并存的,所以服务端接口在升级是必须考虑这种情况。
2 如何思考
既然不能直接升级,那么应该如何解决这个问题?我认为需要从两个维度思考:
- 分层维度
- 分端维度
2.1 分层思考
在代码结构落地实践中可以分为多层,但是核心还是三层,我们分别分析每一层如何适配接口变更:
- 数据层
- 业务层
- 表现层
2.1.1 数据层
因为要考虑新老版本并存的问题,所以数据层必须保留新老版本所有字段,标记老字段标记为废弃,但是需要修改字段是否必填性,因为一些老版本字段变成不必填:
xml
public class OrderCreateDO {
private String orderId;
private String bizColumnA;
private String bizColumnB;
@Deprecated
private String bizColumnC;
@Deprecated
private String bizColumnD;
private String bizColumnE;
private String bizColumnF;
}
2.1.2 业务层
业务层是承载核心业务的层级,所以包含大量的业务逻辑,有两种方案:
- 方案一:新增业务对象,重写业务方法
- 方案二:修改业务对象,适配业务方法
方案一优点是不与老业务耦合,缺点是新老方法逻辑可能只有少部分不一样,所以需要复制老业务方法大量逻辑到新方法中。
方法二优点是可以复用老逻辑,缺点是新老逻辑耦合,如果适配逻辑没有处理好,可能会影响老逻辑。
所以两种方案有各自使用场景,方案一适用于业务逻辑重大变动场景,既然是重构所以可以重新声明新业务对象:
xml
public class OrderCreateNewBO {
private String bizColumnA;
private String bizColumnB;
private String bizColumnE;
private String bizColumnF;
}
方案二适用于业务逻辑微调变动场景,所以老业务对象需要包含新老字段,标记老字段标记为废弃:
xml
public class OrderCreateBO {
private String bizColumnA;
private String bizColumnB;
@Deprecated
private String bizColumnC;
@Deprecated
private String bizColumnD;
private String bizColumnE;
private String bizColumnF;
}
2.1.3 展示层
展示层不应该处理复杂业务逻辑,而应该是对业务对象的裁剪和适配,所以可以新增一个新版本接口,没有业务层那种负担:
- 创建订单对象
xml
public class OrderCreateDTOV2 {
private String bizColumnA;
private String bizColumnB;
private String bizColumnE;
private String bizColumnF;
}
- 订单响应对象
xml
public class OrderCreateResultDTOV2 {
private String orderId;
private String bizColumnA;
private String bizColumnB;
private String bizColumnE;
private String bizColumnF;
}
但是如果展示层不规范,包含大量业务逻辑,那么思考方式和业务层一样,也需要使用方案二。
2.2 分端思考
一个系统在业务上通常分三个端:
- 面向B端用户
- 面向C端用户
- 面向运营用户
这三个端都具有BFF层,但是通常实现技术不同:
- 面向B端和C端有APP端
- 面向运营端通常H5实现
所以如果不存在APP端,为了接口不要越来越臃肿,所以面向运营用户BFF层可以考虑删除老字段。
3 技术系统为什么复杂
《为什么需要生物学思维》这本书中提到复杂系统形成的四个原因:
- 吸积
- 交互
- 必须处理的意外情况
- 普遍的稀有事务
3.1 什么是吸积效应
从字面上解读可以将这一过程理解为吸附和积累。如同一粒粒沙子,每次加入一点点,最终汇聚成一座沙山。在软件开发的世界中,无论每一次的代码迭代在表面上看起来多么独立和微不足道,它们都在客观上促使代码量不断增长。
3.2 吸积效应带来哪些挑战
吸积效应导致代码变得如此庞大和复杂,以至于没有人能够全面掌握这个系统。当系统出现问题或故障时,最熟悉这部分代码的人可能早已离职,消失在人海中。
面对这种情况开发人员往往只能添加一段段兼容逻辑,以期降低对原有系统的影响。然而这种做法反而加剧吸积效应。甚至在某些无奈的情况下,团队可能被迫选择容忍某些错误,因为修复这些错误的代价远大于容忍它们所带来的风险。这就是吸积效应在软件开发中带来的巨大挑战。
3.3 怎么应对
技术系统都是往熵增方向发展,从有序发展到无序,所以工程师要通过一些手段减缓这个进程,常见方案是:
- 统一技术架构
- 统一技术规范
- 统一业务语言
- 代码分享与审查
- 技术与业务分享
- 系统稳定性建设