现在开始阅读《代码大全2》这本书。输出一些内容进行总结。
因为我是使用Java作为主要生产力的编程语言,所以我对很多代码大全里面的东西选择性的侧重Java中存在的情况。所以像第七章中对应的宏子程序、内联子程序这些我就没有很详细的理解,不做多余的解释。各位看到认为我写的有什么的问题或者错误点,欢迎评论、私信进行讨论。大家共同进步,共勉!
什么是子程序
一个为特定的目的而编写的可调用的方法或者过程。对于Java来说,这个比较统一就是一个方法。
创建子函数的正当理由
理由有千千万,怎么好用怎么算。
我们最常见的一个理由,多处出现了一样的代码操作,为了方便复用,减少修改。
- 例如:当一个userService内出现了很多的一样的检查用户名、密码传参的操作,可以将这些操作封装为一个同一个的私有方法,或者工具类。
第二个理由比较好想到的就是抽象。
- 也就是对于子程序名和具体实现的抽象。也就是说你封装一个方法,你可以不用考虑这个方法是怎么具体实现的,只需要在方法名称中体现具体的意义即可。这样在代码阅读的情况下你就不要去管理这些和你本次迭代无关的代码了。
和第二个同样的含义的就是,降低代码的复杂度。我们一旦将所有的业务代码按照流水帐的方式去写这个代码。就会出现后续代码越来越难维护。无法了解部分变量的具体修改是在哪里等情况。
具体的业务含义,这个主要是对于一些看似很简单,但是能隔离业务的封装了。
支持重写、重载。这个主要是表现在继承的时候,因为我们很多时候会出现一个情况。
比如:在Person类中,会存在一个相对通用的行为,是eat。但是亚洲人的实现是用筷子吃作为common的方法,但是到了啊三哥的时候他们执行的方式就是用手了,这个比较特殊。
如果我们是在流水账中,可能就是复杂的if else判断了。但是封装了以后可以在亚洲人的实现当中写对应eat重写实现,去使用筷子。在阿三哥中重写eat为使用手实现。
隐藏顺序,隐藏指针操作。这个我其实也没有很明确的案例,但是的确是存在这样的情况。这个的核心其实是在做一些复杂的操作的时候。你进行了封装以后我就不需要知道对应的一些与我核心业务无关的代码的执行顺序和对应的指针操作。这样反而让主流程的代码变的更加的简洁明了。
提高可移植性,性能优化。
- 这里的主要是将不可移植的代码聚合在一起后,后续你需要进行代码移植的时候可以更好的分析移植风险和成本计算。提高了其他代码的可移植性。
- 性能优化主要是你的业务代码进行了子程序封装以后,你可以分析当前这个子程序的性能,而不再是整个大的业务接口的性能,这样有利于我们去进行这个业务的性能优化。
子程序层的设计
内聚性说明
这个部分可以理解为是《代码大全2》担心我不会内聚性相关的知识点,给我进行一些简要的知识补充。万分感谢《代码大全2》[狗头].
- 功能内聚
- 顺序内聚
- 通信内聚
- 临时内聚
其中功能内聚是比较好的内聚了,顺序内聚其实可以将其分离为两个子程序,有利于功能上的内聚。通信内聚就是两个通信操作使用了同一个数据出现的。通信内聚可以利用获取数据的方式优化从而提高子程序的内聚性。临时内聚就一些因为需要一起执行的操作从而出现的内聚,这个是最不可取的。如果是无法避免的,应该在子程序名称和注释上进行说明。
好的子程序名称
这个在我认为是封装子程序需要做的第一步,也是最有意义的一步。因为我们会发现很多子程序的最大的问题导致很多人不愿进行封装的核心原因就是因为名字起的不好,让人无法理解这个子程序的具体作用,到时代码需要浪费更多的时间去理解。下面写一下《代码大全2》中写的几个原则和一些我的理解。
- 好的名称一点是描述了子程序做的所有事情的。
- 这里说明的就是单一职责问题了,我们最担心的也是你不能一下子看出这个子程序做了什么,所以名称上就需要说明子程序所有的行为。这里其实和下面的长度有一个互相限制的状态的,就是在一定的情况下,为了更好的说明子程序执行的具体内容,可以牺牲长度的限制。
- 为什么说和单一职责有关呢?因为单一职责才能剪短你的名称说明。
- 避免无意义,模糊不清的动词。
- 这里最重要的其实是二义性,比如使用一个 check的动词 fun checkProduct (),这里的check其实有很多含义的,你可以说明校验参数、可以是检查数据库中的product信息。还可以是制止、托运等动词含义。这里应该使用下面要求的语气强烈的动词这个限制。
- 尽量不要使用数字来形成不同的子程序名称。
- 这个主要是表现在对不同情况的处理的时候偷懒使用1、2、3等方式去区分子程序。这里应该遵循第一个要求,说明子程序该做的事情。
- 使用合适的长度
- 因为过长的子程序名称不利于开发人员去理解对应的具体操作,也代表了这个子程序负责的内容过多。
- 给返回值一定的描述
- 给我们明确的返回时会得到什么东西,可以有利于第三方调用我们的方法时理解我们的封装。
- 使用语气强烈的动词+宾语的形式。
- 这个主要的效果是能更加清晰的表达你想要表答的具体操作和对应对象,避免模糊不清的语句或者存在双重否定的情况。
子程序代码长度
- 使用合适的长度。
- 这个在我们的习惯里面可能听过每个子程序不超过35行。阿里巴巴给的规范是80行。但是《代码大全2》中给的是30行,也就是一屏的长度,这个就是可以不拉动阅读条的情况下,直接看完子程序的全貌。
- 我会认为优先执行《代码大全2》的方式,然后我在没有办法避免的时候才根据阿里巴巴的规范去限制为80行。也就是《代码大全2》是追求,阿里巴巴开发手册是底线。
- 同时一个子程序如果超过200行其实无论是可读性还是迭代的维护都会出现问题。
如何使用子程序参数
这个主要是说明我们如何进行子程序的传参,包括一些参数命名规范、使用规范。
按照 ~ 输入,修改,输出顺序。
- 这个主要是按照一定的顺序进行传参,这样有利于系统子程序中的统一和和谐。
保持类似子程序的入参统一性。
- 就是作为类似作用的子程序,应该考虑保留相同的参数顺序。
这里面分为两块,第一个是非重载类型。在这个时候如果是很类似的两个功能的子程序,那么他们的入参顺序应该尽量保持一致。如:getProductByNo(String productNo, int status); 当你出现一个和类似的操作时 getProductByName的方法时,传参是 name、status,就应该保持一致: getProductByName(String name, int status); 而不是: getProductByName(int status, String name);
因为在相同顺序的情况下,调用和理解你封装的子程序都是最低成本的。
使用所有入参。
- 这里面就一句话,不要进行多余传参。
将状态,出错变量放在最后。
- 这个我感觉在Java上更多的是具体业务的体现了。不然剩下我看到的应该Assert这些断言类的封装会有这个形式,因为我们出错变量这些传递的比较少,但是如果把他放在最后面的时候就可以考虑没有对应的输出了。也就是我们需要的就是处理这个状态、出错变量。状态也可以理解为 type,这种情况下我认为可以放在属于 输入的最后面,这样有利于我们使用时的一个提示作用。
不要直接使用入参作为工作变量。
- 这里主要是考虑到两个问题。
- 第一个是比较正常的这个参数可能你在子程序前几个步骤是要修改的,但是后面又要使用原始值,所以直接进行一个方法内的本地变量进行具体的操作其实更合适,特别是有些参数你可能输入值需要一次,但是具体的实现里面你可以实现为多个含义的,这样你分别生成对应的局部变量比你直接使用要清晰很多。
- 第二个是语义问题,因为你可能一些参数是输入参数,但是最后返回值也是对应这个参数,这个是不利于我们与理解对应语义。将变量局部化,那可以理解为输入输出含义。
- 虽然这个没有那么强制,但是也是能提升我们亿丝丝代码质量的。
- 这里主要是考虑到两个问题。
增加一些对参数假定性说明。
- 这个主要的意思就是断言,这样有利于你在后续使用这些变量时,不要担心说会在开发测试时没有问题,但是在线上出现严重的异常情况。Java中最普遍的就是NPE的判断了。
参数个数不大于7个。
- 这里面有的问题就是就一个到底面向对象还是面向过程的问题了,你如果面向过程。这个子程序需要多少个参数,就直接传参。而不考虑所有可读性,就会出现一个问题。后续这个子程序的更新就会变成基本无人能实现,调用也将变得无人愿意。因为你如果传了十几个参数,同时相邻的参数数据类型也是一样的,那我调用或者修改的时候,可能要大量的精力去判断我的传参问题。
- 这个在阿里巴巴的规范好像是5个参数。大于这个就要封装对应的 pojo (dto)了。这样我们可以直接理解你的pojo是怎么注释,加上不会限制我在对pojo的赋值顺序问题。一下子问题就迎刃而解了。而且pojo的命名也可以望文生义,这更加有利于我们去维护。
增加参数对输入,修改,输出类型说明。
- 这个主要是增加一下前缀、后缀。比如:i_、e_、o_去标识你是还说输入-修改-输出中哪个类型的参数。更好区分参数的使用。
参数应该保持为抽象或者接口。
- 这个主要是出现在源程序中为大对象,但是在子程序只需要用其中部分参数的情况如何对子程序进行传参的问题了。我个人认为,与应该与参数个数同步更合适,但是不是直接将整个大对象传递过去。