目录
前言
一、软件质量
二、高质量的代码
1、编程价值观
2、代码坏味道
3、代码检测工具
4、编写高质量代码技能,为什么创建函数?
5、函数的复杂性度量
6、编写高质量的函数
三、修改旧系统的代码
前言
作为软件开发者
软件编码质量是作为一名开始写代码的程序员必备的一门基础技能,更贴切的说应该是程序员的一种习惯。在写完一段代码保证代码的正确之后,对代码进行自我审查、代码重构是作为一名有良好编程习惯的程序员编写程序的一个重要环节。
构造软件设计有两种方法:一种是简单,明显地没有缺陷;另一种方法是使其复杂,却没有明显的缺陷。-----Tony Hoare
代码的编写应该尽可能的简单、清晰,让代码所要表达的事情一目了然,这不管是对后期维护还是功能扩展都是至关重要的。
作为项目管理者
软件的质量不仅只包括项目功能完成进度情况等这些外在的质量,同时组成软件质量的更大部分是软件代码的内在质量。软件的外在质量就像冰山显露在海面上的一角,而海水下面的部分才代表着软件的内在质量。在软件开发初期,作为项目管理者就应该对软件内在质量足够的重视,把这种重视传达给每一个开发者,并制定提高软件质量的办法及管控措施。
总结
不管是作为开发者还是管理者,都应该知道和掌握。
学习要点:
1.什么样的代码是坏代码?
2.如何编写好的代码?
3.如何对编写完的代码进行重构?
4.如何对旧系统代码进行修改?
名句:培训的本质,不在于记住哪些知识,而是在于它触发了你的多少思考,一旦我们开始反思我们的工作,工作将会不再一样。
一、软件的质量
软件同时拥有外在的和内在的质量特性。
外在的质量指该产品的用户所能够感受到的部分;包括:正确性、可用性、效率、可靠性、完整性、适应性和健壮性。
质量的外在特性是用户关心的唯一软件特性。
程序员除了关心软件质量的外在特性之外,还要关心它的内在特性。
内在的质量包括:可维护性、灵活性、可移植性、可重用性、可读性、可测试性和可理解性。
软件的内在质量将影响外在质量。
软件的设计:编程前做设计这种思路是没错的,然而设计后不应该就认为该模型就是任务的最好设计。你会发现最好的设计是你在编码阶段,一步一步逐渐形成的。
软件质量像冰山一样,外在质量往往只是冰山之上看得见的一小部分,内在质量才是占据软件质量的大部分。
二、高质量的代码
1、编程的价值观
编程是一种态度!
编程是一种技艺!
编程是一种习惯!
价值观决定行为!
良好的程序员首先应拥有积极的态度:编写程序时一心一意,专注,怀有谦卑的心态,这是能够编写出高质量的代码,成为一名良好程序员的基础。
编程同时也是一种技艺:拥有了积极的态度之后,同时要也通过不断学习,让自己技艺更强更精!
我不是什么伟大的程序员,我只是一个有着很多好习惯的程序员--------Kent Beck
编程还是一种习惯:让自己通过学习好的编程习惯,从特意要求自己遵守到这些好习惯成为自己的习惯。
2、代码的坏味道
什么是高质量的代码:Good code is not bad code!(好代码不是坏代码)
识别代码坏味道,是创造好代码的重要一步。22种经典代码坏味道--参见《重构》,常见的代码坏味道如下:
a)重复代码(处理:通过提取方法实现复用)
b)过长方法(处理:一个函数只做一件事;公有方法代码应该目录化,让人一目了然,让代码遵守单一抽象层次原则,私有方法建议不超过50行)
c)过长类(处理:通过将方法抽离到新的类中进行调用,类中方法嵌套层次不应超过5层)
d)过长参数列表(处理:建议不超过5个)
e)注释过多(处理:通过代码进行自我解释;只有在不太成熟容易出错的地方才应该加注释。如高速公路上只有急转弯才会有警示牌)
f)变量(变量含义不断切换,一个变量代表多个意义。处理:一个变量只有一个含义)
g)函数命名含义无法理解
h)复杂的表达式,不易理解(处理:一行代码只做一件事。将复杂表达式通过简单明了的方式进行表达)
i)复杂的条件判断(处理:尽量用肯定句,去非判断,拆分多个if,提取成函数,表驱动解决多条件,解释变量代替计算操作变量)
代码的坏味道有很多很多,我们可以依据二八原则:
二八原则:任何一组事物中,最重要的只占其中约20%,其余的80%虽然是多数,但是却是次要的。
在项目开发开始阶段,项目管理者或程序员自身应将编写代码过程中最重要的20%的代码坏味道记住,避免开发过程中编写出有坏味道的代码。
例如制定程序员守则以及编程规范,时刻提醒自已,让编写良好代码从特意到变成自己习惯:
3、代码检测工具
a)PMD:静态分析工具PMD
质量是衡量一个软件是否成功的关键要素。而对于商业软件系统,尤其是企业应用软件系统来说,除了软件运行质量、文档质量以外,代码的质量也是非常重要的。软件开发进行到编码阶段的时候,最大的风险就在于如何保证代码的易读性和一致性,从而使得软件的维护的代价不会很高。
在软件开发的过程中,以下几种情形随处可见:
1) 软件维护时间长,而且维护人员的积极性不高:
做过软件维护的开发人员,尤其是在接手不是自己开发产品的源码的时候,即使有良好的文档说明,仍然会对代码中冗长、没有注释的段落“叹为观止”。理解尚且如此困难,何况要修改或者增加新的功能。因此,很多开发人员不愿意进行软件维护的工作。
2)新的开发人员融入团队的时间比较长:
除了没有良好的培训、文档等有效的机制以外,每个人一套的编码风格,也容易造成新成员对于已有代码的理解不够,甚至出现偏差。
提高代码的质量,除了要提高逻辑上的控制以及业务流程的理解外,代码本身也存在提高的空间,例如一些潜在的问题可以很早的就避免。类似于编码规范上的内容,如果全靠编码人员进行自行检查,那么无疑需要很大的工作量,如果可以使用代码的静态检查工具进行检查的话,那么将大大的提高编码的效率。
PMD正是这样一种工具,可以直接使用它自带的规则(当然也可以使用自己的规则)对Java源程序进行分析找出程序存在的问题,可以很大程度上的减轻代码检查工作的繁琐,为项目组今后的维护和开发工作起到指导的作用。
PMD是一种开源分析Java代码错误的工具。与其他分析工具不同的是,PMD通过静态分析获知代码错误。也就是说,在不运行Java程序的情况下报告错误。PMD附带了许多可以直接使用的规则,利用这些规则可以找出Java源程序的许多问题,例如:
® 潜在的bug:空的try/catch/finally/switch语句
® 未使用的代码:未使用的局部变量、参数、私有方法等
® 可选的代码:String/StringBuffer的滥用
® 复杂的表达式:不必须的if语句、可以使用while循环完成的for循环
® 重复的代码:拷贝/粘贴代码意味着拷贝/粘贴bugs
® 循环体创建新对象:尽量不要再for或while循环体内实例化一个新对象
@ 资源关闭:Connect,Result,Statement等使用之后确保关闭掉
此外,用户还可以自己定义规则,检查Java代码是否符合某些特定的编码规范。例如,你可以编写一个规则,要求PMD找出所有创建Thread和Socket对象的操作。
安装PMD
® 启动Eclipse
® 选择Help-->Software Updates-->Find and Install
名称:PMD
地址:https://dl.bintray.com/pmd/pmd-eclipse-plugin/updates/
使用PMD
1、启动Eclipse IDE,打开工程,选择 "Windows"->"Preferences"下的PMD项,其中Rules Configuration 项目可以配置PMD的检查规则,自定义检查规则也可以在此通过Import的方式导入到PMD中
2、配置好后,鼠标右键点击工程中需要检查的JavaSource,选择"PMD"->"Check Code With PMD" ,之后PMD就会通过规则检查你的JavaSource了并且将信息显示在PMD自己的视图上
PMD检查代码的一些规则
b)Sonar
Sonar是一个代码质量管理的开源平台,用于管理源代码的质量,通过插件形式,可以支持包括java、C#、JavaScript等二十余种编程语言的代码质量管理与检测。
Sonar理论篇https://blog.csdn.net/qq_26545305/article/details/70224916
Sonar实战篇https://blog.csdn.net/qq_26545305/article/details/70231629
c)sourceMoniter
SourceMonitor是一款免费的软件,运行在Windows平台下。它可对多种语言写就的代码进行度量,包括C、C++、C#、Java、VB、Delphi和HTML,并且针对不同的语言,输出不同的代码度量值。
SourceMonitor的学习和使用https://blog.csdn.net/gantleman/article/details/51351649
4、编写高质量代码技能,为什么创建函数?
创建子程序的理由-1-减低复杂度
降低复杂度:创建子程序的一个最重要原因,就是为了降低程序的复杂度。可以通过创建子程序来隐藏一些信息,这样你就不必再去考虑这些信息了。
创建子程序的理由-2-职责单一
职责单一:函数应仅实现应有的功能,而不必实现其他功能(内聚)。
创建子程序的理由-3-命令与查询分离
命令-查询分离原则(CQRS),是指一个函数要么是一个命令来执行动作,要么是一个查询来给调用者返回数据。但是不能两者都是。
CQRS模式最早由著名软件大师Bertrand Meyer(面向对象开-闭原则OCP提出者)提出,他认为,对象的行为仅有两种:命令和查询,不存在第三种情况。用他自己的话来说就是:“提问永远无法改变答案”。
一个方法要么是执行某种动作的命令,要么是返回数据的查询,而不能两者皆是。换句话说,问题不应该对答案进行修改。更正式的解释是,一个方法只有在具有参考透明性时才能返回数据,此时该方法不会产生副作用。
参考:浅谈命令查询职责分离(CQRS)模式
创建子程序的理由-4-封装变化
可维护性是目标:修改错误/替换时引起的改动不会影响其他代码,因为只有一个函数需要修改,不会碰到其他代码。函数隐藏可以改变的(封装)。
函数实现信息隐藏:a)隐藏复杂度,这样你就不用再去应付它,除非特别关注的时候;b)隐藏变化源,这样当变化发生时,其影响就能被限制在局部范围。
创建子程序的理由-5-引入中间的,易懂的抽象
引入中间的,易懂的抽象:a)把一段代码放入一个命名恰当的子程序内,是说明这段代码用意最好的方法之一;b)代替注释。
创建子程序的理由-6-单一抽象层次原则SLAP
单一抽象层次原则SLAP:让一个方法中的所有操作处于相同的抽象层。
创建子程序的理由-7-主程序过程序列化步骤
a)类的业务逻辑方法,形成序列步骤。增加小的,私有的方法来使计算的主体部分表达得更简明。
b)助手方法:通过暂时隐藏目前不关心的细节,让你得以通过方法的名字来表达意图,从而令大尺度的运算更具可读性。
创建子程序的理由-8-避免代码重复
a)创建子程序的原因最普遍的原因是为了避免代码重复。应该把两段子程序中的重复代码提取出来,将其中的相同部分放入一个基类,然后再把两段程序中的差异代码放入派生类中;你也可以把相同的代码放入新的子程序中,再让其余的代码来调用这个了程序。
b)代码改动起来也更方便,因为你只需要在一处修改即可。这时的代码也会更加可靠,因为验证代码的正确性,你只需要检查一处代码。同时,这样做也会使改动更加可靠。
创建子程序的理由-9-简化复杂的布尔判断
a)为了理解程序的流程,通常并没有必要去研究那些复杂的布尔判断的细节。应该把这些判断放入函数中,以提高代码的可读性,因为这样就把判断的细节放到一边了,一个具有描述性的函数名字可以概括出该判断的目的;
b)把布尔判断的逻辑放入单独的函数中,也强调了它的重要性。这样做也会激励人们在函数内部做出更多的努力,提高代码的可读性。最终,代码的主流和判断代码都变得更加清晰。
创建子程序的理由-10-转换/适配
对现有的函数进行转换,也许功能并没有增加。
创建子程序的理由-11-不同API-重写/重载
同样的功能的、不同的API、方便客户的容易调用。
创建子程序的理由-12-提高可移植性
可以用子程序来隔离程序中不可移植的部分,从而明确识别和隔离未来的移植工作。
创建子程序的理由-13-占位函数
待实现的方法/方式--TODO
创建子程序的理由-14-方便测试函数
利于单元测试
创建子程序的理由-15-封装全局变量操作
隐藏全局数据:如果直接使用全局数据,就可隐藏到函数内。与直接使用全局数据相比,当需求变化时,你可以修改数据结构而无须修改程序本身。
创建子程序的理由-16-递归
创建子程序的理由-17-遗留系统增加新功能
分离关注点,解决缠绕:把Base和Extension隔开。
创建子程序的理由-18-改善性能
改善性能:通过使用子程序,你可以只在一个地方优化代码。把代码集中在一处可以更方便地查出哪些代码的运行使用到该子程序,使用到该子程序的所有代码都能从中受益。
创建子程序的理由-19-隐藏顺序
隐藏顺序:把处理事件的顺序隐藏起来是一个好主意。假设你写了两行代码读取栈顶的数据,然后减少stackTop变量的值。你应该把这两行代码放到一个叫popStack()的了程序中。从而把这两行代码所必须执行的顺序隐藏起来。把这种信息隐藏起来,比让它们在系统内到处散布要好很多。
创建子程序的理由-20-支持子类化
支持子类化:覆盖简短而规整的子程序所需新代码的数量,要比覆盖冗长而邋遢的子程序更好。如果你能让可覆盖的子程序保持简单,那你在实现派生类的时候也会减少犯错的几率。
5、函数的复杂性度量
有能力的程序员会充分地认识到自己的大脑容量是多么地有限;所以,他会非常谦卑地处理编程任务。 -------Edsger Dijkstra
简单函数复杂度度量方式:
a)代码行数(私有方法建议<50)
b)函数参数个数(传入参数建议<5)
c)调用其他函数/对象/包的数量
d)每行运算符的数量(建议一行代码只做一件事)
e)调转语句个数(goto/break/continue/throw)
f)控制结构中的嵌套层数
g)变量个数(临时变量和全局变量,建议变量数量<7)
h)同一个变量的先后引用之间的代码行数(跨度)
i)变量生存的代码行数
j)递归
k)函数出口数量(return)
l)注释比例
m)分支语句比例
圈复杂度:
圈复杂度是一种度量方法,由Thomas McCabe于1975年定义。圈复杂度是一个方法中执行路径的数量。
圈复杂度的度量方式定义:
a)从进入方法开始,一直往下通过程序;从进入方法,加1;
b)一旦遇到以下关键字,或者其他同类词,加1(if/while/for/and/or);
c)给case语句中的每一种情况加1;
d)三元运算符a?b:c加1;
e)给catch语句加1
正常的程序员看上去很难处理好5~9个以上的智力实体,并且提高的可能性不大,因此你只有减低你程序的复杂度。 ------Miller 1995
圈复杂度与可维护性关系表
圈复杂度<10 修复不成功率5%
圈复杂度20~30 修复不成功率20%
圈复杂度>50 修复不成功率40%
圈复杂度接近100 修复不成功率60%
经过各种研究已经确定:圈复杂度大于10的方法存在很大的出错风险。
1-4 is low complexity(低复杂度),
5-7 indicates moderate complexity(表明适度的复杂度,),
8-10 is high compleity(高复杂度)
and 11+ is ver high complexity (非常高复杂度)
6、编写高质量的函数
代码的编写应当使别人理解它所需时间最小化。
例如常见简化复杂代码方式:
a)简化boolean判断:尽量使用肯定句;
b)使用卫语句,尽快返回,避免深层嵌套;
c)函数别返回null,别传递null,不如返回异常或返回特例空对象。(凡是可能出错的地方,终可能会有人出错,调用函数的地方必须对返回判空处理,否则将出错)。
d)命令与查询分离,检查与扩展分离
e)建议加大括号
公有方法尽量遵守单一抽象层次原则,代码目录化,按步骤层次化;公有方法注重流程,目录化简单明了。
私有方法单一职责、尽量短小,命令与查询分离;私有方法注重实现,代码<50行。
私有方法需要再提取子程序的情况:a)违反单一职责b)业务逻辑复杂c)嵌套层次超过3层
私有方法处理方法:
a)do/while语句尽量不要用,我们倾向于把条件放在“前面我能看到的地方”;
b)break,continue语句,使用时需要小心谨慎,尽量不要使用;使用break,continue可读性下降,容易出错;已经明确结束了,可以直接return,删除控制标记变量。
c)避免赋值和判断放在一起;
d)变量数量<7;变量作用域不要过大,避免变量改变频繁,一个变量多个含义。
e)分支尽量简单明了,合并分支可能出错,产生更多问题。
f)尽量避免空语句,最小意外法则:代码不要让人感到意外。
g)函数尽量短小,行数<50行。
h)函数短小三原则:要短小、还是要短小、必须要短小;
i)条件判断语句中尽量使用肯定句。
j)条件判断If语句里判断的维度要一致。
k)一个循环只做一件事,循环内是一件事事的多个步骤不能拆分。
简化if-else-if等多条件语句方法:
a)去非判断
b)解释变量,代替计算操作变量(你有一个复杂的表达式,将该复杂表达式的结果放进一个临时变量,以此变量来解释表达式的用途。)
c)拆分为多个if
d)提取函数
e)表驱动解决多条件(表驱动法是一种编程模式--从表中查找信息而不是使用逻辑语句(if或else)。如果逻辑很复杂,导致逻辑判断链很长,使用表驱动法有助于降低复杂度。否则,表驱动法只会增加复杂度。
可以参考学习Junit源码,学习大牛的编码方式。
单一抽象层次原则
让一个方法中的所有操作处于相同的抽象层。
过分深层的缩进,或者“嵌套”,已经困扰了计算机界达25年之久,并且至今仍然是产生混乱代码的罪魁祸首之一。
Noam Chomsky和Gerald Weinberg做过一份研究表明,很少有人能够理解超过3层的嵌套,很多研究员建议避免使用超过3层的嵌套。
解决深层嵌套:
a)使用卫语句。卫语句:如果某个条件极其罕见,就应该单独检查,立刻从函数中返回。如传入参数为null时,直接return返回。如果是if-then-else其各个分支是同样重要。
b)通过重复检测条件中的某一部分简化嵌套的if语句
c)用break块简化嵌套的if
d)把嵌套if转化成一组if-then-else语句
e)把嵌套if转换成case语句
g)把深层嵌套的代码抽取出来放在单独的子程序
h)使用面向对象派发
i)使用状态变量重写代码
j)使用异常
函数单一职责原则
过去30年来,以下建议以不同形式一再出现:
函数应该做一件事,做好这件事,只做这件事。
编写函数毕竟为了实现某些功能(也就是函数的名称),也就是拆分为一个抽象层次上的一系列步骤。
简易标准:如果函数只是做了该函数名下抽象层次的步骤,则函数还是只做了一件事情。
如果你不能把一件事解释给老奶奶听的话说明你还没有真正理解它。---爱因斯坦
函数语句无副作用:函数承诺做一件事,但还是会做其他被隐藏起来的事情。
将查询函数和修改函数分离:某个函数既返回对象状态值,又修改对象状态。建立两个不同的函数,其中一个负责查询,另一个负责修改。(CQRS)
函数单一职责检查步骤:
a)看看函数名称(或代码块)问问自已该代码目标是什么?
b)函数的执行步骤,抽象层次相同吗?
c)如果有足够的行数在解决不相关的子问题,抽取代码到独立函数。
d)对每一行代码,问一下:它是为了目标而工作吗?如果不是请提取。
e)函数每个语句有副作用吗?
函数短小原则
函数的第一原则:是要短小;
函数的第二原则:是还要短小;
函数的第三原则 :是必须要短小;
函数返回值:
a)如果能增强可读性,在可以return的地方,那么就使用return。
b)检查所有的返回路径,设置一个默认的返回值。
c)别返回null值,不如抛出异常,或者返回特例对象(NullObject)
函数的10个一:
1)每个变量只用于单一用途
2)每一行代码只表达一件事
3)一个循环只做一件事
4)单一抽象层次原则
5)代码组织得一次只做一件事(相关语句的组织放在一起,在函数的内部组织代码,使得感觉像有分开的逻辑段。)
6)一种变化的仅仅修改一处(变量修改只放在一处)
7)函数应该遵守单一职责
8)函数圈复杂度应该小于一十(简单)
9)函数第一原则是必须要短小(让人容易阅读)
10)编写函数必须一心一意,专注,怀有谦卑的心态。
每一行代码只表达一件事
即使你能轻松看懂有副作用的语句(复杂的表达式、复杂的条件判断语句等),你也应该照顾那些读你代码的人。多数优秀的程序员在理解带副作用的表达式时都会三思。最好让他们考虑你的代码的业务流程,而不是思考其中特殊语言表达式细节。
相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需要的时间。
每一行只有一条句的好处:
不要将多个语句放在同一行,除非你要掩饰什么!
a)各语句单独一行,则代码仅需自上而下读,而不必自左到右。当寻找某特定行的代码时,你只用盯着代码的左边界,不再由于某一行可能包括两条语句而去深入每一行。
b)将每个语句单独置于一行,能够提供有关程序复杂性的准确观点。应该让复杂的语句一看就是复杂的,简单语句一看就是简单。
c)各语句单独占一行,在编译器以行号指出某行有错误时,容易定位错误。如果一行有多条语句,行号并不能告诉你究竟是哪条语句出错了。
d)各语句单独占一行,在基于调试器中就容易单步执行代码。如果一行有多条语句,高度器就会一次执行这些语句。
e)各语句单独占一行,编辑单个语句也更容易删除一行或者临时将某行改为注释。
函数的SOFA原则
1、保持简单(Short),以便能迅速获取主要目的
2、只做一件(One)事情,以便测试能集中于彻底检查这件事情。一个变量/循环/函数只做一件事。
3、输入少量(Few)变量/参数/逻辑,以便非常重要值组合都能被被测试到。
4、抽象(Abstraction)层次一致,以便它不会在如何做和怎么做之间来回跳转。
三、修改旧系统的代码
代码修改准则:首先做到不伤害!
代码随时间变差的原因:需求变更或增加,不断硬塞代码(缠绕),不关注隔离,不知道怎么隔离。
当你对代码改动的时候,要从全新的角度审视它,因为后面维护这份代码的人的上下文环境会丢失。
每次我修改软件的时候,我会倍加小心,不让它变得更糟。
解决缠绕的方法:把Base和Extension隔开(新旧分离)
1)新生方法
2)新生类(将新增代码写入新编写的类或方法中)
3)外覆方法(直接新增方法或将原有方法名字修改,使用新编写的方法替换它)
4)外覆类(代理模式)
四、推荐书籍
1、《代码整洁之道》
2、《程序员的职业素养》
3、《程序员开发心理》
4、《重构》
5、《修改代码的艺术》