整洁的函数
section 0 前言
我是java工程师,所有的博客主要针对java代码,其他语言可参考,不尽相同。
最近在跟一个新的项目,属于新的产品线,时间紧任务重,但是我在这个产品线上做的第一个需求,提测就delay了两天,最后上线是delay更多,遇到了各种各样的问题,一些是因为自己的开发不规范,代码可读性可维护性不高。另一些问题是沟通不好,以及自己对原有的业务和要做的需求没有完全理解,等到做的时候才发现有好多坑是没有考虑到的。
所以愈发觉得,越是时间紧任务重的项目,越是要遵守开发规范,写出整洁代码显得格外重要。正好最近厂内也有培养代码规范的相关课程,也在某个app上看一个大佬写的专栏,同时自己也在读相关的书,我是先从《代码整洁之道》读起,后面还会去读《设计模式》,《程序员修炼之道》,《人月神话》,《敏捷软件开发:原则模式与实践》,《Effective Java》,《重构》,《编写可读代码的艺术》,《深入浅出面向对象分析与设计》,《设计模式之禅》。这个分类就是以博客的形式,记录下自己学习的知识和心得,以及自己平时工作上的好代码和不好代码的特点。有些地方不太认同,也会写到博客里标注上来源和不认同或者部分认同。所以这个分类的博客会不断补充,修改和更新~~
Section 1 无用的碎碎念(请忽视)
入职半年多了,终于过了转正答辩,也过了good coder。说真的,虽然过了good coder,但是我认为自己离真正的good coder还有一段距离,至少我目前写出来的代码并不简洁美观。顺便吐槽一句,厂内也有一些good coder写代码并不优雅,甚至不去看readme。。。。。所以good coder并不一定写出good code。我不想让别人认识我之后,仍然指着我说,你作为一个good coder,连这个规则都不能遵守,连这个设计模式都不会用,甚至连read me文档都不会看。
虽然最近依然很忙,但是总要抽出时间来学习和总结输出不是?正如我厂的一位老师说的,加班和工作忙不是不学习的理由和原因,反而是不学习的后果。不断学习进步,才能更高效工作。所以,打破原来的恶性循环,进入良性循环吧!
Section 2 函数原则
函数,经历几十年存活至今,是程序中非常重要的单元和结构,几乎没有哪个语言会不支持函数,但是什么样的函数才是一个好的函数呢?如何才能写出好的函数呢?
最重要的原则之一:函数必须短小
函数应该有多短小才足够?《代码整洁之道》一书中给出的答案是:20行封顶最佳。在章老师《代码的艺术》一课中,给出的答案是不能超过一屏(如果我没记错的话),当然不能把字体调小或者搞一个竖屏。20行这个更加严格也更加明确,所以我更倾向于书上的要求。如果函数写长了,难免就会留一些不应该留在函数里面的内容和功能,违背了函数只做一件事的原则。
另外,如果函数中存在if语句、else语句、while语句,那么其中的代码块应该只有一行,而且这一行应该是一个函数调用语句。这也意味着函数不应该大到足矣容纳潜逃结构,所以函数的缩进层级不应该多于一层或两层。
——《代码整洁之道》
谈谈我在工作上遇到的函数吧~ 说真的,能遵守函数不超过20行的情况很少,微乎其微,很多函数都会超过50行,几百行的函数也见过。这类函数往往逻辑复杂,难以维护。写完这些函数明明可以顺手就重构掉的,但是没有重构,反而直接提交到代码库里,还过了cr,emmmmm。。。。我想了想,应该是没有相应的单测导致重构的成本略高。打磨代码和重构代码的同时,应该运行单测,保证每次重构的结果都是可以通过单测的。
最重要的原则之二:只做一件事
一个函数应该只做一件事,那么这件事是什么呢?如果有多行语句和步骤,如何判断这些代码是否属于一件事呢?
《代码整洁之道》提到:如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出来一个函数,该函数不仅只是单纯地重新诠释其实现。另外,只做一件事的函数无法被合理切分为多个区段。
抱歉,工作上遇到的代码几乎很少有遵守这个规则的。
原则三:每个函数一个抽象层级
应该自顶向下地阅读代码。程序就像是一系列To起头的段落,每一段都描述当前抽象层级,并引用下一抽象层级的后续To起头段落。这是保持函数短小、确保只做一件事的要诀。
——《代码整洁之道》
抱歉,工作上遇到的代码几乎很少有遵守这个规则的。
原则四:switch语句怎么处理?
我们要确保每个switch都埋藏在较低的抽象层级,确保不被重复。可以利用多态来实现这一点。
比如将switch语句埋到抽象工厂之下,该工厂使用switch语句为Employee的派生类创建适当的实体,Employee 是一个抽象类,不同派生类的不同函数,可以借由接口多态地实现。
——《代码整洁之道》
目前在工作代码中没注意到明细的case,后面会补充上这里。
原则五:使用描述性的名称
使用描述性的名称,可以名称较长,但是命名方式要保持一致
原则六:函数参数
函数的参数应该尽量少。
6.1 一元函数的普遍形式
向函数传入单个参数只能有两个普遍理由:询问关于参数的问题或者是操作该参数。
6.2 标识参数
如果函数存在一个标识参数,那就把它拆成两个函数。
6.3 二元函数
可以通过组合等方式,减少二元函数的参数,使之变成一元函数。
6.4 三元函数
三元函数的参数太多,不友好。写之前要考虑清楚。慎写三元函数。
6.5 参数对象
如果函数有两个、三个甚至更多参数,应该考虑封装成类。
原则七:无副作用
不要在函数中干一些与函数主题无关的。尽量简单清晰,不要给自己挖坑埋雷。
有时候会遇到这种坑。
原则八:分隔指令与询问
函数要么做某事,要么回答某事,二者不可得兼
原则九:使用异常替代返回错误码
因为错误码实际上应该是个枚举类,可能造成依赖磁铁。在这个枚举类发生变化的时候,经常需要重新部署相关的类。
工作上很多都是返回错误码,由于对错误码的改动不多,所以目前来看问题不大。
9.1 抽离try/catch代码块
try/catch代码块会搞乱代码结构,把错误处理与正常流程混为一谈,最好是把try和catch代码块的主体抽离出来,另外形成函数。
9.2 错误处理就是一件事
函数应该只做一件事,错误处理就是一件事。因此应该有一个函数专门在做错误处理。如果关键字try在某个函数中存在,它就应该是这个函数的第一个单词,而且在catch/finally代码块后面也不应该有其他内容。
实际工作中几乎没有代码遵守这一规则
原则十:别重复自己(DRY)
原则十一:结构化编程
只要函数足够短小,偶尔出现的break,continue和return语句没有坏处。但是goto语句禁用。
Section 3 如何写出好的函数
先写再打磨,一定要配上单测。
不用从一开始就按照规则写函数。