目录

如何写CleanCode:易读与易修改的艺术

如何写CleanCode:易读与易修改的艺术

在软件开发领域,代码质量一直是开发者们追求的核心目标之一。关于什么样的代码是好代码,好代码最基本的一个属性是实现客户所有诉求,这个可以作为好软件L0层的特点。再往上则有有很多不同的关注面、不同的维度来衡量,比如安全、运维、成本等,在每一个关注的维度上有不同的标准来度量。其他的维度要求与度量相对都比较明确固定,本文主要从软件工程的视角与维度来看怎么写好代码。

关于这点行业里好代码有很多的评价标准,如:灵活性、简洁性、可扩展性、可读性、可复用性、可测试性等等,好代码的第一性是什么,他们的关系是什么,他们要解决什么问题?

作者认为,软件工程角度来看,好的代码的最核心特点是易读易改

  • 易读:软件工程是一个工程,是多人分工协作完成的,写的代码是给别人看的,让别人看懂是一个很重要的事情,否则就没有了协作的基础。
  • 易改:软件工程区别于传统的建筑工程等,是个持续演化的过程,在项目交付后,会面临持续的需求变化与能力演进,方便简单的进行扩展修改是好代码的另一个关键特点

本文尝试以提纲挈领的形式来总结好代码的一些本质,介绍的内容大多是笔者自己的思考总结,可能存在与读者观点上的冲突,有不同想法欢迎大家一起批评讨论

如何写出易读的代码

写出容易阅读的代码的本质是对人类认知效率的优化

什么是易读的代码,在软件工程当前人类协作工作的情况下,符合人类认知规律代码就是易读的代码,也是好代码的关键特点。当然如果后面随着AI的发展,软件编码的工作都被AI所替代,那么同样的,符合AI认知规律能够让不同的AI都能读懂的代码就是易读的代码

我们以当前的人类协作的情况下来看,我们要理解易读代码的本质就要理解人类认知的规律。笔者下面尝试从视觉与认知两个角度来回答这个问题

符合视觉信息采集规律

喜欢看:易读的代码在空间上是符合人类视觉习惯的

毕竟当前人类在阅读代码的时候信息的采集还是通过眼睛进行的(盲人的盲文阅读暂不在我们讨论的范围内),什么的信息是符合视觉采集习惯的,这里涉及到人类的生理和心理特点。

在这里我们不展开讨论人类视觉采集的生理与心理原理,在这个我们一些主观上的认知也可以支撑我们理解这个问题,如:过长的内容伴随眼球的运动对信息采集不友好,视觉擅长识别统一的模式和规律,视觉系统习惯将分散的元素组合成整体等等

落到代码上对我们的代码风格很多明确的指导如:

  • 使用统一的代码缩进
  • 使用统一的括号风格
  • 避免过长的代码内容
  • 使用统一的命名规则

符合人类认知规律

易读的代码容易理解,是建立在人类认知事物的局限性基础上的 下面从几个方面简单介绍下人类认知的一些局限性以及在代码设计上如何对代码面向易读性进行优化

记得住:人类的短时记忆只能记忆5-9个信息块

一个著名的米勒定律的研究,揭示人类短时记忆只能记忆7±2个信息块,对应到计算机上,我们可以理解成我们的大脑只有7±2个寄存器,如果我们大脑在思考计算中超过寄存器的数量,那么必然伴随着信息的丢失,写内存与硬盘当然是可以的(记录下来后面查阅),但是必然伴随着处理速度的下降和全局感的确实

指导到我们编码上,我们希望尽量在一个代码单元中,减少大脑暂存的内容,让大脑顺序的执行指令减少寄存器的读写,减少上下文的切换,一些编码原则大家可以体会下:

  • 变量/函数的命名应该清晰表达含义(减少变量与逻辑语义对应关系的存储)
  • 避免使用魔法值(减少数值含义的存储)
  • 圈复杂度应该低于5(减少条件上下文语义的存储)
  • 减少临时变量的使用,临时变量的定义应该靠近使用(减少临时变量的存储长度)

看得懂:符合人类既有认知的抽象

人类认知事物有自己的机制,其中抽象是人类思考认知的核心机制之一 所谓抽象就是从事物中提取公共特征,忽略不重要的细节,抽象是分层的,每个层次上都有不同粒度不同维度的关注点。

举例来说明一下抽象的含义,比如我今天从家到北京天安门这个事情,对比下面两种描述

1
2
3
描述一:今天从家中坐地铁到南京南站,乘坐高铁到了北京西站,从北京西站打出租车到了天安门

描述二:今天早上8:00起床,刷完牙吃了个早饭,出门的时候已经9:00了,出门发现外面下雨了,又回去拿了把伞。坐地铁上不小心睡着了做过了,好不容易才赶上高铁,在高铁上吃了个盒饭,下午才到北京...

描述二的内容更加详尽,但是一味的信息堆叠是不利于人们理解这个事情的。

描述一则做了高层次的抽象,先从宏观高层次上给出了描述。在不同的层次上我们有不同的关注点,在这段行程中,首先我们对整体有一个概念,整体分了三段坐地铁-高铁-打车,下面的层次我们可能会关注具体一层的细节,如地铁高铁上发生的事情。 当然在同一层上也有不同关注的维度,例如对时间的关注、天气的关注、费用的关注等等,我们对事情在不同方向不同维度平面进行抽象,逐层展开,才更有利于对事情的理解。

在写代码上,我们有很多的设计都是类比人类既有认知的抽象原则,比如面向对象就是一种抽象,是类比人类对事情的理解组织代码,DDD也是一种类比业务系统概念认知的代码抽象,将代码语言对齐人类语言,使人们更加容易理解。其他还有一些代码要求也是为了让代码更好的抽象(不要忘记抽象的方向是类比符合人类既有认知):

  • 模块化与分层设计原则
  • SLAP:Same Level Abstraction Principle

读得轻松:符合人思考问题的粒度

根据微软资助得研究,2000年后人类得平均专注力时间从12s下降到了8s,而金鱼得专注力是9s

这个研究一定程度上反应了现在在多种因素得影响下,人的专注力是在逐渐下降得。 可能大家有这种感觉,在上学得时候一节课45min的时间总会有些分神,根据研究人类在坐高专注力的工作时,最大的专注时长在15-20min,这对我们写代码有什么启示呢?我认为在我们做代码模块的粒度设计时,要在一个专注度周期内让用户get到你想表达的含义,过度的消耗专注力或者更多的上下文切换对别人理解你的代码是无益的

在代码的一些设计原则中也体现了这种思想,比如限制一个函数的长度,限制一个类的长度,限制包类的数量等等

如何写出易改的代码

易修改的代码本质是能够很好的对抗软件熵增,通过结构化设计对抗系统的自然退化

什么是软件熵,熵这个词从热力学里面来的,类比下来当前在软件领域和信息技术领域也有很多关于熵的定义和使用 熵是衡量系统的混乱程度的,在我们讨论的CleanCode领域,衡量的是代码的混乱程度

我们在引入一个需求变化调整时我们需要修改很多的地方,对其他很多不相关的需求实现也造成了影响,系统的稳定性非常弱,我们认为这种系统时非常无序混乱的,也就时高熵

熵增的过程时伴随着软件演化的进程的,初始的软件一般都有相对清晰的设计,相对明确的模块边界,版本的变化的引入,在新增需求或者修复缺陷的时候,因为缺乏约束和设计,代码会越来越混乱,软件熵增,最终成为一个大泥潭。我们在做软件设计时要可能明确模块设计,设计良好的约束方式,降低软件演化过程中的熵增速度

要控制好软件熵,两个关键的点是混乱隔离局部有序 说白了将混乱隔离到各个模块当中避免扩散,在模块内部保障熵稳定

局部有序:高内聚的模块设计

代码模块设计时,最最重要的原则是:单一职责原则

什么是模块:模块是一个代码单元,可以是一个方法,可以是一个类,可以是一个包,可以是一个微服务。

单一职责是在对应分层上的单一职责:在做任何一个层次的模块设计时,都要考虑好在这个层次上的单一职责。什么叫在这个层次上的单一职责呢,这就跟上面讲的抽象相关了,每个分层都有对应层次的抽象,比如controller层上单个接口的处理就是单一职责的,service层上一个业务逻辑对象的能力就是单一职责的,dao层上一个数据库表的操作就是单一职责的,单一职责是和对应层次的设计语义相对应的

什么是职责:职责就是变化原因,大家为了同一个变化原因凑在了一起,这个设计是跟需求变化方向紧密相关的

模块和变化的关系:变化是动态的,模块是隔离变化的,所以模块也会动态变化。我们将需求变化看成一个向量,刚开始的时候只有一个变化,我们在向量方向上做了调整,后面随着业务发展,不断有新的向量出现,我们拟合这些变化向量,发现这些向量在同一个向量空间里,这个向量空间实际上应该是一个变化内聚点,我们应该抽象一个层或一个类来聚合这些变化,而不是在不同的方向散点的去变更修改。如果我们没有良好的设计承载好各个变化的不同向量空间,就会将变化散布在整个软件系统里,造成软件系统的熵增

混乱隔离:低耦合的模块设计

模块是变化原因内聚,模块之间最小程度的互相了解,避免熵扩散,这就是低耦合。最大程度降低模块之间的互相影响。

在软件设计中,我们往往使用接口来降低两个模块之间的互相依赖,接口中只有抽象能力定义,最大程度降低交互双方互相了解的程度,避免熵扩散。

同样的在做软件设计的时候,不是认为只要定义了interface就是符合低耦合的,我们要抓住本质接口是为了最大化降低模块之间的信息熵,减少互相模块之间的互相了解(最小化知识),避免一方面的知识变化熵扩散到另一模块(影响另一方)。

例如我们经常看到一个很大的service接口定义,只有一个类实现这个接口并将所有代码实现写在里面,这就是对低耦合的一种误解。接口的设计是要有良好的抽象(参考上面的说明),接口是在良好抽象上的基础上对一个变化方向的封装

我们在面向对象设计中有一个很著名的SOLID原则

  • S单一职责原则:模块应该只做一件事,有一个对应层次上的唯一的变化原因
  • O开闭原则:面向扩展的开放与面向修改的关闭
  • L里氏替换:程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换
  • I接口隔离:多个特定客户端接口要好于一个宽泛用途的接口
  • D依赖反转:依赖于抽象,而不是具体实现

这些原则指导我们如何做好高内聚低耦合的设计,我们可以一起重新理解下这些原则:

  • 单一职责原则体现的最多的是模块的高内聚;
  • 开闭原则更多体现的是低耦合,通过良好的设计将变化隔离到一个接口中;
  • 里氏替换更多体现的是内聚,考虑的是基类对协议的定义子类都应该遵从,他和和依赖反转相结合来降低耦合;
  • 接口隔离则是进一步识别变化子空间,细化变化方向,让模块间了解的知识更少,避免熵扩散

总结一下

/images/post/image-13.png