01 背景 最近开发了一个需求,发现一个历史功能,从产品和技术代码的角度看,将简单的事情变得复杂。这一经历再次深化了我对一个核心理念的认识:简化复杂性是产品设计和软件开发中永恒的挑战。我们必须不断努力,将复杂的逻辑转化为直观、易用的用户功能,并将冗长、难以维护的代码结构变为简洁、效率高的形式。
在《人月神话》中作者提到,软件开发的复杂度可以划分为本质复杂度和偶然复杂度。本质复杂度它是一个客观的东西,跟你用的工具、经验或解决渠道都没有任何关系。而偶然复杂度是因为我们在处理任务的时候选错了方向,或者使用了错误的方法。 作为工程师,我们的追求不仅仅局限于代码的编写。更深层次的,我们探索的是如何对抗软件本身产生的复杂度,如何将繁杂的需求转化为简洁、优雅的解决方案。 不单单是程序员,任何化繁为简的能力才是一个人功力深厚的体现,没有之一。越简单,越接近本质。这个“简单”指的是整体的简单,而不是通过局部的复杂让另一个局部简单。
附:需求案例复杂点
1)业务产品设计方面:Promise 业务类型(比如生鲜时效、航空时效、普通中小件时效等)与单据类型、作业类型之间存在一系列复杂的转换关系。但这几个类型本质是一样的,没必要转换,术语统一,对业务使用来说也简单。 2)技术代码方面(组内同学CodeReview发现的):代码方法“副作用”(side effect),即方法除了返回值之外,还通过修改某些外部状态或对象来传递信息。比如filterBusinessType
方法的主要作用是返回一个int
类型的值,但它也修改了入参的response
对象作为一个副作用,外部链路会使用reponse对象属性值。并且代码内部调用链路复杂,对于新人来说成本较高。为了确保清晰理解这些关系,并有效地进行代码维护,特意对这些关系及代码链路进行了详细的梳理。 02 为什么要简单
最近开发了一个需求,发现一个历史功能,从产品和技术代码的角度看,将简单的事情变得复杂。这一经历再次深化了我对一个核心理念的认识:简化复杂性是产品设计和软件开发中永恒的挑战。我们必须不断努力,将复杂的逻辑转化为直观、易用的用户功能,并将冗长、难以维护的代码结构变为简洁、效率高的形式。
不单单是程序员,任何化繁为简的能力才是一个人功力深厚的体现,没有之一。越简单,越接近本质。这个“简单”指的是整体的简单,而不是通过局部的复杂让另一个局部简单。
附:需求案例复杂点
filterBusinessType
方法的主要作用是返回一个int
类型的值,但它也修改了入参的response
对象作为一个副作用,外部链路会使用reponse对象属性值。并且代码内部调用链路复杂,对于新人来说成本较高。为了确保清晰理解这些关系,并有效地进行代码维护,特意对这些关系及代码链路进行了详细的梳理。为什么要简单
为什么我们要追求简单性?不应该是复杂,才能显得技术牛吗?
1.比如你架构图别人一看就明白这是干什么的,系统之间如何交互,模板之间如何交互。
2.比如定义API 别人一看文档就明白功能职责,请求入参,出参含义,有基本的计算机知识人都能看明白,这叫所见即所得。
接下来从本次需求的复杂案例着手,引入自己的一些思考
案例详情
1)产品设计
1.1)现状
1.业务类型 转换单据类型(1 VS 1)
2.单据类型 转换为 作业类型:根据单据类型找到 仓、干支线、本地 作业类型
1.2)思考点
1.一对一映射的简化
2.概念统一
3.维度划分
2)代码问题
2.1)内部链路太长
业务类型逻辑入口之一:getBusinessTypeInfoForAll > getBusinessTypeInfo > getOrderCategoryNew > obtainOrderCategoryByCode > filterBusinessType
在我们的代码库中,上面关键的五个方法被多个调用入口所使用,这种情况使得管理这些入口变得极为棘手。由于调用点的广泛分布,理解代码的影响范围变得复杂,难以一目了然地掌握。此外,这种做法也显露出我们的代码缺乏清晰的分层架构。这一原则的缺失,不仅使得现有代码难以维护,也给未来的功能扩展和迭代带来了不必要的复杂性和风险。
2.2)副作用
在Java 编程语言中,术语“副作用”(side effects) 指的是一个函数或表达式在计算结果以外对程序状态(如修改全局变量、改变输入参数的值、进行I/O 操作等)产生的影响。
如下filterBusinessType
方法的主要作用是返回一个业务类型int
类型的值,但它也修改了传入的response
对象的A值作为一个副作用。在外面链路使用了A属性值做逻辑判断
副作用问题:在filterBusinessType方法中
如果是在response之前return了数据,从方法角度看不出问题,但整个链路会出现问题。
public int filterBusinessType( Request request,Response response) {
if(...){
return ...
}
boolean flag = isXXX(request, response);
}
public int filterBusinessType( Request request,Response response) {
/**
* 切记:return必须在下面这行代码(isXXX方法)后面,因为外面会使用response.A()来判断逻辑
* 你可以理解本filterBusinessType方法会返回业务类型,同时如果isXXX方法会修改response.setA()属性
*/
boolean flag = isXXX(request, response);
if(...){
return ...
}
}
2.3)思考点
1.避免过度抽象:虽然抽象可以帮助简化代码,但过度抽象反而可能会增加复杂性。确保你的抽象层次是合理的,并且每个抽象都有明确的目的和价值。
2.分层架构:将代码按照逻辑和职责分成不同的层次,每一层只对其下一层有依赖性,而不需要知道更深层次的实现细节。这样可以减少代码之间的耦合度,简化内部链路。
3.合并重复代码:如果发现多个方法都在执行类似的操作,尝试合并这些重复的代码段,创建一个通用的方法来处理它们。这样可以减少代码的总量,提高代码的可读性和可维护性。
1.将副作用明确化:如果一个方法有副作用,应该在方法的名称、文档或使用方式中明确指出。这样可以帮助其他开发者更好地理解该方法的行为。
2.避免使用静态变量:静态变量可以被多个线程或方法共享,容易引起副作用问题。除非有明确的理由,否则应尽量避免使用静态变量。
1.单一责任原则:每个方法或类都应该有单一的责任或功能。
filterBusinessType
方法既返回一个整数结果,又更新了response
对象,这违反了单一责任原则和最小惊奇原则。因为调用者可能会预期这个方法只会过滤业务类型,而不清楚它还会修改response
对象。1.分离关注点: 可以将获取业务类型和响应设置分离成两个不同的方法。这样,调用者就可以清晰地看到每个方法的职责。
public int filterBusinessType(String logPrefix, Request request) {
// 过滤逻辑...
int businessType=...;
return businessType;
}
public void setResponseData(int filterResult, Response response) {
// 根据过滤结果设置响应数据...
response.setFilteredData(...);
}
public FilterResultAndResponse filterBusinessType(Request request) {
// 过滤逻辑...
int result=...;
Response response=new Response();
response.setFilteredData(...);
return new FilterResultAndResponse(result, response);
}
class FilterResultAndResponse {
private int filterResult;
private Response response;
public FilterResultAndResponse(int filterResult, Response response) {
this.filterResult = filterResult;
this.response = response;
}
// Getters and setters for filterResult and response
}
1.增加注释和注意事项:在代码中添加详细的注释和注意事项,帮助团队其他开发者理解这部分代码的工作原理和潜在风险。这样可以降低新成员上手的难度,并且减少在维护或扩展时出现错误的可能性。
这两种方法虽然不能立即简化复杂性,但它们可以提高代码的可读性和可维护性,减少长期的技术债务。同时,团队分享也能促进知识共享和团队协作,让大家知道什么是正确的做法,帮助我们在面对类似挑战时做出更明智的决策。
1)产品设计
以用户为中心
深入理解用户需求:深刻洞察用户的核心需求和痛点,用客观数据驱动决策,而不是单凭个人直觉。
简化用户旅程:力求打造一个直观的用户旅程,尽量减轻用户的决策压力和学习负担。一个优秀的界面应该是清晰、易懂的,使用户能够毫不费力地完成所需任务。
减法设计
在设计每一个功能环节时,我们需要反复自问:这个功能是否真正必要?如果它的缺失不会损害用户体验,那它很可能是多余的。
直观交互
设计时应确保控件和操作逻辑能够自然地映射其功能,使用户能够直觉地理解产品的使用方法。
持续迭代
不断地收集用户画像和分析用户反馈,将其作为产品设计迭代的重要依据,以此不断地精进和完善产品。
案例:
1、Alfred:这个我觉得根本无需介绍,神器,使用 macOS 的同学应该都知道。一句话来说就是,Alfred 是 macOS 上神级的效率应用,能够在实际操作中大幅提升工作效率。
2)架构设计
案例1:
业务架构简单化-小件日历天数30天扩充到90天
复杂解法:
1)目前是根据业务的时效配置预计算好30天日历,依赖N个配置(仓-干支线-本地缓存等),在现有基础上,预计算90天日历。
2)缺点:牵扯数据预计算N个地方改造,并且增加了数据量的存储。改造排期长并且数据存储成本高
简单解法:
1)还是保持现有30天日历的算法。第31天以后的日历按照最后一天日历进行复制。如果日历计算命中集约地址(比如3天1送),过滤对应日历即可。
案例2:技术架构简单化-避免过度使用技术栈
为了解决这些问题,我们可能需要引入更复杂的缓存失效策略,如基于时间的失效、事件驱动的失效机制, 这些策略的引入和管理本身就增加了系统的复杂性,因此在设计缓存解决方案时,我们需要仔细权衡其带来的效率提升和潜在的复杂性增加,以找到最适合当前系统需求的平衡点。
3)最小API
对外 API 的设计决定了系统的可扩展性和兼容性。一个清晰、简洁且易于理解的 API 设计可以减少各种交互问题。编写一个明确的、最小的API是管理软件系统简单性的必要条件,我们向API消费者提供的方法和参数越少,这些API就越容易理解,就有更多的时间去完善这些方法.
1.使用标准格式:遵循一致的命名约定、数据格式和错误处理机制。这将使API更加一致和易于使用。
2.API功能单一职责原则:在API设计中,单一职责原则也非常重要。如果一个API具有多个职责,那么它将变得复杂且难以维护。因此,建议将API拆分为多个简单的API,每个API只负责一个特定的职责。明确其功能和用途。这将有助于确保API具有清晰的职责划分,避免不必要的复杂性。
3.简化参数:尽量避免使用过多的参数,而是使用简单、易于理解的参数。如果必须使用多个参数,请确保它们之间有明确的关系。
4.提供简洁的文档:编写简洁明了的API文档,解释每个端点的功能、请求方法、参数和响应格式。确保文档易于阅读和理解,以便用户能够快速上手。
案例1:Promise适配M系统API
背景:M系统的时效是自己计算闭环的,promise是对外统一收口时效,在M系统时效业务线上,promise只是透传,不做任何时效逻辑
复杂解法:
1)每次M系统相关时效需求,下游M系统的API需要变更,promise也需要参与改造,改造点2个,第一个是从订单中间件xml获取M系统需要的参数。第二点把获取的参数调用M系统API透传
2)缺点:需求改造系统多,但都是转发适配,无核心逻辑,工作量耗时长,项目排期协调,沟通成本大
简单解法:
1)跟M系统沟通,M系统时效要的信息从X节点获取,promise把该节点的json信息全部透传给M系统,这样后期需求promise不参与改造;
案例2:
❌错误码设计---未传播错误码
外单无妥投时间,目前链路是A---->B---->C系统。但错误码是各自封装,没有把根本原因传播出去,而是各自加工,导致最终看到的原因跟真实的原因千差万别。导致整个链路牵扯 业务方--->A研发---->B研发---->C研发---->C业务同事 总共5个环节,如下图:
4)代码简单
1.遵循单一职责原则:每个函数或类应该只负责一个特定的任务。这样可以使代码更易于理解和维护,并减少错误的可能性。
2.避免冗余代码:尽量避免重复的代码。如果需要多次使用相同的代码块,请将其封装为函数或方法,以便在需要时调用。
3.使用注释来解释复杂的逻辑:如果代码中包含复杂的逻辑或算法,请使用注释来解释其工作原理。这可以帮助其他人更好地理解代码。
4.将长代码段拆分为多个小段:如果一个代码段很长,可以考虑将其拆分为多个小段,每个小段只做一件事情。这可以使代码更加清晰明了,并有助于调试和维护。
我也是正在积极践行以下原则。虽然在实践中仍面临挑战,但正不断学习和改进
1)复杂(重复)的事情简单(工具)化
2)简单的事情标准化
3)标准的事情流程化
4)流程的事情自动化
案例:
行云部署发布上线 简单提效快
背景:为解决用户手动部署操作耗时高、分组多人工容易遗漏、对人依赖度高等痛点,2个以上分组,20个容器以上的应用,强烈推荐您使用【部署编排】功能,用户可灵活制定部署策略,实现从编译构建到实例部署的自动化运行,提高部署效率!
1.复杂的事情简单化
4.流程的事情自动化
我们先踏出第一步化繁为简
简化复杂性不仅能在短期内提高开发效率和代码质量,也对产品和技术的长期价值产生深远影响
3.软件的简单性是可靠性的前提条件。这样我们可以持续关注创新,并且可以进行真正的有价值的事、长期的事。