简洁至上——探索产品与技术的优雅原则
背景
上周开发了一个需求,发现一个历史功能,从产品和技术代码的角度看,将简单的事情变得复杂。这一经历再次深化了我对一个核心理念的认识:简化复杂性是产品设计和软件开发中永恒的挑战。我们必须不断努力,将复杂的逻辑转化为直观、易用的用户功能,并将冗长、难以维护的代码结构变为简洁、效率高的形式。
在《人月神话》中作者提到,软件开发的复杂度可以划分为本质复杂度和偶然复杂度。本质复杂度它是一个客观的东西,跟你用的工具、经验或解决渠道都没有任何关系。而偶然复杂度是因为我们在处理任务的时候选错了方向,或者使用了错误的方法。
作为工程师,我们的追求不仅仅局限于代码的编写。更深层次的,我们探索的是如何对抗软件本身产生的复杂度,如何将繁杂的需求转化为简洁、优雅的解决方案。
不单单是程序员,任何化繁为简的能力才是一个人功力深厚的体现,没有之一。越简单,越接近本质。这个“简单”指的是整体的简单,而不是通过局部的复杂让另一个局部简单。
附:需求案例复杂点 1)业务产品设计方面:Promise 业务类型(比如生鲜时效、航空时效、普通中小件时效等)与单据类型、作业类型之间存在一系列复杂的转换关系。但这几个类型本质是一样的,没必要转换,术语统一,对业务使用来说也简单。 2)技术代码方面(组内同学CodeReview发现的):代码方法“副作用”(side effect),即方法除了返回值之外,还通过修改某些外部状态或对象来传递信息。比如filterBusinessType方法的主要作用是返回一个int类型的值,但它也修改了入参的response对象作为一个副作用,外部链路会使用reponse对象属性值。并且代码内部调用链路复杂,对于新人来说成本较高。为了确保清晰理解这些关系,并有效地进行代码维护,特意对这些关系及代码链路进行了详细的梳理。
一、为什么要简单?
为什么我们要追求简单性?不应该是复杂,才能显得技术牛吗?
对应简单,各有个的说法,个人理解如下:所见即所得
接下来从本次需求的复杂案例着手,引入自己的一些思考
二、案例详细
1)产品设计
1.1)现状
Promise业务类型 > 单据类型 > 作业类型 各种转换关系,如下图

根据单据类型找到 仓、干支线、本地 作业类型

仓库

干支线

本地

1.2)思考点
“业务类型&单据类型 其实是1V1映射”,这表明系统当初设计时考虑了业务类型和单据类型之间的直接关联。如果这种一对一的关系确实存在,那么单据类型可能是一个冗余的概念,因为每个业务类型已经隐含了单据类型的信息。简化模型,去除冗余的单据类型,可以减少系统的复杂性,并可能简化数据库设计和代码实现。
“作业类型本质还是业务类型”,这意味着在不同的上下文中可能使用了不同的术语来描述相同的概念。在编码和产品设计中,使用统一的术语可以减少混淆,提高团队成员之间的沟通效率,并使得新成员更容易理解系统。
将业务类型进一步细分为“仓、干支线、本地维度”,这表明系统有不同的操作维度或者分类标准。这种维度划分有助于在不同层面上组织和处理业务逻辑。
2)代码问题
2.1)内部链路太长
业务类型逻辑入口之一:getBusinessTypeInfoForAll > getBusinessTypeInfo > getOrderCategoryNew > obtainOrderCategoryByCode > filterBusinessType

在我们的代码库中,上面关键的五个方法被多个调用入口所使用,这种情况使得管理这些入口变得极为棘手。由于调用点的广泛分布,理解代码的影响范围变得复杂,难以一目了然地掌握。此外,这种做法也显露出我们的代码缺乏清晰的分层架构。这一原则的缺失,不仅使得现有代码难以维护,也给未来的功能扩展和迭代带来了不必要的复杂性和风险。
2.2)副作用
在Java 编程语言中,术语“副作用”(side effects) 指的是一个函数或表达式在计算结果以外对程序状态(如修改全局变量、改变输入参数的值、进行I/O 操作等)产生的影响。
如下filterBusinessType方法的主要作用是返回一个业务类型int类型的值,但它也修改了传入的response对象的bulkOrder值作为一个副作用。在外面链路使用了bulkOrder属性值做逻辑判断
副作用问题:在filterBusinessType方法中如果是在response之前return了数据,从方法角度看不出问题,但整个链路会出现问题。
❌错误写法
public int filterBusinessType(String logPrefix, OrderCategoryRequest request, OrderCategoryResponse response) {
if(CommonSwitch.isSendPay362Switch()){
if(OrderInfoFilterUtils.sendPay362is3or4(sendpay)) {
log.info("{} 业务类型=1(普通订单),命中医药低温时效,当sendpay362=3|4时", logPrefix);
return BusinessSchemaEnum.forwardOrder.getBussinessType();
}
}
boolean bulkOrderFlag = isBulkOrder(logPrefix, dictId, storeId, request.getSkuQuantity(), request.getWeight(), response);
}
✅正确写法
public int filterBusinessType(String logPrefix, OrderCategoryRequest request, OrderCategoryResponse response) {
/**
* 切记:return必须在下面这行代码(判断是否满足大宗)后面,因为外面的getOrderCategoryNew
* 方法会使用response.isBulkOrder()来判断是否大宗
* 你可以理解本filterBusinessType方法会返回业务类型,同时如果bulkOrderFlag是大宗为true,则入参response也需要赋值返回 response.setBulkOrder(true)
*/
boolean bulkOrderFlag = isBulkOrder(logPrefix, dictId, storeId, request.getSkuQuantity(), request.getWeight(), response);
if(CommonSwitch.isSendPay362Switch()){
if(OrderInfoFilterUtils.sendPay362is3or4(sendpay)) {
log.info("{} 业务类型=1(普通订单),命中医药低温时效,当sendpay362=3|4时", logPrefix);
return BusinessSchemaEnum.forwardOrder.getBussinessType();
}
}
}
详细代码链路如下:
//入口方法1
public OrderCategoryResponse getOrderCategoryNew(OrderCategoryRequest request) {
response = obtainOrderCategoryByCode(request);
//关注这行代码,使用了response.isBulkOrder()值
int promiseSchemeType = getPromiseSchemeType(request.getLogPrefix(), false,
response.isBulkOrder(), response.getBusinessId());
}
//方法2
private OrderCategoryResponse obtainOrderCategoryByCode(OrderCategoryRequest request) {
filterBusinessType(logPrefix, request, response);
}
//方法3
public int filterBusinessType(String logPrefix, OrderCategoryRequest request, OrderCategoryResponse response) {
/**
* 切记:return必须在下面这行代码(判断是否满足大宗)后面,因为外面的getOrderCategoryNew方法会使用response.isBulkOrder()来判断是否大宗
* 你可以理解本filterBusinessType方法会返回业务类型,同时如果bulkOrderFlag是大宗为true,则入参response也需要赋值返回 response.setBulkOrder(true)
*/
boolean bulkOrderFlag = isBulkOrder(logPrefix, dictId, storeId, request.getSkuQuantity