代码可读性之道
1 代码可读性
程序员常常调侃自己,失业了就去送外卖、搬砖,虽然是个玩笑话,但是也直击了问题的本质。在软件开发的工地上,写代码的程序员真的等于搬砖工人。
职能 | 建筑工程 | 软件工程 |
---|---|---|
设计 | 建筑设计师 | 架构师、团队负责人 |
管理 | 建造工程师、造价工程师、项目经理、施工员 | 团队负责人 |
施工 | 架子工、抹灰工、砌筑工、混凝土工等 | 程序员 |
质量 | 监理工程师 | 软件测试 |
后勤 | 食堂、宿舍 | 系统运维 |
建筑行业的标准非常多,比如房屋建筑制图标准、硅酸盐水泥国家标准、建筑用钢筋国家标准等等。如果不遵守这些标准,工地无法展开工作或者建筑质量低下。与建筑工程不同是,软件是智力产品,可以反复迭代升级。
代码也是团队的沟通方式之一。试想,其他同事接手你负责的功能,如果代码结构清晰、注释合理,他就不会频繁打断你的工作、询问代码中的疑点。编写代码的时候,首先要考虑到别人的阅读感受,而不是你自己。在实际的开发工作中,最花费时间的事情是理解当前代码以及上下游代码逻辑。如果需要变更代码,代码可读性越差,花的时间越长,这个模块的可维护性就越低。
代码的可读性到底怎么定义呢?维护者很容易看出代码的意图,代码的真实逻辑按照维护者的直观感受来执行。
2 合适的命名
起名字是头等大事,金庸绝对是起名的高手。《天龙八部》里有“南慕容北乔峰”,以乔峰为例,这个“峰”字听上去伟岸、有英雄气,如果叫乔发、乔雄、乔布斯,就像个暴发户,不像大英雄。再说慕容复,复姓“慕容”非常文雅,长得也是翩翩公子,为了“复”国走上歧路,最后众叛亲离。
好的命名能够自己表达意图,降低阅读者的心智负担。编程语言都是以英文为基础,但是要想起好名字,不光要英语好,还要深刻的理解业务逻辑,才能做到“信、达、雅”。“信”指的是准确,不能偏离,也不可以遗漏;“达”指的是不拘泥细节,用词通顺清晰;“雅”指的是选用的词语要得体。
2.1 确定领域词汇
软件系统用来支撑特定领域的业务,每个领域都有专业词汇。在项目开发初期,通过宣讲或者文档的方式确定业务领域词汇,有助于提高代码可读性。以电商领域为例,通常会出现下面的词汇:
英文 | 全称 | 解释 |
---|---|---|
order | order | 订单 |
product | product | 商品 |
sku | Stock Keeping Unit | 最小存货单位 |
payment | payment | 支付 |
遇到不确定的词,不要闭门造车,要抛出来集体讨论确认,比如互金领域有个“分期”的业务,对应的英文有“stage”、“installment”。如果集体决定用“installment”,就固定下来,不准再使用“stage”。
2.2 遵守约定俗成
任何编程语言都有俗成的命名约定或者单词缩写,比如循环计数器可以被命名为i、j或k,以Java语言为例:
- 驼峰命名:名称由多个单词构成,从第二个单词开始,所有的单词首字母必须大写,如:firstName、ActionEvent、ActionListener
- 类名称:以大写字母开头,包含名词,如: Color、Button、System、Thread
- 接口名称:以大写字母开头,包含形容词,如: Runnable、Remote、ActionListener
- 方法名称:以小写字母开头,包含动词,如:actionPerformed()、main()、print()
- 常量名称: 全部大写字母,如RED、YELLOW、MAX_PRIORITY
- 常用缩写:document 缩写 doc ; string 缩写 str ;text 缩写 txt
2.3 附带额外信息
附加一些额外信息可以提升命名的精度,通常从单位、属性、格式三个方面考虑,但是要注意长度。如下代码所示:
int CACHE_EXPIRE = 60;
// 增加了second 表示单位是秒
int CACHE_EXPIRE_SECOND = 60;
String content = "MTIz";
// 增加了base64 表示编码方式是base64
String base64Content = "MTIz";
int totalMemory = 1024;
// 增加了 MB 表示容量单位是兆
int totalMemoryMB = 1024;
2.4 区分方法参数
如果一个方法有多个含义相同的参数,要加一些形容词区分参数的用途,如下代码所示:
private void modifyPassword(String password1 ,String password2)
以上代码示例,方法使用者难以区分两个password的用法,改写后:
private void modifyPassword(String oldPassowrd,String newPassword)
3 短小的方法
方法要多短小,才最合适呢? 这个没有固定结论,一般不要超过半个屏幕。如果一个方法几个屏幕滚不完,阅读者看了后面忘记前面,绝对要骂人。一定要把长方法拆分成功能内聚的短方法,如下代码所示:
// 获取个人信息
private UserDTO getUserDTO(Integer userId)
{
//获取基本信息
… 此处写了10行
//获取最近的一次订单信息
… 此处写了30行
// 获取钱包余额、可用优惠券张数等
… 此处写了30行
return userDTO;
}
改写如下:
// 获取个人信息
private UserDTO getUserDTO(Integer userId)
{
//获取基本信息
UserDTO userDTO= getUserBasicInfo(userId);
//获取最近的一次订单信息
userDTO.setUserLastOrder(getUserLastOrder(userId));
// 获取钱包、可用优惠券张数等
userDTO.setUserAccount(getUserAccount(userId));
return userDTO;
}
private UserDTO getUserBasicInfo(Integer userId);
private UserLastOrder getUserLastOrder(Integer userId);
private UserAccount getUserAccount(Integer userId);
4 减少嵌套
4.1 减少if/else嵌套
为什么要减少嵌套,难道嵌套不时尚吗?我曾经看到一个同事的代码嵌套达到9层,他自己维护的时候都看晕了。如下代码所示:
// 修改用户密码,这个例子只有3层嵌套,很温柔了
public boolean modifyPassword(Integer userId, String oldPassword, String newPassword) {
if (userId != null && StringUtils.isNotBlank(newPassword) &&
SpringUtils.isNotBlank(oldPassword)) {
User user = getUserById(userId);
if (user != null) {
if (user.getPassword().equals(oldPassword) {
return updatePassword(userId, newPassword)
}
}
}
}
改写如下:
// 修改用户密码
Public Boolean modifyPassword(Integer userId, String oldPassword, String newPassword) {
if (userId == null || StringUtils.isBlank(newPassword) ||
StringUtils.isBlank(oldPassword)) {
return false;
}
User user = getUserById(userId);
if(user == null) {
return false;
}
if(!user.getPassword().equals(oldPassword) {
return false;
}
return updatePassword(userId, newPassword);
}
采用卫语句减少了嵌套,但是并非所有场景都适合这样改写,也可以将关联性强的逻辑抽取成一个独立的方法减少嵌套。
4.2 抽离try/catch
大家有没有见过一个超长的方法,从头到尾被一个try/catch照顾着?我经历过的项目中,这种不负责的写法比比皆是。并非每行代码都会抛出错误,只要将会抛出错误的业务放在一个独立的方法即可。如下代码所示:
// 获取个人信息
private UserDTO getUserDTO(Integer userId)
{
try {
//获取基本信息
... 此处写了10行
//获取最近的一次订单信息.
...此处写了20行
// 获取钱包、可用优惠券张数等
...此处写了20行
}catch (Exception e) {
logger.error(e);
return null;
}
}
return userDTO;
}
改写如下:
// 获取个人信息
private UserDTO getUserDTO(Integer userId)
{
//获取基本信息
UserDTO userDTO= getUserBasicInfo(userId);
//获取最近的一次订单信息
userDTO.setUserLastOrder(getUserLastOrder(userId));
// 获取钱包、可用优惠券张数等
userDTO.setUserAccount(getUserAccount(userId));
return userDTO;
}
private UserDTO getUserBasicInfo(Integer userId);
private UserLastOrder getUserLastOrder(Integer userId);
private UserAccount getUserAccount(Integer userId){
try{
// TODO
} catch ( Exception e)
{ //TODO }
}
5 封装参数
如果方法参数将超过3个,建议放在类中包装起来,否则再增加参数时,由于语义的强耦合会导致调用方语法错误。在后台管理中的分页查询接口,常常会有很多查询参数,而且有可能增加,封装起来是最好的。
// 分页查询订单 6个参数
public Page<Order> queryOrderByPage(Integer current,Integer size,String productName,Integer userId,Date startTime,Date endTime,Bigdecimal minAmount ,Bigdecimal maxAmount) {
}
改写如下:
public class OrderQueryDTO extends PageDTO {
private String productName;
private Integer userId;
private Date startTime;
private Date endTime;
private Bigdecimal minAmount ;
private Bigdecimal maxAmount;
}
// 分页查询订单
Public Page<Order> queryOrderByPage(OrderQueryDTO orderQueryDTO) {
}
6 注释规范
注释的使用原则是:如果代码能够说清楚,就不要注释。许多程序员犯的错误,不是注释少了,而是多了,主要体现在以下几个方面:
- 多余的注释:注释没有提供比代码更多的信息,也没有解释代码的意图或逻辑,读代码比读注释还要容易,这种注释就是多余的。
- 误导性注释:代码随着需求变动,注释也要随之变动,不准确的注释不如没注释。
- 日志式注释:每次编辑代码都在模块处理添加一条注释,例如:2018-10-02增加了某某功能。通过源代码控制系统的 commit 字段就可以保存这种信息,这一类注释是画蛇添足。
- 署名注释:随着不断迭代,代码将会与原作者脱离关系,署名注释没有价值。
- 注释掉的代码:注释掉的代码让读者产生疑问,这个代码到底有用还是没用?如果有用,为什么要注释掉,如果没用,为什么不删除?
本文链接:https://www.codingbrick.com/archives/1406.html
特别声明:除特别标注,本站文章均为原创,转载请注明作者和出处倾城架构,请勿用于任何商业用途。