如何保持代码整洁——可维护
程序从结构来说由类、函数、包、变量、注释组成,从功能来说由实现类、测试类、依赖管理、打包部署、持续集成组成,从模式来说由架构风格、设计模式组成,这各个方面都是保持代码整洁——可维护的入手点。
函数
1.有准确恰当的命名:通过命名准确地告诉阅读者这个函数做的是什么,且只包括所描述的功能,这意味着在函数名的表达对实现来说,既不空泛,也不会不足(在实现中暗藏着从名字看不出来的功能,如get方法里面却有add/save的逻辑)。这准则能帮助识别函数职责。函数名通常由动词+名词组成。同时在整个系统中,对于同样的操作应该有统一的名字,如save/insert/add这些含义差不多,那么就应该在团队中统一使用或明确定义它们的使用场景,不应该同样一个意思,在这里叫add,在那里就save。命名同时应该考虑到更符合技术人员的专业术语,比如工厂类就叫Factory,这样很容易理解,因为这个概念和这个名词用的很普遍,如果用Provider就不如Factory本身的含义清晰。
2.每个函数都只做一件事。完成一个功能是从上到下不断地由粗到细来完成的,但在一个函数中需要保持只描述同一个抽象层级的内容,如果其中含有细节层次的部分,那么这是应该将这细节提取出一个方法的时候了。所以对于较粗级别的函数内,应该是由一系列方法组成。
3.函数应该尽量短。如果函数较长,那么很可能就是将不同抽象级别的代码混杂在一起了,或者是做的不是一件事,需要抽取。
4.按照从上到下的级别排列函数。由于人的通常习惯是从上往下阅读,所以当写了一个函数之后,它内部调用的函数应该按照出现顺序排列在它的下方。另外又有一种划分方法是将重载函数/关联函数放在一起,综合这两种折衷考虑,首先按照内部调用函数向下排列,如果出现重载函数,则将它们放在一起。传统的先public再protect再private这种方式并不符合人的阅读习惯。
5.函数的参数应该不超过2个。从测试的角度出发,函数参数越多,那需要构造的scenario就越多,造成复杂性,所以最好的是没有参数,然后是1个,做多是2个,当参数超过2个的时候,就要考虑是否应该把一些参数封装成对象了。
6.当若干函数都使用同样的参数时,就应该考虑应该把它作为成员变量。因为这意味着该类和此参数的关联性比较强,那么就有可能是它的固有属性。当不断这样提取发现类的成员变量过多的时候,那么很可能就是该类的职责过多,是该考虑对其拆分的时候了。
变量
1.变量的命名取决于其作用域,在大多数情况下应该准确描述,并且作用域越广那就应该越清晰。比如public static,那么就要明确的描述它是什么,如MAX_ACCESS_LIMIT;如果在通常的for循环中,i,j,k这些是通常用法,用其来描述索引也很恰当,此时如果用index反而有些没那么清晰。
2.类成员变量如果过多那么就要考虑应该把当前类拆分成多个类,因为这往往意味着职责不清。
3.类成员变量的顺序应该是:public static,private static,public,private这样的顺序,且挨在一起更紧凑清晰,一眼就能看出所有的成员变量。
注释
1.当通过程序/命名本身无法描述所做的事情的时候才需要注释。注释是最容易被滥用的部分,自己以前也曾经以长注释为目标。注释最大的问题就是过时,当注释不准确会误导阅读者的时候,它所造成的危害比没有注释更大,所以应该尽量不要注释,通过良好的函数名和结构来清晰和准确表达。
通常不应该有的注释包括:
. 只描述自身实现做的是什么事情的注释。
. 毫无意义的注释。如在成员变量上加仅是用来满足检查的注释。
. 纯个性、喃喃自语的注释。这种情况比较少,纯粹是作者在写的时候随便写了几个字,娱乐一下,后面又忘记删了。
. 不需要阅读者关注的细节。
. 维护代码时表明修改者是谁的注释(潜在意思是将来阅读者可以找它),但随着时间变迁,修改的部分早已被数次修改,面目全非,很少有人需要用到这种注释。
. 删除代码时使用注释而不是删除,防止将来需要改回来。这对代码造成很大伤害,这部分的找回工作应该由版本控制工具来完成。
但绝不能由上面这些来完全否定注释,有些注释是必须的,除了版权信息之类从商业角度出发的要求之外,主要是通过代码/名称无法表达出来的是有必要添加注释的,包括:
. 我为什么要这么做。程序只能表明当前是怎么做的,那么在有多种选型的时候,我为什么选择这种办法而不是其他办法呢?这种思考过程(意图)是应该注释的。
. 这里阅读者需要注意些什么。为什么用concurrentHashMap而不能用HashMap?这是需要阅读者注意不能随意修改的,或者修改需要考虑些其他因素的,属于放大风险/提醒的作用。
. todo:这种注释是必要的,把当前未做的事情记录下来,但要经常检查todo列表。
有一点必须要说明的是,对于公开的API,如jdk、开源框架这些,由于需要很多外部的,对它并不了解的人来阅读,所以注释写的很详细,包括对接口本身的阐述,举了很多例子,否则调用者就很难知道该如何用,当然他们的作者也承担着维护这些注释的准确性的义务。而对于自身系统,不存在开放性,也没有那么复杂,此时权衡维护性来说,注释的使用就要相当限制了。
包
包一般按照职责来分,如domain、service、controller,或者按照业务来说,如member(成员模块)、analyse(分析模块),不要混杂在一起。如果较为复杂,可在每个职责包里再按照业务分,或者在每个业务包里再按职责分。总之整个项目组保持统一风格很重要。
缩进
缩进也是很重要的一部分,比如在操作符两端留有空格,保持每行最大长度(120是上限),由于现在有很好IDE和格式化工具,这不成问题,只要项目组在项目之初确定统一风格就可以了。
类
对于面向对象而言,类的最重要的几个设计原则分别是:SPR(单一职责原则)、OCP(对扩展开放对修改封闭)、DI(依赖倒转)。关于类的设计有很多复杂的模式,上述这些对函数、对变量的方法能够反过来改进对类的设计。单一职责强调一个类只做一件事,也就是说只有一种变化会导致此类相应变化,反过来,如果存在多种变化因素导致此类发生变化,那么这个类的职责就是不清晰的。特别要注意的是,“多种变化”是事实上发生或预期会发生的,不是臆想出来、不合实际的变化。成熟的抽象和抽象本身一样重要。
另外一个关注点是面向切面。要使贯穿于大部分业务、同时又与业务不是很有关的功能,使用动态代理来以面向切面的思路剥离出来,也就是AOP。如事务处理、日志记录、异常监控/捕获。
异常
异常在部分情况下是必要的,在部分情况下阻碍了整个程序的流程。对第三方或其他无法控制同时也没有恢复可能的check-exception进行封装转化为uncheck-exception,然后在程序顶层通过AOP来捕获所有异常进行统一处理,这样可以使程序的流程清晰很多。
在需要try/catch/finally时,为了防止它们对可读性进行干扰,那么可以把try中的部分提取出一个函数,这样就把实现和异常流明显地区分出来了。
null值是很多罪恶的根源,所以在设计方法时,不应该返回null,以相应的特殊类来代替。比如返回元素数量为0的List、返回金额为0的实体对象,如果无法进行这样的转化,则意味着可以剥离出is类的true/false判断的方法,通过控制流来保证不会返回null。
测试
测试和实现一样重要。没有实现代码,功能就不能完成;没有测试,功能就不能正确持久地正确。随着测试越来越多,对测试代码的维护成为整个项目维护的重要部分。设想测试如果无法维护,就会抛弃它,然后导致功能正确性无法得到保证,无法进行持续的改进,整个项目就会进入无法维护的地步。
测试代码的几条原则包括:
1.每个测试方法只测试一个验证点。通过测试函数名表明当前测试的是什么,通过清晰的given/when/then表明当前输入的参数、执行的操作和验证的结果。如果一个测试方法测试了多个验证点,表明这个测试该拆分了。如果given比较复杂,那么可以把given抽取到setup或抽取成单独private函数。但这并不意味着一个测试函数只能有一个assert,这会造成严重的given/when的重复。
2.测试应该能够快速执行。测试是需要经常执行以保证重构正确、或者新写功能能通过测试(TDD的办法是先写测试并运行使其不通过,然后编写实现,然后再运行测试直到它通过)。如果不能快速执行,那么就会导致重构的成本增大,进而疏忽或放弃不停的重构。
3.每个测试应该相互独立,不应该影响到彼此。换句话说,所有测试函数可以以任何顺序执行。
4.测试必须是自己能够验证的,不应该通过手工或肉眼的方式来验证。
5.测试是可以重复执行的。
综合以上几点,有个F.I.R.S.T原则。Fast,Independent,Repeatable,Self-Validating,Timely.