这一团糟的代码,真的是我写的?!

阿里妹导读:你有没有遇到过这种状况:过几周或者几个月以后,再看到本身写的代码,感受一团糟,不由怀疑人生?咱们天天都与代码打交道,但当被问道什么是好的代码时,不少人可能会先愣一下,而后给出的回答要么比较空泛,要么比较散,没办法简单明了地归纳出来。今天,咱们就来讲什么是好的代码?

一句话归纳
衡量代码质量的惟一有效标准:WTF/min —— Robert C. Martingit

图片来源:https://www.osnews.com/story/19266/wtfsm/

Bob大叔对于好代码的理解很是有趣,对我也有很大的启发。咱们编写的代码,除了用于机器执行产生咱们预期的效果之外,更多的时候是给人读的,这个读代码的多是后来的维护人员,更多时候是一段时间后的做者本人。程序员

我敢打赌每一个人都遇到过这样的状况:过几周或者几个月以后,再看到本身写的代码,感受一团糟,不由怀疑人生。编程

咱们本身写的代码,一段时间后本身看尚且如此,更别提拿给别人看了。函数

任何一个傻瓜都能写出计算机能够理解的代码。惟有写出人类容易理解的代码,才是优秀的程序员。 —— Martin Fowlerspa

因此,谈到好代码,首先跳入本身脑子里的一个词就是:整洁。好的代码必定是整洁的,给阅读的人一种如沐春风,赏心悦目的感受。设计

整洁的代码如同优美的散文。 —— Grady Booch版本控制

好代码的特性

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

高内聚低耦合对象

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

  • 开闭原则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);
            }
        }
    }

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);
    }
}

若是去掉了空行,可读性大大下降:

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。

总结

本文首先一句话归纳了我认为的好代码的必要条件:整洁,接着具体分析了整洁代码的特色,又分析了好代码的否认条件:什么样的代码不是好的代码。仅是本人的一些看法,但愿对各位之后的编程有些许的帮助。

我认为仅仅编写出可运行的代码是远远不够的,还要时刻注意代码的整洁度,留下一些漂亮的代码。

参考:
《重构——改善既有代码的设计》Martin Fowler
《代码整洁之道》 Robert C. Martin
《Java开发规约中文版 》阿里巴巴集团约码项目组



本文做者: 马飞翔(泽畔)

阅读原文

本文来自云栖社区合做伙伴“阿里技术”,如需转载请联系原做者。

相关文章
相关标签/搜索