代码的艺术-Writing Code Like a Pianist | 京东云技术团队

前言

如何评定一个系统的质量?什么样的系统或者软件可以称之为高质量?可以从三个角度来看,一是架构设计,例如技术选型、分布式系统中的数据一致性考虑等,二是项目管理,无论是敏捷开发还是瀑布式开发,都应当对技术负债进行清理,对代码进行重构等,最后离不开的是代码质量,代码质量的高低直接影响系统的可维护性和可扩展性。好比一辆汽车,内饰高级,外观漂亮,但是底盘不行,动力孱弱,也难以称得上是一辆好车。本文将从主观和客观的角度,和大家探讨一下,作为程序员,应该如何写出整洁高质量的代码。

主观角度

工程师精神

点开京ME头像可以看到,咱们内部的title是“xx开发工程师”,而不是“xx代码编写员”,这个师可以理解为大师的师(master),大师级程序员把系统当作故事来讲,而不是当作程序来写。作为开发者,应该对自己写下的代码负责,当在一个类上@author冠名的时候,应当有一种成就感,在未来的某一天,可能一年,两年甚至五年之后,其他同事读到这段代码,会由衷的发出感叹“牛B”,而不是吐槽“写的什么玩意”,Doug Lea写的并发包,时隔多年,他的大名依然如雷贯耳。

提高代码可读性

首先应当达成共识的是,代码是写给人看的,给机器运行那只是基础,优秀的代码,不需要过多的注释,代码本身就是注释。可读性指的是,其他开发人员能够迅速理解代码的意图和功能,简洁的代码更易于理解、测试和维护。其次,避免重复,代码重复是软件中一切邪恶的根源,许多原则和设计模式都是为了消除重复而创建。

客观角度

接下来从客观角度,例举日常代码评审中的常见问题,给大家作一些参考。

try/catch使用

1. try/catch块应该尽量的小,只针对无法预料的情况进行try/catch,可以预见的异常情况应该在try之前被校验住。

无法预料的情况:远程调用,IO读写,第三方API等。 可以预见的情况:某个参数为空,某个列表无元素等。

try/catch块小有什么好处?

  • 代码整洁,可以减少其他代码的缩进
  • 异常控制精细,可以对具体异常进行具体的提示、UMP报警、日志输出或者错误码返回

2. 如果确实需要对整段方法进行tryCatch,不如新写一个方法,将业务逻辑处理和异常处理区分开来。

例如:

try/catch代码块中的内容多,说明对代码掌控能力不够好 如果存在try,那么尽量保证try在第一行,并且在catch/finally后不该有其他内容

方法长度

方法行数应该尽可能的短,越短越好。能够让读者点进来的一瞬间就明白这个方法做了什么事情。举个例子:

这是一个查询价格的方法,这个方法长度截图已经一页截不下了。如果要想搞明白这个方法做了什么事,凭借方法体中的注释是没法轻易梳理清楚。

只需要做简单的一些处理的包装,这个方法就能变得通俗易懂,而且并不用写一行注释。

简单重构后:

从上面的代码上看,第一段将价格查询的代码分成了两个部分,首先解析查询的审批状态,然后根据审批状态分别查询价格。第二段将价格查询分成了两个部分,一个是查询审批中的价格,另一个是查询审批通过的价格,最后将二者组合起来返回。第三段是截了审批中价格查询的代码,显而易见地,查询审批中价格有两种模式,百分比模式和底价模式,查询到结果后,组合返回即得到商品的渠道价。

对比一下不难看出,原代码块篇幅过长,嵌套较深(for循环嵌套if再嵌套if),导致可读性下降。如果我想改动某块的代码,都需要深思熟虑。但是在拆分之后,就可以快速定位到目标代码块,并且不用担心对其他方法产生影响,另外,对其进行单元测试也会比较方便编写测试用例。

禁止在方法内部修改入参数据

从系统概念上来看,一般可以将系统分为两类,一类是给一个输入,得到一个输出,这类称之为响应式,另一类是给一个输入,但是没有输出,这类被认为是命令式。同理,对于方法而言,响应式方法即给定一个入参,得到一个出参,比较常见的比如模型转换、数据查询等,命令式方法有发送消息、run一个线程。

但是无论怎么变化,这些都有一个共同点,他们不会去修改入参的数据

  1. 引起意外的副作用:如果方法修改了传入的参数,而调用者依赖于原始的参数值,那么调用者可能会在不知情的情况下受到影响。这可能导致程序的行为变得难以预测,增加代码的复杂性和错误的可能性。

  2. 破坏数据的一致性:如果多个方法共享同一个对象作为参数,并且这些方法都可以修改该对象,这些方法可能会相互干扰,导致数据状态变得混乱和不可预测。

  3. 可能增加代码的维护难度:当方法修改传入的参数时,调用该方法的代码可能需要依赖于这种修改行为。这样会增加代码的耦合性,使得代码更难以理解、维护和重用。当需要对方法进行修改时,可能需要同时修改调用该方法的所有地方,增加了代码的维护成本。

举个例子:

这里调用了3个set方法,但是谁能一眼看出来,它把值set到哪个object里了?还得挨个点进去看看具体实现,才知道在哪里赋值了。

如果确实需要修改参数的值,可以创建一个新的对象来存储修改后的结果,并将其返回给调用者。这样可以保持代码的清晰性、可维护性和可测试性,减少意外的副作用和数据一致性问题。

方法参数

方法的参数也是越少越好,最理想的参数是无参,其次是单参,应该尽量避免三个或者三个以上的参数

继续上面的代码,最后一行“设置渠道类型”,set方法里有两个入参,完全可以将其赋值对象提取出来,减少一个入参,如下:

如果方法的处理逻辑,只依赖于某一局部变量object的数据,那么完全可以将这个方法放在对象内部,进一步减少方法参数,提高代码复用性,例如:

这样来看,就成功将两个入参的方法,缩减成了无参方法,和原始代码相比,原始代码方法的作用域仅限于类内部,而最终的无参方法,只需要拿到目标对象实例即可在任何地方调用,还能减少重复代码的编写。

减少缩进

代码中出现大量的缩进显然是影响阅读的,在可能的情况下,我们应该尽可能的减少代码中的缩进和层次。好的代码读起来应该是和报纸一样,排版整齐优雅,而不是爬楼梯。

来看看下面的代码:

代码中出现两层if嵌套,首先我们可以对第一层if嵌套做一个简化,简单将if的判断条件做一个反转得到下面代码:

这样就减少了一层缩进,然后再将变量和参数进行一些微调:

这样,和之前的代码比起来,是不是可读性提高了?

单一职责

在面向对象编程中,“单一职责原则”(Single Responsibility Principle,SRP)是指一个类或模块应该只有一个引起它变化的原因,即一个类或模块应该只有一个主要责任或目标。通过细化功能和职责,可以提高代码的可维护性、复用性和可扩展性。

看下面的代码:

这个方法是从声明上来看,是用于保存任务数据,但是实际上如果入参中的某些数据不为空(实际上看这个方法的实现也很难一眼看出来具体是哪些数据),会进行更新update操作。这种写法不仅容易让读者感到困惑,也会降低代码的可扩展性。

Q:AtomicInteger的compareAndSet方法违反了单一职责吗? A:并不违反单一职责原则。compareAndSet 方法是 AtomicInteger 类提供的一种原子操作,用于比较当前值与给定期望值是否相等,如果相等则将当前值设置为新值。这个方法的职责是实现原子性的比较和设置操作,确保在多线程环境下的线程安全性。compareAndSet 方法作为其中的一种操作,是为了满足特定的需求而设计的,它并不违反单一职责原则。单一职责原则要求一个类或模块只有一个主要责任,而 AtomicInteger 类的主要责任是提供原子整数操作,compareAndSet 方法是其中的一部分,属于该类主要责任的一部分。因此,compareAndSet 方法并不违反单一职责原则。

使用意义明确的命名

下面的代码,checkParam的返回值是布尔值,但是作为读者,我不知道是应该check通过了返回True,还是check失败的情况下返回True。

一般而言,check作为开头的方法名称,没有返回值,对于check不通过的情况以异常的形式抛出。返回布尔值的方法通常以is作为起始,换种写法看看是不是会好读很多?

使用有意义的命名

这里比较典型的是flow编排文件里的条件判断节点,使用0,1这类没有意义的数字,很难一眼看出来说了什么,需要挨个点进去看逻辑,才能梳理明白,改成有意义的命名,便于理解。

类、方法命名规则

类名使用名词或者名词短语 方法命名使用动词或者动名词结构

这个比较好理解,暂不举例了。

不要留无关内容

在发起MR之前,检查一遍代码里有没有“被注释掉的代码”以及TODO。

对于注释掉的代码而言,如果后续有其他开发人员介入,他们会觉得非常困惑,这两行代码为什么要注释掉?它们重要吗?它们放在那里,是因为在未来的某一天需要用?还是说只是当时忘记删除了?还是说为了给未来修改做提示?这些顾虑可能让其他开发人员也不敢动手清理这些被注释的代码,进而造成这些代码被永久的保留下来,形成“幽灵代码”。

对于TODO来说,建议在写todo的时候,要求在后面跟上自己的ERP,谁来解决,怎么解决,deadLine是什么时候。因为在大多数情况下,Later equals never。久而久之,连自己都忘了这个todo,忘了当时为啥写这个todo,应该怎么处理,给系统埋下隐患。

如上,当年写下这个TODO的同事已经离职了,现在只有上帝才知道todo什么东西。

枚举

养成良好的习惯,在用枚举定义的变量,拉一条引用指向枚举,方便其他开发人员阅读。

例如:

这是一个可穷举的变量(因为状态是有限的),但是读者不知道具体都有哪些状态。这里其实是有一个枚举关联上来的.

为了方便阅读,可以在变量定义的地方,使用javaDoc的@see/@link方式,说明这个变量的枚举范围。

单元测试

单测的重要性不言而喻,这里先挖个坑,后续单开一篇写。

结语

写在最后,给大家推荐一本经典书籍《代码整洁之道》,实际上该书的出版时间较早,书中的某些知识或许有些过时,但是前面几章内容仍然值得一读。整洁的代码需要团队的共同努力,团队成员应该遵循一致的编码风格和标准,进行代码审查和知识分享,共同维护和改进代码质量。它不仅仅是一种编码风格,更是一种思维方式和价值观。优雅的代码,就像是艺术品,正如标题所言,应当像一名钢琴家一样编写代码。

作者:京东零售 谭磊

来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/162136.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【SpringCloud微服务项目实战-mall4cloud项目(4)】——mall4cloud-rbac

mall4cloud-rbac角色权限访问控制模块 系统架构与模块介绍系统架构rbac模型介绍 相关代码权限校验接口代码 补充 代码地址 github地址 fork自github原始项目 gitee地址 fork自gitee原始项目 系统架构与模块介绍 系统架构 从图中可以看到,微服务集群中,…

基于Qt QSlider滑动条小项目

QSlider 是滑动条控件,滑动条可以在一个范围内拖动,并将其位置转换为整数 1. 属性和方法 QSlider 继承自 QAbstractSlider,它的绝大多数属性都是从 QAbstractSlider 继承而来的。 2.QSlider信号 - `valueChanged(int value)`: 当滑块的值改变时发出信号,传递当前滑块的值…

基于SVM+Webdriver的智能NBA常规赛与季后赛结果预测系统——机器学习算法应用(含python、ipynb工程源码)+所有数据集(三)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 特征提取3. 模型训练及评估1)常规赛预测模型2)季后赛模型创建 4. 模型训练准确率 相关其它博客工程源代码下载其它资料下载 前言 本项目使用了从NBA官方网站获得的数据&…

JOSEF约瑟 漏电继电器 JHOK-ZBG1 φ25mm AC220V 0.1A/0.1S 分体式

系列型号 JHOK-ZBG1 φ25mm漏电(剩余)继电器 JHOK-ZBG2 φ25mm漏电(剩余)继电器 JHOK-ZBG1 φ45mm漏电(剩余)继电器 JHOK-ZBG2 φ45mm 漏电(剩余)继电器 JHOK-ZBG1 φ100mm漏电&a…

猜数字游戏(Rust实现)

文章目录 游戏说明游戏效果展示游戏代码游戏代码详解生成神秘数字读取用户输入解析用户输入进行猜测比较 游戏说明 游戏说明 游戏运行逻辑如下: 随机生成一个1-100的数字作为神秘数字,并提示玩家进行猜测。如果玩家猜测的数字小于神秘数字,则…

初始web项目tomcat部署报错404

问题 简单地创建了一个web项目,结果一运行就404咧,真滴烦。。。接下来的项目也没法继续了 问题原因:缺少文件 其实造成这样问题的原因有不少,但在这里我是踩了一个坑。在出问题之前,我运行的其他项目都是可以跑的&…

2. 验证1101序列(Mealy)

题目要求: 用 M e a l y \rm Mealy Mealy型状态机验证 1101 1101 1101序列 题目描述: 使用状态机验证 1101 1101 1101序列,注意:允许重复子序列。 方法一: 去掉 M o o r e \rm Moore Moore的 s 4 s_4 s4​&#xff…

掌握.NET基础知识(一)

前言 本文将讲解一些.NET基础。NET基础是指在计算机编程中使用.NET框架所需要的基础知识..NET的认识: .NET是由微软开发的一个跨平台的应用程序开发框架。它包括一个运行时环境和一个面向对象的程序库,可以用于开发各种类型的应用程序,包括桌面应用程序、…

数据驱动娱乐的未来:揭秘爱奇艺如何利用Apache Spark实现个性化推荐和内容分析

Apache Spark 在爱奇艺的现状与应用 Apache Spark 是爱奇艺大数据平台的核心组件,被广泛应用于数据处理、数据同步和数据查询分析等关键场景。在数据处理方面,爱奇艺通过数据开发平台支持开发者提交 Spark Jar 包任务或 Spark SQL 任务,进行…

【JVM系列】- 类加载子系统与加载过程

类加载子系统与加载过程 😄生命不息,写作不止 🔥 继续踏上学习之路,学之分享笔记 👊 总有一天我也能像各位大佬一样 🏆 博客首页 怒放吧德德 To记录领地 🌝分享学习心得,欢迎指正…

25.1 MySQL SELECT语句

1. SQL概述 1.1 SQL背景知识 1946年, 世界上诞生了第一台电脑, 而今借由这台电脑的发展, 互联网已经成为一个独立的世界. 在过去几十年里, 许多技术和产业在互联网的舞台上兴衰交替. 然而, 有一门技术却从未消失, 甚至日益强大, 那就是SQL.SQL(Structured Query Language&…

进化策略算法

前言 进化策略 (Evolution Strategy) 后面都简称 ES,其本质就是:种群通过交叉产生后代,我们只保留较好的父代和子代,一直这样迭代下去, 我们的保留方式是: 父代产生后代,然后将后代DNA和原来的…

02_单片机及开发板介绍

单片机简介 单片机,又称为微控制器(Microcontroller),是一种集成了微处理器核心、存储器、输入/输出接口及各种功能模块的集成电路芯片。它通常由中央处理器(CPU)、存储器、输入/输出接口以及各种外设组成&…

【Leetcode】 707. 设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。 单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。 如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点…

保序回归与金融时序数据

保序回归在回归问题中的作用是通过拟合一个单调递增或递减的函数,来保持数据点的相对顺序特性。 一、保序回归的作用 主要用于以下情况: 1. 有序数据:当输入数据具有特定的顺序关系时,保序回归可以帮助保持这种顺序关系。例如&…

rust学习—— 控制流if 表达式

控制流 根据条件是否为真来决定是否执行某些代码,或根据条件是否为真来重复运行一段代码,是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 if 表达式和循环。 if 表达式 if 表达式允许根据条件执行不同的代码分支。你提供…

vue-cli脚手架创建项目时报错Error: command failed: npm install --loglevel error

项目背景 环境:vue-cli 5.x 在工程文件中,后端模块wms已经创建完成,现在想新建一个名为vue-web的前端模块 执行命令vue create vue-web时, 报错Error: command failed: npm install --loglevel error 问题分析及解决 排查过程…

Linux性能优化--使用性能工具发现问题

9.0 概述 本章主要介绍综合运用之前提出的性能工具来缩小性能问题产生原因的范围。阅读本章后,你将能够: 启动行为异常的系统,使用Linux性能工具追踪行为异常的内核函数或应用程序。启动行为异常的应用程序,使用Linux性能工具追…

centos 7.9 源码安装htop

1.下载源码 wget http://sourceforge.net/projects/htop/files/latest/download 2.上传到tmp目录,并解压 tar xvzf htop-1.0.2.tar.gz mv htop-1.0.2 /opt/ 进入到 cd /opt/htop-1.0.2/ 3.编译并安装 ./configure && make && make install 4.…

基于混沌博弈优化的BP神经网络(分类应用) - 附代码

基于混沌博弈优化的BP神经网络(分类应用) - 附代码 文章目录 基于混沌博弈优化的BP神经网络(分类应用) - 附代码1.鸢尾花iris数据介绍2.数据集整理3.混沌博弈优化BP神经网络3.1 BP神经网络参数设置3.2 混沌博弈算法应用 4.测试结果…