AICon全球人工智能与机器学习技术大会8折特惠,购票立减¥960! 了解详情
写点什么

阿里工程师谈,什么是好的代码?(二)

2019 年 12 月 19 日

阿里工程师谈,什么是好的代码?(二)

好代码的特性

很难给好的代码下一个定义,相信很多人跟我一样不会认为整洁的代码就一定是好代码,但好代码一定是整洁的,整洁是好代码的必要条件。整洁的代码一定是高内聚低耦合的,也一定是可读性强、易维护的。


高内聚低耦合

高内聚低耦合几乎是每个程序员员都会挂在嘴边的,但这个词太过于宽泛,太过于正确,所以聪明的编程人员们提出了若干面向对象设计原则来衡量代码的优劣:


  • 开闭原则 OCP (The Open-Close Principle)

  • 单一职责原则 SRP (Single Responsibility Principle)

  • 依赖倒置原则 DIP (Dependence Inversion Principle)

  • 最少知识原则 LKP (Least Knowledge Principle)) / 迪米特法则 (Law Of Demeter)

  • 里氏替换原则 LSP (Liskov Substitution Principle)

  • 接口隔离原则 ISP (Interface Segregation Principle)

  • 组合/聚合复用原则 CARP (Composite/Aggregate Reuse Principle)


这些原则想必大家都很熟悉了,是我们编写代码时的指导方针,按照这些原则开发的代码具有高内聚低耦合的特性。换句话说,我们可以用这些原则来衡量代码的优劣。


但这些原则并不是死板的教条,我们也经常会因为其他的权衡(例如可读性、复杂度等)违背或者放弃一些原则。比如子类拥有特性的方法时,我们很可能打破里氏替换原则。再比如,单一职责原则跟接口隔离原则有时候是冲突的,我们通常会舍弃接口隔离原则,保持单一职责。只要打破原则的理由足够充分,也并不见得是坏的代码。


可读性

代码只要具有了高内聚和低耦合就足够好了吗?并不见得,我认为代码还必须是易读的。好的代码无论是风格、结构还是设计上都应该是可读性很强的。可以从以下几个方面考虑整洁代码,提高可读性。


命名


大到项目名、包名、类名,小到方法名、变量名、参数名,甚至是一个临时变量的名称,其命名都是很严肃的事,好的名字需要斟酌。


► 名副其实


好的名称一定是名副其实的,不需要注释解释即可明白其含义的。


  /**
* 创建后的天数
**/
int d;

int daysSinceCreation;

复制代码


后者比前者的命名要好很多,阅读者一下子就明白了变量的意思。


► 容易区分

我们很容易就会写下非常相近的方法名,仅从名称无法区分两者到底有啥区别(eg. getAccount()与 getAccountInfo()),这样在调用时也很难抉择要用哪个,需要去看实现的代码才能确定。


► 可读的

名称一定是可读的,易读的,最好不要用自创的缩写,或者中英文混写。


► 足够短

名称当然不是越长越好,应该在足够表达其含义的情况下越短越好。


格式

良好的代码格式也是提高可读性非常重要的一环,分为垂直格式和水平格式。


► 垂直格式

通常一行只写一个表达式或者子句。一组代码代表一个完整的思路,不同组的代码中间用空行间隔。


public class Demo {    @Resource    private List<Handler> handlerList;    private Map<TypeEnum, Handler> handlerMap = new ConcurrentHashMap<>();
@PostConstruct private void init() { if (!CollectionUtils.isEmpty(handlerList)) { for (Handler handler : handlerList) { handlerMap.put(handler.getType(), handler); } } }
publicResult<Map<String, Object>> query(Long id, TypeEnum typeEnum) { Handler handler = handlerMap.get(typeEnum); if (null == handler) { return Result.returnFailed(ErrorCode.CAN_NOT_HANDLE); } return handler.query(id); }}
复制代码


如果去掉了空行,可读性大大降低。


  public class Demo {      @Resource      private List<Handler> handlerList;      private Map<TypeEnum, Handler> handlerMap = new ConcurrentHashMap<>();      @PostConstruct      private void init() {          if (!CollectionUtils.isEmpty(handlerList)) {              for (Handler handler : handlerList) {                  handlerMap.put(handler.getType(), handler); } } }      public Result<Map<String, Object>> query(Long id, TypeEnum typeEnum) {          Handler handler = handlerMap.get(typeEnum);          if (null == handler) {              return Result.returnFailed(ErrorCode.CAN_NOT_HANDLE);          }          return handler.query(id); }  }

复制代码


类静态变量、实体变量应定义在类的顶部。类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。


► 水平格式

要有适当的缩进和空格。


► 团队统一

通常,同一个团队的风格尽量保持一致。集团对于 Java 开发进行了非常详细的规范。(可点击下方阅读原文,了解更多内容)


类与函数

► 类和函数应短小,更短小

类和函数都不应该过长(集团要求函数长度最多不能超过 80 行),过长的函数可读性一定差,往往也包含了大量重复的代码。


► 函数只做一件事(同一层次的事)

同一个函数的每条执行语句应该是统一层次的抽象。例如,我们经常会写一个函数需要给某个 DTO 赋值,然后再调用接口,接着返回结果。那么这个函数应该包含三步:DTO 赋值,调用接口,处理结果。如果函数中还包含了 DTO 赋值的具体操作,那么说明此函数的执行语句并不是在同一层次的抽象。


► 参数越少越好

参数越多的函数,调用时越麻烦。尽量保持参数数量足够少,最好是没有。


注释

► 别给糟糕的代码加注释,重构他

注释不能美化糟糕的代码。当企图使用注释前,先考虑是否可以通过调整结构,命名等操作,消除写注释的必要,往往这样做之后注释就多余了。


► 好的注释提供信息、表达意图、阐释、警告

我们经常遇到这样的情况:注释写的代码执行逻辑与实际代码的逻辑并不符合。大多数时候都是因为代码变化了,而注释并没有跟进变化。所以,注释最好提供一些代码没有的额外信息,展示自己的设计意图,而不是写具体如何实现。


► 删除掉注释的代码

git 等版本控制已经帮我们记录了代码的变更历史,没必要继续留着过时的代码,注释的代码也会对阅读等造成干扰。


错误处理

► 错误处理很重要,但他不能搞乱代码逻辑

错误处理应该集中在同一层处理,并且错误处理的函数最好不包含其他的业务逻辑代码,只需要处理错误信息即可。


► 抛出异常时提供足够多的环境和说明,方便排查问题

异常抛出时最好将执行的类名,关键数据,环境信息等均抛出,此时自定义的异常类就派上用场了,通过统一的一层处理异常,可以方便快速地定位到问题。


► 特例模型可消除异常控制或者 null 判断

大多数的异常都是来源于 NPE,有时候这个可以通过 Null Object 来消除掉。


► 尽量不要返回 null ,不要传 null 参数

不返回 null 和不传 null 也是为了尽量降低 NPE 的可能性。


如何判断不是好的代码

讨论了好代码的必要条件,我们再来看看好代码的否定条件:什么不是好的代码。Kent Beck 使用味道来形容重构的时机,我认为当代码有坏味道的时候,也代表了其并不是好的代码。


代码的坏味道

► 重复

重复可能是软件中一切邪恶的根源。—— Robert C.Martin


Martin Fowler 也认为坏味道中首当其冲的就是重复代码。


很多时候,当我们消除了重复代码之后,发现代码就已经比原来整洁多了。


► 函数过长、类过大、参数过长

过长的函数解释能力、共享能力、选择能力都较差,也不易维护。


过大的类代表了类做了很多事情,也常常有过多的重复代码。


参数过长,不易理解,调用时也容易出错。


► 发散式变化、霰弹式修改、依恋情结

如果一个类不是单一职责的,则不同的变化可能都需要修改这个类,说明存在发散式变化,应考虑将不同的变化分离开。


如果某个变化需要修改多个类的方法,则说明存在霰弹式修改,应考虑将这些需要修改的方法放入同一个类。


如果函数对于某个类的兴趣高于了自己所处的类,说明存在依恋情结,应考虑将函数转移到他应有的类中。


► 数据泥团

有时候会发现三四个相同的字段,在多个类和函数中均出现,这时候说明有必要给这一组字段建立一个类,将其封装起来。


► 过多的 if…else 或者使用 switch

过多的 if…else 或者 switch ,都应该考虑用多态来替换掉。甚至有些人认为除个别情况外,代码中就不应该存在 if…else 。


总结

本文首先一句话概括了我认为的好代码的必要条件:整洁,接着具体分析了整洁代码的特点,又分析了好代码的否定条件:什么样的代码不是好的代码。仅是本人的一些见解,希望对各位以后的编程有些许的帮助。


我认为仅仅编写出可运行的代码是远远不够的,还要时刻注意代码的整洁度,留下一些漂亮的代码,希望写的代码都能保留并运行 102 年!


后续增加一些实际的例子来说明好的和坏的代码;分享下如何编写整洁代码——自己认为有用的一些编程技巧。


本文转载自淘系技术公众号。


原文链接:https://mp.weixin.qq.com/s/AjubL4vVhFa_FIlaopLVCA


2019 年 12 月 19 日 18:18358

评论 1 条评论

发布
用户头像
跟《代码整洁之道》上的观点基本一致
2021 年 04 月 23 日 14:11
回复
没有更多了
发现更多内容

区块链食品溯源,食品安全溯源链搭建

135深圳3055源中瑞8032

低代码和零代码快速开发崛起,迎来普通人开发软件的时代!

低代码指南

应用案例| 基于Volcano 的锐天大规模离线高性能计算生产实践

华为云原生团队

云计算 AI 云原生 批量计算 大数据平台

uni-app实现实时消息SDK插件

anyRTC开发者

uni-app 音视频 WebRTC 跨平台 sdk

详解 Flink 容器化环境下的 OOM Killed

Apache Flink

flink 流计算

RocketMQ如何保证消息顺序性

废材姑娘

RocketMQ

Ansible 新手指南 - 如何批量管理 NGINX

东风微鸣

ansible

大作业一

Geek_83908e

架构师一期

企业架构培训感悟

Man

企业架构 中台战略

Java内存模型精讲

伯阳

Java 多线程 后端开发 多线程与高并发 Java内存模型

面向行业智能,华为数据通信推动的2020之变

脑极体

<译文>NGINX 实战手册 - 控制访问

东风微鸣

CSS01 - 引入方式

桃夭十一里

html/css

在线自习室场景爆发,在线教育平台用户时间争夺战打响

ZEGO即构

一周信创舆情观察(2020.12.28~2021.1.3)

统小信uos

可用性、可维护性、可靠性有什么区别?

禅道项目管理

DevOps 可用性 质量保障 可靠性

智慧社区解决方案,智慧社区管理系统

135深圳3055源中瑞8032

CSS03 - 常用字体样式

桃夭十一里

html/css

CSS04 - 常用外观属性

桃夭十一里

html/css

Kubernetes 疑难问题排查 - 10s 延迟

东风微鸣

Kubernetes

微服务可能失败的11个原因

xcbeyond

微服务 方法论

CSS02 - 选择器

桃夭十一里

html/css

一文教你学会Hive视图和索引

大数据老哥

大数据 hadoop hive

抄答案就是了,两套详细的设计方案,解决头疼的支付掉单问题

楼下小黑哥

支付系统 架构设计

传统产业高成本低效率该怎么解决?物联网打辅助稳赚上千万!

一只数据鲸鱼

物联网 数据可视化 绿色交通 工业节能 绿色农业

Spring 事务,你真的用对了吗(下篇)?

废材姑娘

Java Spring Framework

「顺畅不卡顿」,看华为云如何修炼音视频“内外功”

华为云开发者社区

数据中台 数据湖 云原生 RTC 华为云

技术干货丨隐私保护下的迁移算法

华为云开发者社区

迁移

在NGINX中根据用户真实IP限制访问

东风微鸣

云算力系统APP开发|云算力软件开发

开發I852946OIIO

系统开发

ROMA Compose:ROMA的新武器

华为云开发者社区

数据 API ROMA

阿里工程师谈,什么是好的代码?(二)-InfoQ