当前位置: 首页 > 科技观察

说说那些年我们遇到的精彩代码

时间:2023-03-20 23:31:45 科技观察

简介无论是开发新需求还是维护老平台,我们在工作过程中都会接触到各种风格的代码,有时候也会遇到一些优秀的代码.凛然,但更多的时候我们会遇到很多奇奇怪怪的代码,有时我们会对着一段奇奇怪怪的代码大骂一顿,然后仔细看看作者。几个月前自己写的,脑海里不免浮现曹操的那句话。座右铭:不可能,绝对不可能。很多同学可能会说要求不要太高,只要代码能运行就行。但实际上,代码就是程序员的名片。技术同学不应局限于功能需求的实现,而要有写出高质量代码的追求。那么今天就和大家聊聊我那些年遇到的奇葩代码,看看我以前有没有写过这样的代码,现在还会这样写。妙码奖的名称没有业务语义publicvoidhandleTask(LongtaskId,Intgerstatus){TaskModeltaskModel=taskDomainService.getTaskById(taskId);断言.notNull(taskModel);taskModel.setStatus(status);taskDomainService.preserveTask(taskModel);}或许乍一看这段代码没什么大问题,但是如果你想知道这段代码是干什么的,可能一下子反应不过来,需要看代码逻辑才能知道。通过查看代码,我们知道这里代码的业务语义是改变任务状态,但实际的方法名称是handleTask,命名显然过于宽泛,无法准确表达实际的业务语义。那么为什么要滚动代码来弄清楚方法的含义呢?归根结底,方法命名不够准确,无法完整表达这段代码对应的业务语义。那为什么我们经常不能准确地命名类或方法呢?我觉得最根本的原因是写代码的同学没能准确把握这段代码的业务语义,所以要么名字太宽泛,要么文字没有表达意思。因此,无论是类命名还是方法命名,都必须能够清晰地表达业务语义。只有这样,不管是你回头看一会,还是其他维护人员看代码,光看命名就能清楚地理解代码中包含的业务逻辑。单个方法太长尤其是在一些老项目中,我们经常会遇到几百行代码可以塞进一个方法中。一般来说,这个单一方法的代码过长有两个原因。一种是用过程化思维写代码,所有业务步骤都写在一个方法中;另一种是后期的维护者需要增加新的功能,看到代码这么长,不敢盲目改。只能继续在long的方法里码代码,让方法变得更长更难维护。无论是从后期代码的可维护性,还是从SRP的设计原则,单个方法的代码行数最好不要超过100行,否则后果就是各种业务逻辑混在一起,不仅是同学以后维护代码的人不会很容易理解其中包含的业务语义,如果功能发生变化,修改起来会更费力。publicvoidshelveFreshGoods(){//查货//几十行代码(查重、查新鲜度等)//CargoFerry//几十行代码(生成货号、载重等)//上架//几十行代码(标记商品,绑定库存等)...}比如生鲜上架的逻辑,可以看出,当生鲜上架时,他们将经过多次检查、摆渡、上架。步骤,但在这个shelveFreshGoods方法中,这些业务步骤混合在一起。如果我们要修改或者增加业务逻辑,只需要在这个长方法中修改这个方法,这样可能会导致方法变得更加复杂。越来越长。但是,如果通过拆分的方式来划分业务子流程,也就是将上述步骤封装成方法。那么修改某个业务逻辑就可以直接在拆解对应的步骤中进行,这样就减少了修改的范围,业务逻辑看起来一目了然。publicvoidshelveFreshGoods(){//查询商品check();//货物轮渡转运();//搁置搁置();}业务数据循环插入在开发业务代码时,批量插入业务数据CRUD基本操作是很常见的。但是有些同学在写批量插入接口的时候会这样写,通过for循环或者stream来写循环数据。这种写法会无故增加服务与数据库的交互次数,占用不必要的数据库连接,容易遇到性能问题。如果一次插入的数据不多(几条数据),影响不大,但如果数据量增加,肯定会成为性能瓶颈。for(TaskPOtaskPO:taskPOList){saveTask(taskPO);}显然可以看出,原来的写法需要多次与数据库交互。优化后的写法只需要和数据库交互一次。其实我们可以在mapper文件中进行批量插入进行优化,这样其实通过批量插入的SQL语句,服务和数据库只需要交互一次就可以完成数据的批量存储。insertintotask(c_id,c_name,c_type,c_content,c_operator,i_container_type,c_warehouse_type)values(#{item.id},#{item.type},#{item.content},#{item.operator},#{item.containerType},#{item.warehouseType})先查数据再更新数据库。在写业务代码的时候,经常会遇到这样的场景。如果数据库中有数据,则更新它。如果没有数据,则直接Insert。我们来看看下面的写法。先从数据库中查询数据,存在则更新,不存在则插入数据。有两个数据库交互操作。任务task=taskBizService.queryTaskByName();if(Objects.isNull(task)){taskBizService.saveTask();//省略参数}taskBizService.updateTask();//省略参数其实可以通过数据库的sql直接控制有数据就更新,不存在就插入,避免与数据库的多次交互。insertintotask(c_id,c_name,c_type,c_content,c_operator,i_container_type,c_warehouse_type)values(#{name},#{type},#{content},#{operator},#{containerType},#{warehouseType})onconflict(c_name)doupdatesetc_cnotallow=#{content}业务依赖技术细节先来看一下RobertC.Martin提出的依赖倒置原则是如何描述的:High-levelmodulesshouldnotdependedonlow-level模块。两者都应该依赖于抽象。高级模块不应该依赖于低级模块,两者都应该依赖于抽象。RobertC.MartinAbstractions不应该依赖于细节。细节(具体实现)应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。RobertC.Martin的这两句话听上去有点不清楚,我们不妨结合具体的业务场景更好地理解。假设在一个监控告警平台中,如果线上平台出现问题,比如调用订单生成接口失败,导致无法生成订单。平台检测到此类异常后,需要通知研发同学排查定位问题。此时监控告警平台会将告警信息发送到钉钉群进行通知。因此,我们需要一个发送钉钉消息的接口,如下图。publicbooleansendDingTalk(Messagemessage){...}代码好像没什么问题。如果有报警,则调用发送钉钉消息的接口方法。但实际上,这样的写法违反了依赖倒置的设计原则。为什么这么说,试想一下,如果有一天公司决定不使用钉钉接收报警信息,而是使用微信或者自己公司的通讯软件。那么这里的sendDingTalk就要修改了,因为我们的告警通知业务依赖于发送消息通知的具体实现细节,这显然是不合理的。因此,这里更好的做法是定义一个notifyMessage接口。上层不需要关心具体的实现细节。不管是通过钉钉通知还是企业微信通知,只要实现了通知接口就OK了。即使后面进行切换,也不需要修改原有的业务逻辑,只需要修改具体通知接口的实现即可。publicinterfaceNotifyMessage{booleannotifyMessage(Messagemessage);}publicclassDingTalkimplementsNotifyMessage{@OverridepublicbooleannotifyMessage(Messagemessage){...}}publicclassWeChatimplementsNotifyMessage{@OverridepublicbooleannotifyMessage(Message){..}}一个长SQL程序员在接手一个项目时,最怕遇到的就是项目中几百行的长SQL。这些长SQL有的有各种嵌套查询,甚至有三四级子查询;有些包括四个或五个左连接,内部连接连接五个或六个表。这些长长的SQL在电脑屏幕上放不下,看来放不下的是写这条长SQL的同学的“天赋”。更让人无语的是,如果写这个SQL的同学离职了,一般的查询逻辑也没人问你。.有的同学可能会说,我不想写长SQL,但是数据分散在各个表中,业务逻辑比较复杂,只能加入各种子查询,不知不觉就写了长SQL。但实际上,长SQL并不能解决上述数据分散、业务复杂的问题。反而带来后期维护不善等各种问题。从表面上看,长SQL是对数据库的操作,但在数据库引擎层面,长SQL还是被分成了多个子操作,每个子操作完成后,会统一返回结果数据。那么如何避免写出这么长的可维护性差的SQL呢?对于一些查询场景比较多的长SQL,可以尝试使用宽表来携带需要展示的各个字段的数据,这样查询页面的时候直接在宽表上查询就可以了,不需要结合各种业务用于查询的数据,或者一些长的SQL拆分成多个视图和存储过程,以简化SQL的复杂性。接口参数过多的问题在实际项目开发中经常遇到。当你需要调整一个别人封装好的接口时,对方突然抛出一个包含七八个参数的方法。我想你当时的心情应该是想深深的想对他说,你真的被Q绑了。其实对于一个方法的参数,建议参数个数最多不要超过5个。IntegerpreserveTask(StringtaskId,StringtaskName,StringtaskType,StringtaskContent,Stringoperator,IntegercontainerType,StringwarehouseType);其实我们可以使用模型对象来进行参数封装,这样可以避免方法中参数过多导致的后期维护困难。因为随着业务的发展,有可能修改接口能力以满足新的需求,但是此时如果接口参数发生变化,则需要修改相应的接口和实现类。万一这个接口在别处被调用,那么需要修改的地方就会更多,这显然不符合OCP的设计原则。所以此时如果使用对象作为方法参数,无论是增减参数只需要修改参数对象即可,不需要修改对应??方法的接口参数,这样界面将更具可扩展性。所以,我们在写代码的时候,不能只着眼于当下,还要考虑当相应的需求发生变化时,我的代码如何适应这种变化,尽量减少修改。以后不管是自己维护还是其他同学维护,都会方便一些。让它更容易。整数preserveTask(TaskDOtaskDO);重复代码我之前写过一篇文章介绍如何剔除系统重复代码,具体可以参考以下:如何优雅地剔除系统重复代码常用代码优化写法尝试复用工具函数集判断日常开发我们经常遇到判断数据集非空的逻辑。常用的写法如下。虽然没有问题,但是看起来很不顺畅。简而言之,它不够直截了当。你必须一眼就反应过来。if(null!=taskList&&!taskList.isEmpty()){//业务逻辑}但是通过封装的工具类直接判断,所见即所得,清晰表达集合校验逻辑。if(CollectionUtils.isNotEmpty(taskList)){//业务逻辑}布尔值转换在某些场景下,我们需要将布尔值转换为1或0,所以常用如下代码:if(switcher){return1;}else{return0;}其实可以借助工具和方法简化为如下代码:returnBooleanUtils.toInteger(switcher);lambda表达式简化集合最常见的场景是过滤数据,过滤掉符合条件的对象。代码如下:ListoldStudents=newArrayList();for(Studentstudent:studentList){if(student.getAge()>18){oldStudents.add(student);}}其实我们可以使用lambda表达式来简化代码:Optionalreduceif判断假设我们要获取任务名称,如果不是,则返回unDefined,传统的写法可能是这样的,里面包含了多个if判断,显得有点啰嗦,不够简洁。publicStringgetTaskName(Tasktask){if(Objects.nonNull(task)){Stringname=task.getName();如果(StringUtils.isEmpty(名称)){返回“未定义”;}返回名称;}返回“未定义”;当我们尝试使用Optional来简化和优化代码后,是不是立马就简单多了呢?publicStringgetTaskName(Tasktask){returnOptional.ofNullable(task).map(p->p.getName()).orElse("unDefined");}总结本文主要和大家聊聊日常工作中比较常见的任务当然,吐槽稀奇古怪的代码不是目的。真正的目的是让研发同学能够识别出怪异的代码并进行优化,在实际开发项目中尽量避免编写这些代码。不知道大家在工作中有没有遇到过类似的奇葩代码,或者写过一些奇葩的代码,现在回过头来看看。如果有,欢迎在评论区讨论交流。