什么是好的代码?局部干净,核心逻辑简洁。
写出整洁代码不仅需在函数、类级别上用功,也应该理解一些其他主题,如项目架构、设计原则等,软件工程是复杂(complex)的,只有各个方面都处理得干干净净,才能在整体上做到代码整洁。
原则:消除重复,分离关注点,统一抽象层次
消除重复
重复的代码会让系统臃肿,难以维护,增加程序员的心智负担。消除重复的手段不外乎封装,抽取函数、类等。
-
代码重复,完完全全重复的代码,应该抽取出公共的函数。同一段代码出现两次及以上,就应该抽取出函数。
-
结构重复,代码虽然不一样,但结构类似,也应该抽取。结构重复可以推导出一些高级技术,如:
-
继承体系
-
泛型
-
模板方法
-
高阶函数
-
过程重复,如果我们的代码总是重复做同一件事,应该使其自动化。
分离关注点
物以类聚,人以群分,代码也是一样。关注点相同的代码应该在一起,天然具有亲和性,这句话的另一个含义,对关注点不同的代码天然具有隔离性,相互之间不应该太深入了解。
我们在业务代码开发中,主要业务逻辑是主线,应该突出主线,淡化支线,按照人的思维,这样才是好理解的。
例如在下单的逻辑中,可能的主线是:检查库存、检查余额、生成订单。那这个下单方法里就应该只有 3 行代码,而不应该有诸如权限判断、性能记录等,如果出现就会有 2 行代码是跟主线无关的,造成不必要的干扰,不要造成无谓的心智负担,应该解放心智去完成更复杂的事情。
-
分离主线和支线
-
分离技术和业务
技术型代码常常是公用的,如日期计算、日志记录、性能测量、数据库链接、基础工具类。这些应该和业务逻辑分开,相信这点大家都没有疑问。
-
按业务性质分离
对业务开发来说,业务知识永远都是第一位的。一个技术水平很高的程序员,但是对业务不理解,他也发挥不了全部水平,就像杀鸡用牛刀,施展不了全部功力。不同业务应该分开,在模块级、服务级甚至更高的产品级,这也应该是共识。
-
分离变化快慢的代码
变化快的代码和长年不变的代码分开。
-
分离性能高低的代码
重 I/O 的代码和重 CPU 的代码理应分开,方便合理分配资源,其他诸如此类的代码应该注意分开。
统一抽象层次
将有关认识与那些在实际中和他们同在的所有其他认识隔离开,这就是抽象,所有具有普遍性的认识都是这样得到的。——John Locke 《关于人类理解的随笔》
怎么理解抽象?抽象的反面是具体,具体是细节,可见抽象是细节的反面,抽象刻画了统一的画像,描述能力,是对事物在某些方面的特征的提取总结。总之,抽象表达的是意图,另一个理解就是,它不表达细节。抽象层次高,偏意图,语义(代码在上下文中表达的语义)清晰,信息量小;抽象层次低,偏实现,语义模糊,信息量大。
两个原则:
-
同一抽象层次上的对象才能直接对话;
-
同一抽象层次上的对象之间存在着紧密合作;
一个好的函数结构,应该这样像一棵树一样层次分明。每一个层次都只有 2~5 个步骤,一般而言我们做一件事也就 2~5 个步骤,分解太多太少都不好,太少没必要分解,太多记不住,增加心智负担。
以“把大象装进冰箱”为例,不外乎三步:
-
打开冰箱门
-
放进大象
-
关闭冰箱门
所以关于如何把大象切成碎片,不应该出现在上面,应该在步骤 2 的后续调用中。
隔离与隐藏
信息隐藏,是抽象的一种手段。通过信息隐藏,来暴露只想让外界知道的东西,表达意图。隔离是实现信息隐藏的重要手段。
编码 tips
以下都是一些简单实用的技术,以如何写出整洁代码,很多是出自《代码整洁之道》。
1. 类
-
类应该足够小
最初级的程序员可能会在一个 Controller 里做完所有的业务逻辑,最终会使这个类成为 God Class。一个类太大,代码太多,会使类的结构不清晰,职责混乱,维护代码时花费很多时间去寻找修改位置。譬如我们所见的世界,由分子、原子甚至更小的粒子排列组合而成,所以才有缤纷多彩的各色物质(对象),但如果构成物质的最小粒子就是人,那还能组合出什么其他物质呢?代码也是如此,类应该足够小,才能发挥排列组合的威力。
-
单一职责
类的职责应该单一,即“SOLID”五大原则的 S,职责单一意味着,“只有一个理由可以修改它”。另外,类名一般而言应该是名词,且描述其职责。
如果无法为一个类以精确的名称,这个类大概就太长了。类名越含混,该类越有可能拥有过多的权责。
——《Clean Code》
-
内聚
内聚的含义是,类的每一个字段都应该被某个方法所使用到。如果不能达到这个结果,应该考虑是否类的字段应该拆分出去成为新的类。
-
严格控制访问权限,注意信息隐藏,OCP
访问权限应该能小则小。能 private 就不要 package,能 package 就不要 protected。这样做能使我们更好的遵循 OCP 原则。最稳定的系统,是从不修改的系统。
2. 函数
-
尽可能小
经过漫长的试错,经验告诉我,函数就应该小 ——《Clean Code》
应该控制在 10 行以内,至多 20 行,除非是细节代码。这是完全可以做到的,做不到的原因可能有:函数功能太多,职责不单一;函数抽象层次划分不清;语言支持不够等。前面已经说过,做一件事大概也就 2~5 步,每一步一个函数,加上可能的条件判断,10 行是一个比较合理的数字。而且,函数越小,功能越集中,越便于取一个好名字。
-
单一职责
一个函数只做一件事。这一点很容易理解,难的是我们如何确定函数做的那件事是什么。一千个读者就有一千个哈姆雷特,同样的,不同的人对一个函数的理解也有所不同,对于做一件事的步骤拆分也可能有所不同。对此,一个可靠的判断准则是:函数的内容(函数体内的代码)只是做了函数所在抽象层级的步骤,那这个函数就是只做了一件事。函数所在抽象层级,根据对业务的理解,应该用良好的函数名加以示意。
3. 参数尽量少
最理想是 0 个,其次是 1 个,2 个,最多 3 个参数,不要超过 3 个参数,除非你有非常特殊的理由。——《Clean Code》
参数带了极大的语义干扰,而且也难于测试。一个典型的不好的设计,就是用 bool 作为公开函数的参数,因为 bool 变量天然地会使人想到这个函数不会只做一件事,它分情况处理,bool 入参的命名稍有歧义就会使人困惑。例如
func GoToWork(raining bool) {
if raining {
// 开车去
} else {
// 走路去
}
}
更推荐的做法是,将 bool 参数的函数私有,另外公开两个语义清晰的函数。
func WalkToWork() {
goToWork(false)
}
func DriveToWork() {
goToWork(true)
}
// 私有
func goToWork(raining bool) {
if raining {
// 开车去
} else {
// 走路去
}
}
任何时候,我们维护代码,最关心的都是对外可访问的函数,这些函数应该尽我们所能使其整洁。
golang 里能够返回多个返回值,但这绝不可以滥用。试看
func func1(/* params */) (string, string, string, string, string) {
// 函数职责不单一,功能太多
}
func func2(/* 此处多达6个参数 */) {
// 函数职责不单一,功能太多
}
这样多入参、多返回值,给调用方造成很大困扰,调用方需要反复分辨每个参数、返回值的对应关系。不能因为眼前就只有自己调用自己写的函数而这样放纵,我们写的代码,终究是会由别人接手的。
-
无副作用
一般而言,函数应该是无副作用的,对于调用方来说,它就是一个黑盒:给定输入,产生输出。仅此而已。不要让调用方去思考我这次调用会不会产生输出以外的其他结果。例如应该尽量避免这种情况:一个函数,以指针作为参数,返回一个结果的同时,还修改了指针所指向的内容。一个函数的作用,要么是 get,要么是 post,即要么函数无修改的 get 一个结果,要么就是单纯修改而不返回修改以外的结果。
-
if 嵌套不应超过 2 层
if 不要嵌套超过 2 层,这初听起来有些强人所难,仿佛要求每个职业篮球运动员都应该以乔丹的能力作为基准。可人的天性就是不喜欢思考的,喜欢简单。在此再一次强调统一抽象层次,if 嵌套太多,一定要思考,是不是函数做的事情太多,跨层次在搞事情。我们应该用一些高标准去检验自己的代码,想办法去满足,这个过程才会有所成长,否则除了收获经验以外,不会有进阶的成长(其实人生又何尝不是如此)。
消除多层 if 嵌套的一些手段
-
提前返回,将嵌套 if 铺陈开来,使不满足条件的分支提前返回;
-
碰到第三个 if,直接将其抽取为函数(简单粗暴);
-
语义和实现距离不为 0 时应该抽取函数
好的代码读起来就应该像自然语言,而不是像程序,这就要求在高抽象层次时,函数应该表达意图,而只有在叶子结点——抽象层次最低的实现部分才表达实现,这个地方的代码更像是程序。所以,在代码中的某个位置,我们本应该表达意图,却写了细节实现代码,这就应该抽取出函数。
代码应该表达意图,特别是 if 条件分支里,不要让人再去推理,直接表达语义。就像人走路,相比于一马平川,我们不会更喜欢岔路;但凡岔路,就应该明确指明路线,而不是在路口打个机锋,才让你思考十年然后顿悟才选择出了某一条路。
3. 命名与注释
坊间流传着一句话,给变量命名犹如给自己亲女儿命名一般,只因如此,就不会随意命名了。命名的一般原则无外乎完整、简洁、准确等,我们要注意的是命名虽然重要,但也无需发展成为圣战,下面来看下本人汇总的几点。
-
顾名思义、望文知义、无歧义,也就是说命名乃至日常工作沟通中应当如清楚明白无歧义地表达含义,不要让别人猜你的意思。
-
表达语义,避免误导,命名不应该表达实现(如 List 实现,数据结构等),而应该表达语义。
-
使用读得出来的名字,谨慎使用缩写
-
团队统一业务术语,DDD 的一个重要理念就是统一术语,从运营产品到开发测试等,都应该对某一个业务专有词不产生任何歧义。
-
注释,好的代码是自注释的。
最后,本人还要表达一点:只完成功能的代码,是最基础的代码。好的代码还应该尽量完成代码的非功能特性。