希望你的代码如同你本人一样优雅。:)
我们编写的代码,除了用于机器执行产生我们预期的效果以外,更多的时候是给人读的,这个读代码的可能是后来的维护人员,更多时候是一段时间后的作者本人。
如何能够写出优雅整洁且不让人【哔——】的代码?
今天我们邀请了 4 名淘系技术工程师,结合他们自身在写码过程中的感受,给大家分享一些他们对于优雅代码的理解和具体写作方式,希望能够对你有帮助。
01
淘系技术部 玄路
所谓优雅,相对应的是坑。只有见过足够多的坑,才会形成自己的编码理念。
工程开发,除了要满足业务需求和性能需求之外,还需要保证可维护。要随时面对人员流动对系统带来的风险,所以我对优雅代码的理解更偏向于易维护。下面是我的个人理解:
基础理念:易懂、简单、高效
三个都要是最好的,但是很多时候我们需要做相对取舍,作为工程师,我认为易懂也就代表后来者更容易维护。这里的易懂不是意味着加注释,如果一段代码开发者感觉需要加注释,那说明它本身就不易理解了,可以考虑优化。
编码规范
作为java开发,不得不提孤尽大神的《阿里巴巴开发规约》,这里面关于编码规范的说明很详细。不过以我的经验很少有人或者团队能够完全做到。比较实际的角度我认为,首先,团队的技术leader有必要去总结一份贴近与自己现状的开发规范,统一团队的开发风格,编码规范,工程结构,保证一个团队产出代码风格一致。其次,规范落实,团队新人进入之后,除了帮助了解业务,对于现有开发风格,编码规范,工程结构的培训和监督必不可少。
设计模式
二十三种设计模式是每个开发者都会遇到的。但是新手往往执着于设计模式本身,而忽略了背后的六大原则。
首先,设计模式要了解它的适用场景,引入设计模式会增加一定的理解成本,自身开发很难察觉到理解成本。所以需要在代码 review 的时候,也评估使用场景。
其次,现实中,我们学习设计模式,只看到它优势的一面。但是在实际开发中使用,一定需要了解它的缺点,防止滥用。
重构
重构应该存在于每一次的开发过程中,完成功能需求,性能需求之后,还需要思考改动后的代码是否优雅,结构是否清晰。很多时候虽然只加了一行代码,但全局维度需要重新调整结构。
最后想说,所谓优雅,相对应的是坑。只有见过足够多的坑,才会形成自己的编码理念。总结自己遇到的坑,找到避免坑的方法,就能保证持续进步。
02
淘系技术部 泽畔
整洁的代码不一定是好代码,但好代码一定是整洁的。
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。(Martin Fowler)
很难给好的代码下一个定义,相信很多人跟我一样不会认为整洁的代码就一定是好代码,但好代码一定是整洁的,整洁是好代码的必要条件。整洁的代码一定是高内聚低耦合的,也一定是可读性强、易维护的。
高内聚低耦合
高内聚低耦合几乎是每个程序员员都会挂在嘴边的,但这个词太过于宽泛,太过于正确,所以聪明的编程人员们提出了若干面向对象设计原则来衡量代码的优劣:
开闭原则 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)
可读性
代码只要具有了高内聚和低耦合就足够好了吗?并不见得,我认为代码还必须是易读的。好的代码无论是风格、结构还是设计上都应该是可读性很强的。可以从以下几个方面考虑整洁代码,提高可读性。
命名
大到项目名、包名、类名,小到方法名、变量名、参数名,甚至是一个临时变量的名称,其命名都是很严肃的事,好的名字需要斟酌。
名副其实
好的名称一定是名副其实的,不需要注释解释即可明白其含义的。
具体可见:阿里工程师谈,什么是好的代码?
后者比前者的命名要好很多,阅读者一下子就明白了变量的意思。
容易区分
我们很容易就会写下非常相近的方法名,仅从名称无法区分两者到底有啥区别(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 的可能性。
我认为仅仅编写出可运行的代码是远远不够的,还要时刻注意代码的整洁度,留下一些漂亮的代码,希望写的代码都能保留并运行 102 年!
03
淘系技术部 鹿姚
命名的重要性要大于注释,注释不是万能的,好的函数命名比写一大段注释易读的多。
我认为,第一要紧的是没有 BUG,实现了该实现的功能,考虑了该考虑到的异常,否则即使命名再完善,注释再详细,都不能改变这段代码是一段没有价值的,甚至对系统有破坏性影响的代码。
其次,需要让其他维护者能够读懂、读明白。好的代码不应该是一本晦涩的古书,而是能够轻松阅读的白话文小说。
软件设计是基础。在写代码前,特别是实现一个复杂功能或大型系统前,要进行必要的软件设计。不同领域的软件设计需要考虑到的方向会有所不同。前端项目一般可包括项目描述、本次实现的范围、基本架构、详细设计、与外部系统的交互设计等。在进行设计时要考虑模块的可重用性和移植性,也就是所谓的“高内聚低耦合”。好的软件设计可以帮助我们厘清思路,避免方向性错误,也能在初期就能从相关方或团队内获得反馈以便及时修改。
对于逻辑复杂或关键核心函数,需要搭配有效且详细的单元测试。单元测试需要考虑通用场景,但也需要考虑边界条件。新手可以根据单元测试的 FIRST 原则逐步熟悉。另外随着代码的迭代,单元测试也需要随之迭代,增加必要的 case。
易读的代码有非常的特性,例如:必要的注释、好的命名等等。我个人认为通常情况下命名的重要性要大于注释,注释不是万能的,好的函数命名比写一大段注释易读的多。命名需要是能够阐明函数功能的、在保证可读性的情况下足够短。对于有硬编码或特殊业务逻辑的代码段来说,需要添加注释,为以后的维护者提供足够的信息。另外,在编码风格上需要遵守团队的编码规范,前端可以借助 eslint、prettier 等工具帮助我们规范编码风格。
04
淘系技术部 怀乡
我认为优雅的代码必然包含以下一些特性:容易理解,便于阅读、测试及其维护,还能够具备良好的扩展性,命名清晰,注释完善等等。
想要写出好的代码并不是一件容易的事情,它需要我们不断地对现有的代码进行反思,那如何写代码才能让它变得更加优雅。
我觉得可以从以下几个方面入手:
注重代码规范
一名合格的码农,都应该至少完整地阅读一遍官方的代码规范指南,我觉得阿里的开发规约做的比较全面,值得推荐。然后在平时的开发中尽量遵守这种规范,久而久之代码就会越来越好看,衡量代码质量的唯一标准是阅读该代码时说脏话的次数!!!
逻辑清晰
对代码的逻辑层次要做到了然于心,编写代码要时时刻刻对当前代码所代表的逻辑层次要有“感觉”,要能意识到这段代码和上一段代码是否在某种标准下,处在同一个层次。编写代码要善于归纳这些层次,才能建构一个优美的结构。
低耦合、高内聚
耦合会严重影响代码的复用性及可扩展性,让后人维护时不得不修改甚至重写这部分代码,不仅浪费时间还会导致代码库中又多出一块类似的代码,很容易让人迷惑。同时,修改耦合度高的代码时经常会牵一发而动全身,如果修改时没有理清这些耦合关系,那么带来的后果可能会是灾难性的。高内聚就是不要将没有任何联系的东西堆到一起。
以上就是对如何写出一段不被骂娘代码的一些浅见。
Robert C. Martin 说,衡量代码质量的唯一标准是阅读该代码时说脏话的次数。
希望你的代码如同你本人一样优雅。:)
✿ 拓展阅读
作者|淘系技术
编辑|橙子君
出品|阿里巴巴新零售淘系技术