这篇文章是 夏桑老中医(巨人网络的帧同步大佬) 整理分享给我的,我觉得有必要分享一下,都是经验,大学的课本根本没有的。
1. 保持松散耦合
1.1 耦合度
表示类与类之间或者子程序与子程序之间的紧密程度。
1.2 松散耦合设计
创建出小的,直接的,清晰的类或者子程序,使它们与其他类或者子程序之间的关系尽可能的灵活。(如火车车厢之间的连接就是松耦合)
1.3 松耦合设计的标准
规模
是指模块之间的连接数,连接数越少,耦合度越低。
如只有一个参数的子程序与调用它的子程序之间的耦合关系比有六个参数的子程序与它的调用方之间的耦合关系更为松散。
再如包含4个公用方法的类与它的调用方的耦合关系,比包含37个公用方法的类与它的调用方的耦合关系更为松散。
可见性
指两个模块之间连接的显著程度。即让模块之间的连接变得广为人知而获取信任。
如通过参数传递数据来连接模块要比使用全局变量来连接模块的耦合度松散。
灵活性
指模块之间的连接是否容易改动。一个模块越容易被其他模块调用,那么它们之间的耦合关系就越松散。
如有一段程序,通过输入雇佣日期和工作级别来查询员工的年假,子程序被命名为
lookupVacationBenefit()。假设在另一个模块里已经有了一个 employee对象,其中包含了雇用日期和工作级别,以及其他一些信息,该模块把这种对象传递给 lookupVacationBenefit(),按照其他耦合标准,这两个模块之间的耦合关系看上去是松散的,这两个模块之间的连接使用参数 employee,是可见的,而且只有这里存在一个连接。
但假如你需要在第三个模块中使用 lookupVacationBenefit(),这一模块中没有 employee 对象,却包含雇用日期和工作级别,这样 LookupVacationBenefit() 一下子就变得不太友好了,它无法和新的模块协同工作,要在第三个模块中使用 LookupVacationBenefit(),就必须了解 Employee类。而且需要临时去拼凑一个只包含两个字段的 employee 对象。其实只有两个字段是LookupVacationBenefit() 所需的,这样的方案非常牵强,而且是否丑陋。
解决方案是对 LookupVacationBenefit() 做出修改,使他以雇员的日期和工作级别为参数,而不用employee对象。
1.4 耦合的种类
简单数据参数耦合 (可接受)
当两个模块之间通过参数传递数据,并且所有的数据都是简单数据类型的时候,这两个模块之间的耦合关系就是简单数据参数耦合。这种耦合关系是正常的,可以接受的。
简单对象耦合(可接受)
如果一个模块实例化一个对象,那么它们之间的耦合关系就是简单对象耦合。这种耦合关系也是可以接受的。
对象参数耦合 (比较差的耦合)
如果object_1要求object_2传给它一个object_3,那么这两个模块就是对象参数耦合。与仅传递给它简单数据类型相比,这种耦合关系要更紧密一些,因为它要求object_2了解object_3。
语义上的耦合 (非常危险,紧耦合)
最难缠的耦合关系是这样发生的:一个模块不仅使用了另一个模块的语法元素,而且还使用了有关那个模块内部工作细节的语义知识。
例如:
例1: Module1 向 Module2 传递了一个控制标志,用它告诉 Module2 该做什么。这种方法要求 Module1 对 Module2 的内部工作细节有所了解,也就是说需要了解 Module2 对控制标志的使用。如果 Module2 把这种控制标志定义成枚举类型,那么这种使用方法还说的过去。
例2:Module2 在 Module1 修改了某个全局数据之后使用该全局数据。这种方式就要求 Module2 假设 Module1 对该数据所做出的修改复合 Module2 的需要,并且 Module1 已经在恰当的时间被调用过。
例3:Module1 的接口要求它的 Module1.Initialize()子程序必须在它的
Module1.Routime() 之前得到调用。Module2 知道 Module.Routime() 无论如何都会调用 Module1.Initialize(), 所以它在实例化 Module 之后只是调用了 Module1.Routime(),而没有先去调用 Module1.Initialize()。
例4:Module1 把 Object 传给 Module2。由于 Module1 知道 Module2 只用了 Object的7个方法中的3个,因此它只部分的初始化 Object(只包含那3个方法所需要的数据)。
例5:Module1 把 BaseObject 传给 Module2。由于 Module2 知道 Module1实际上传给它的是 DisplayObject,所以它把 BaseObject 转换成 DisplayObject,并调用了 DisplayObject的特有方法。
语义上的耦合是非常危险的,因为更改被调用模块中的代码可能会破坏调用它的模块,破坏的方式是编译器完全无法检查的,因此使得调试工作变得非常困难。
松散耦合的关键之处在于,一个有效的模块提供出了一层附加的抽象,一旦你写好了它,你就可以想当然的去用它。这样就降低了整个系统的复杂度,使得你可以在同一时间只关注一件事。如果对一个模块的使用要求你同事关注好几件事情(起内部的工作细节、对全局数据的修改、不确定的功能点等),那么就失去了抽象的能力。
设计模式精炼了很多现成的松耦合解决方案。但应该防止在应用设计模式时,强迫让代码适用于某个模式,即“为了模式而模式”。
2. 高内聚
内聚性是指类内部的子程序或者子程序内部的所有代码在支持一个中心目标上的紧密程度,即这个类的目标是否是集中的。
功能内聚性
是最好的一种内聚,让一个类或者一个子程序仅执行一项操作。以这种方式评估内聚性,前提是子程序所执行的操作与其名字相符。
例如 sin() 是一个高度内聚的子程序,而 cosAndTan() 则内聚性相对较弱的。
3. 创建高质量的类
3.1 良好的类接口
类的接口应该展现一致的抽象层次
把类看着是一种用来实现抽象数据类型(ADT)的机制。每个类应该实现一个ADT,并且仅实现这个ADT。
一定要理解类所实现的抽象是什么
一些类非常抽象,你必须非常仔细的理解类的接口应该捕捉的抽象到底是哪一个。
为类提供成对的服务
大多数操作都有其对应的、相等的及相反的操作。如打开、关闭,添加、删除等。
把不相关的信息转移到其他类中
有时你会发现,某一个类中一半子程序使用该类的一半数据,另一半子程序使用另一半数据。这时你其实已经把两个类混在一起使用了,应该把他们拆分。
尽可能让接口可编程,而不是表达语义
每个接口都有一个可编程的部分和一个语义部分组成。可编程的部分由接口中的数据类型和其他属性构成,编译器能强制的要求他们(在编译时检查错误)。而语义部分则由“本接口将会被怎么使用”的假定组成,而这些是无法通过编译器来强制实施的。
语义接口例子:functionA 必须在 functionB 之前执行。
尽可能的把语义接口转换为编程接口。
不要添加与接口抽象不一致的子程序
每次向类接口添加子程序时,问问“这个子程序与现在接口所提供的抽象是否一致?”,如果发现不一致,就换另一种方法来进行修改,以便能够保持抽象的完整性。
同时考虑抽象性和内聚性
一个呈现出很好抽象的类接口通常也有很高的内聚性。
尽可能的限制类和成员的可访问性
即能用 private, 就别用 protected, 能用 protected,就别用 public。
不要公开暴露成员数据
暴露成员数据会破坏封装性,从而限制对这个抽象的控制能力。尽量用 get, set 等子程序来封装成员变量。
不要对类的使用者做任何假设
类不应该对接口会被如何使用或者不会被如何使用做出任何假设。如:
——把x,y初始化为0。
留意过紧密的耦合关系
紧耦合总是发生在抽象不严谨或封装性遭到破坏的时候,如果一个类提供了一套不完整的服务,其他的子程序就可能直接去读写该类的内部数据。这样以来就把一个黑盒子变成了一个透明盒子,从而消除了类的封装性。
3.2 包含与继承
包含表达的是:有一个……的关系。
继承表达的是:是一个……的关系。
继承远比包含复杂的多,而且更容易出错,继承并不比包含更好,相反包含才是面相对象编程的主力技术。因此建议多用包含,少用继承。
使用继承时,你必须做出如下几项决策:
- 对于一个成员函数而言,它应该对派生类可见吗?它应该有默认的实现吗?这一默认实现是否能被覆盖?
- 对于每一个数据成员而言(变量,具名常量,枚举等),它应该对派生类可见吗?
判断是否使用继承技术的一个很重要的标准是:判断派生类是否真的“是一个”更特殊的基类,否则就不应该用继承。派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。
不要覆盖一个不可覆盖的成员函数
如果基类的一个成员函数是 private 的话,其派生类可以创建一个同名的成员函数。这会给阅读代码的人带来困惑,所以派生类中的成员函数不要与基类的不可覆盖的成员函数重名。
只有一个对象的类是值得怀疑的
只需要一个实例,这可能表明设计中把对象与类混为一谈了。考虑一下能否只创建新的对象而不是一个新的类。派生类中的差异能否数据而不是新的类来表达(单例模式例外)。
只有一个派生类的基类是值得怀疑的
不要抱着“没准儿哪天就能用得到的”想法去创建基类,让眼下的工作成果尽可能的清晰、简单、直截了当。也就是说不要创建任何并非觉得必要的集成结构。
派生类覆盖的某个子程序,但在其中没有任何操作,这种情况是值得怀疑的
这通常表明基类的设计有错误。
比如有一个 Cat 类,它有一个抓的成员函数,可是最终你发现有些猫没有爪在,不能抓,你可能会派生一个抓的成员函数,然后什么都不做。
这种做法修改了接口所表达的语义,破坏了类的抽象。
修正这一问题的方案是:不是再派生类,而是在 Cat 类中创建一个爪子的类,并让 Cat类包含它。
避免让继承体系过深
建议将继承的层次限制在最多6层内。
尽量使用多态来避免大量的类型检查
如果代码中频繁重复的出现 case 语句,有时是在暗示采用继承可能是更好的设计选择。
让所有的数据(属性)都是 private,而非 protected
继承会破坏封装,当你从一个对象继承时,你就拥有了能够访问该对象 protected 子程序和数据的特权。如果派生类真的需要访问基类的属性,就应该提供 protected 访问器函数。
3.3 成员变量
应该尽量在所有的构造函数中初始化所有的成员数据,这是一种防御式编程,既保证了每个成员变量都被初始化了,也规避了因为在定义变量时初始化变量而导致编译器不兼容的问题。
如果在构造函数中分配了内存,那么就应该在析构函数中释放这些内存。
3.5 类质量核对
- 类是否都被看成是抽象数据类型了?
- 类是否有且只有一个中心目的?
- 类的命名是否恰当?其名字是否表达了其中心目的?
- 类的接口是否展现了一致的抽象?
- 类的接口是否能让人清楚明白的知道如何使用它?
- 类的接口是否足够抽象,使你能把类看成是一个黑盒子,而不用关系它如何实现的?
- 类提供的服务是否足够完整,能让使用者无须动用其内部数据?
- 在修改类时是否维持了其接口的完整性?
- 是否把类的成员的可访问性降到最小?
- 是否避免暴露类中的数据成员?
- 类是否避免对其使用者假设该如何使用它?
- 类是否不依赖于其他类?它是否是松散耦合?
- 继承是否只用来建立“是一个”的关系?即派生类是否是一个特殊的基类?
- 类派生是否避免了覆盖不可覆盖的方法?
- 继承层次是否很浅?有没有超过6层?
- 基类中的所有成员是否都被定义成 private 而非 protected 了?
- 是否是在构造函数中初始化所有数据成员?
4. 创建高质量的子程序
子程序是为了实现特定目的而编写的一个可被调用的函数或者过程。
4.1 好的子程序名
好的子程序名字能清晰的描述子程序所做的一切,达到自我注释的效果。
描述子程序所做的所有事情
子程序名字应当描述其所有的输出结果以及所做的事情。
不要通过仅通过数字来实现不同的子程序名字
如 update1(),update2(),…… update100()。这些名字后面的数字无法显示出子程序所代表的抽象有何不同,因此是非常糟糕的命名。
给函数命名时要对应返回值所有的描述
函数有返回值,函数的命名要针对返回值进行。如:cos(),isReady()等。它们都精确的表述了函数将要返回的结果。
给过程命名时,使用动词加宾语的形式
一个功能内聚的过程是针对一个对象执行一种操作。过程的名字应该能反映该过程所做的事情,使用一个针对摸个对象执行的操作就需要一个动词加宾语的形式。
如:printDocument()、checkPartnerInfo()等。
在对象中给过程命名可以省去宾语,因为对象本身已经包含在调用语句中了。
如:在Partner类中,可写成 partner.check()。
准确使用对仗词
命名时遵守对仗词的命名规则有助于保持一致性,从而提高可读性。
如:add/remove、begin/end、next/previous、up/down等。
4.2 子程序代码长度
子程序的最佳最大长度通常是一屏代码,或者打印出来(A4的纸)一到两页的代码,也就是50-150行代码。
IBM把子程序的长度限制在50行内;
微软将子程序的长度限制在120行以内;
Inter将子程序的长度限制在两页纸之内。
如果一段代码的行数超过了200行,这段代码的可读性就已经相当低了,你需要格外小心。
4.3 正确使用子程序参数
子程序之间的接口是最容易出错的部分之一,一项研究发现,程序中39%的错误都是属于子程序内部接口错误,也就是子程序间互相通讯时所发生的。
按照输入-修改-输出的顺序排列参数
不要随机的或者按照字母顺序排列参数,而应当先排列输入用途的参数,然后排列既作为输入又作为输出用途的参数,最后才是排列仅作为输出用途的参数。这种排列顺序暗示了内部的操作所发生的顺序,即先是输入数据,然后修改数据,最后输出结果。
如果几个子程序都是用了类似的一些参数,应该让这些参数的排列顺序保持一致
子程序的参数顺序可以产生记忆效应,不一样的顺序会让参数难以记忆。
如C语言中的 fprintf()函数比printf()函数就多了一个放在开头的文件参数而已,其他都完全一样。
使用所有参数
既然往子程序中传递了一个参数,就一定要使用这个参数,如果不用它,就把它从接口中删除。
把状态或出错变量放到参数表最后
按习惯做法,状态变量和那些用于指示发生错误的变量应该放到参数表的最后。
不要把子程序的参数用做工作变量
把传入子程序的参数用做工作变量是很危险的。应该使用局部变量代替参数。
如代码:
int sample(int inputVal) { inputVal = inputVal * Math.sin(inputVal); return inputVal; } |
这段代码中,inputVal这个名字很容易引起误解,因为当执行到最后一行时,inputVal 包含的已经不是初始的输入值了,它的值是用输入值计算出来的结果。如果日后你又要修改这段程序,要在其它地方使用原来的输入值,你可能会想当然的认为 inputVal 还是原始输入值得参数并使用它时,问题就出来了。
这段代码也给调试带来了不便,如果需要比对输入值和结果值,就很难办到了。
解决方案如下:
int sample(int inputVal) { Int workingVal = inputVal; workingVal = workingVal * Math.sin(inputVal); return workingVal; } |
对不定参数用注释加以说明
在设计带有不定参数的子程序时,一定要在设计的时候,就对不定参数做出注释。
子程序参数限制在7个以内
对于人的理解能力来说,7是一个神奇的数字,人们通常很难同时记住超过7个单位的信息。如果你发现自己一直需要传递很多参数,这就说明子程序之间的耦合太过紧密了。
4.4 子程序应该保证单入单出
子程序里面应该保证只有一个出口,即整个子程序中只有一个return。一些程序员为了书写的方便,会在程序中多次用到 renturn,使程序具有多个出口,这样设计会使以后的维护变得非常的不便。
如:
private boolean isEligible(int age) { int eligibleAge = 18; if(age > eligibleAge) { return true; } else { return false; } } |
改进方案为:
private boolean isEligible(int age) { boolean result = false; int eligibleAge = 18; if(age > eligibleAge) { result = true; } else { result= false; } return result; } |
特例:在子程序的前面,可以允许一些对输入参数进行检验的出口存在,以降低嵌套层次。如:
private boolean isEligible(int age) { if (age <= 0) { return false; } boolean result = false; int eligibleAge = 18; if(age > eligibleAge) { result = true; } else { result= false; } return result; } |
这种类似的参数检验必须放到子程序的最前面。
5. 变量
5.1 变量的命名
为变量命名时,最重要的注意事项是,该名字要完全、精确地描述出该变量所代表的事物。好的名字能表达变量所代表的是什么。
以问题为导向命名,一个好记得名字反映的通常都是问题,而不是解决方案。一个好的名字通常表达的是“what”,而不是“how”。应该让变量的名字反映问题本身。
如:一条员工数据可以称作 inputRecord 或者 employeeData。inputRecord 是一个反映输入、记录这些计算概念的计算机术语。employeeData 则直指问题领域,与计算机世界无关。
最佳的变量名长度,变量名最佳长度为9到15或者10到16个字符长。太短的变量名无法传递足够的信息;太长的名字很难书写,同时使得程序的视觉结构变得模糊不清。
变量名中的限定词,很多程序都有表示计算结果的变量,如:总额、平均值、最大值等,如果你用类似于 total、sum、average、max、string等这样的限定词去修改某个名字,那么请把限定词加到名字的最后。这样修改的优点是变量最重要的部分,即这一变量赋予主要含义的部分可以被突出,并会被首先阅读。
如使用变量 totalRemove 应改为 removeTotal。
多使用对仗词,通过应用对仗词可以提高可读性,如begin/end等。
嵌套循环中,不要使用 i、j、k之类的名字命名循环下标。
例程:
for (var i:int = 0; i < rowCount; i++) { for (var j:int = 0; j < colCount; j++) { … } } |
以上代码中i和j很容易串话,从而给程序带来灾难,改进方案如下:
for (var rowIndex:int = 0; rowIndex < rowCount; rowIndex++) { for (var colIndex:int = 0; colIndex < colCount; colIndex++) { … } } |
不要使用不被广为人知的缩写,很多程序员为了书写方便,常常使用缩写。需要特别注意的是,不要使用随意的缩写,如 character 缩写为 char,mouseEvent 缩写为 me。类似这样的简写会误导读者,看到char ,读者可能会想到是一个字符。请使用那些广为人知的缩写,如 button 缩写为 btn,identity缩写为 id。
布尔变量的命名,使用 done、error、found、success、ok等后缀对布尔类型的变量命名是一个非常不错的选择,如:saveFileSuccess、sourceFileFound等。
避免在变量中出现数字,诸如file1、file2,total1、total2这样的变量名不要出现在程序中。
5.2 变量的初始化
在可能的情况下使用 final 或者 const 关键字
你可以防止该变量在初始化后再被赋值。
在构造函数中初始化成员变量
这一点前面已经提到过。
局部变量——在靠近第一次使用变量时声明和初始化
良好的初始化局部变量例子:
Int accountIndex = 0; //code using accountIndex … |
accountIndex 在靠近首次使用的地方被声明和初始化。
一次性初始化所有具名常量
具名常量应该在程序的最前面被一次性初始化。
不要依赖编译器来初始化变量
很多程序员在定义成员变量时,经常不对其赋初始值,仅仅依赖编译器去赋值,这是一种非常危险的做法。
如AS代码:
private var partnerName:String; private var partnerLevel:int; |
这两句代码在正常的情况下,编译器会给它赋初始值,partnerName会被赋值为null,而partnerLevel 会被赋值为 0。但不要去依赖编译器。
改进方案是:
private var partnerName:String = null; private var partnerLevel:int = 0; |
5.3 变量的作用域
作用域可以看作是一种衡量变量知名度的方法。
5.3.1 使变量的引用局部化
局部化变量是指:尽可能的把变量引用点集中到一起。
尽可能的减小变量的跨度
跨度是用来衡量一个变量的不同引用点的靠近程度。
如AS代码:
var a:int = 0; var b:int = 0; var c:int = 0; a = b + c; |
上例中,对a的第一次调用和第二次调用之间存在两行代码,因此变量a的跨度是2。在两次对b的调用之间存在一行代码,因此变量b的跨度是1,同理c的跨度是0。
再如java代码:
int a = 0; int b = 0; int c = 0; b = a + 1; b = b / c; |
上例中第一次调用b和第二次调用b的跨度是1;第二次调用b和第三次调用b的跨度是0。
平均跨度可以通过对各个跨度计算平均值而获得。在上例中,变量b的平均跨度是:
(1 + 0) / 2 = 0.5。
平均跨度越低的变量,其引用点之间的距离非常近,使得代码的阅读者能每次只关注一部分代码,从而提高了程序的可读性。
尽可能的缩短变量的存活时间
存活时间使的是一个变量存在期间所跨越的语句总数。变量的存活时间开始于引用它的第一条语句,结束于引用它的最后一条语句。与跨度不同的是,存活时间不受第一次和最后一次引用变量之间变量使用次数的影响。
下图同时说明的变量的跨度和存活时间。
图5-1 变量的跨度和存活时间
让变量保持较低的存活时间,可以使得当修改该变量时,被错误或无意修改的可能性降低,同时也增加了代码的可读性。
如果用跨度和存活时间来衡量全局变量,会发现全局变量的跨度和存活时间非常长,这也是为什么要避免使用全局变量的原因之一。
5.3.2 减小作用域的一般原则
- 在循环开始前再去明和初始化循环里使用的变量,而不是在该循环所属的子程序的开始处声明和初始化这些变量;
- 直到变量即将用到时再去声明和初始化;
- 把相关语句放到一起;
如C++代码:
void summarizeData() { getOldData(oldData, numOldData); getNewData(newData, numNewData); totalOldData = sum(oldData, numOldData); totalNewData = sum(newData, numNewData); printOldDataSummary(oldData, totalOldData, numOldData); printNewDataSummary(newData, totalNewData, numNewData); saveOldDataSummary(oldData, totalOldData); saveNewDataSummary(newData, totalNewData); } |
这个例子中,你需要跟踪 oldData、newData、numOldData、numNewData、totalOldData、及totalNewData6个变量,下面例子做了很好的改进:
void summarizeData() { getOldData(oldData, numOldData); totalOldData = sum(oldData, numOldData); printOldDataSummary(oldData, totalOldData, numOldData); saveOldDataSummary(oldData, totalOldData); //以上是使用oldData的一组语句
getNewData(newData, numNewData); totalNewData = sum(newData, numNewData); printNewDataSummary(newData, totalNewData, numNewData); saveNewDataSummary(newData, totalNewData); //以上是使用newData的一组语句 } |
把这段代码拆分后,得到的两段代码都比原来的代码简单,各自含有更少的变量,从而很容易理解。
- 先采用更严格的可见性,然后根据需要扩展变量的作用域;减少变量作用域的最常用的方法就是尽量使变量局部化。
你应该倾向于选择该变量所能具有的最小作用域:首选将变量限于某个特定的循环内,或者某个if语句内,然后是局限于某个子程序中,其次为类的 private变量,protected变量,再其次是对包(package)可见,再其次是public变量,最后在不得已的情况下再把它作为全局变量。
5.4 为变量指定单一用途
每一个变量只用于单一用途,谨防临时变量(如temp、x等);
下例是将单一变量两用的非常糟糕的实践:
var temp:int = Math.sqrt(b * b – 4 * a * c); root[0] = (temp - b) / (2 * a); root[1] = (-temp - b) / (2 * a); … temp = root[0]; root[0] = root[1]; root[1] = temp; |
这段代码中两个 temp 之间没有任何关系,在这两个位置使用同一变量,会使得本无联系的两者看上去似乎彼此相关,降低了代码的可读性。
改进方案为:
var discriminant:int = Math.sqrt(b * b – 4 * a * c); root[0] = (temp - b) / (2 * a); root[1] = (-temp - b) / (2 * a); … var oldRoot:int = root[0]; root[0] = root[1]; root[1] = oldRoot; |
避免让代码具有隐含含义 在编程中,为了方便往往会给变量一些隐含的含义 ;
如:变量 pageCount表示已打印纸张的数量,除非它的值等于-1,则表示有错误发生;
再如:变量customerID代表客户的账号,除非它的取值大于50000,这时则表示过期的账户。
以上两个例子都让变量具有了两个用途,降低了代码的可读性。应该避免使用具有隐含含义的变量,这种滥用被称之为“混合耦合”。
确保使用了所有已声明的变量,声明了变量却不适用,这个程序带来了很大的维护和阅读成本。养成检查代码以确认使用了所有声明过的变量的习惯。一些编译器会对未用到的变量报警(flashBuilder不会)。
6. 组织直线型代码
6.1 使代码易于自上而下地阅读
让代码易于自上而下阅读,而不是让读者的目光跳来跳去,对提高可读性最有帮助。如果别人阅读你的代码不得不通过搜索来辅助阅读,那么就应该从新组织你的代码。
6.2 把相关的语句组织在一起
语句之所以相关,是因为它们处理了相同的数据、执行了相同的任务,或者具有某种执行顺序。
一种简单的检查方式是打印出你的代码,然后把相关的语句画上框。如果排列的很好,你就会得到类似下图的图形,其中的方框是不会彼此交叠的。
图6-1 组织良好的代码
如果排列得不好,就可能得到下图:
图6-2 组织得不好的代码
7. 条件语句的使用
7.1 if语句的使用技巧
利用布尔函数调用简化复杂的检测
例程:
if ( inputChar == “ ” || inputChar == “, ” || inputChar == “! ” || inputChar == “ (” || inputChar == “ )” || inputChar == “ :” || inputChar == “ ?” || inputChar == “ _” || inputChar == “ -” || ) { charType = CharType_punctuation; } |
这段代码很难读,因为它对字符分类的判断很复杂。为了提高可读性,可以把它替换成布尔函数调用。
以上代码改进方案:
if (isControl(inputChar)) { charType = CharType_punctuation; } |
将一大段符号判断代码封装成isControl函数。
把最常见的情况放到最前面,最不常见的情况放到最后面,这样可以让阅读代码的人找出正常情况的处理代码而不太需要去阅读非正常情况的代码。同时由于把常见情况放到前面而限制性,非常见的代码的执行频率就减少了,代码的效率也就提高了。
if里面的不要是空白语句,要是一个有意义的语句
代码中尽量避免出现以下代码
if (partnerInfo == null) { //do nothing } |
用 “{”和“}”把if语句括起来,为了方便,程序员常常会在写简单if语句时,省去“{”和“}”,这种做法是非常不可取的,因为它降低了可读应和扩展性。
例程:
if (file != null) file.close(); |
应改为
if (file != null) { file.close(); } |
for、wihle循环语句同样需要用“{”和“}”括起来。
7.2 case语句的使用技巧
7.2.1 为case 选择最有效的排列顺序
按字母或按数字顺序排列,如果所有情况的重要性都相同,那么就按A-B-C的顺序加以排列,以便提高可读性;
把正常的情况放到最前面,如果有一个正常情况和多个异常情况,那么就把正常情况放在最前面;
按照执行频率排列,把最经常执行的情况放到最前面。
7.2.2 使用case语句的诀窍
简化case语句每种情况的对应操作,简短的情况处理代码会使case语句的结构更加清晰。如果操作比较复杂,那么就写一个子程序,并在该情况对应的case中调用它。
把default子句只用于检查真正的默认情况,有时候你只剩下一种情况需要处理,于是你就把它写到default子句中,这种做法是非常不明智的,这让default子句丧失了检测错误的能力,也同时失去了case语句的标号所提供的自我注释功能。
利用default子句来检测错误,建议每一个switch-case语句都有一个default子句来检测错误(或输出日志,或抛出异常)。
8. 编程习惯
8.1 避免使用神仙数
神仙数指的是代码中出现的,没有经过解释的数值文字量,如100、47等。
例如:
for (var index:int = 0; index < 10; index++) { If (partnerInfo.quality == 6) { … } } |
以上代码中的 10,6就是神仙数。
避免使用神仙数会带来的好处:
- 修改变得更可靠
- 修改变得更容易
- 代码变得更可读
上段代码应该改为:
const partnerCount:int = 10; const partnerGreenQuality:int = 6; for (var index:int = 0; index < partnerCount; index++) { If (partnerInfo.quality == partnerGreenQuality) { … } } |
改进后的代码不需要去猜它的含义,维护起来也更加的容易。
8.2 精简每一个行代码,尽量是每一行代码足够简单
例程:
if (partnerList[index] != null && partnerList[index].getPartnerUrl() != null) { loader.load(new URLRequest(partnerList[index].getPartnerUrl())); } |
这段代码相当的糟糕,非常难以阅读,也非常难以调试(用debug工具单步调试)。
改进方案为:
var partnerInfo:PartnerInfo = partnerList[index]; if (partnerInfo != null) { var partnerUrl:String = partnerInfo. getPartnerUrl(); if (partnerUrl != null) { var urlRequest: URLRequest = new URLRequest(partnerUrl); loader.load(urlRequest); } } |
改进后的代码引入了 partnerInfo、partnerUrl、urlRequest三个变量,这三个变量不会给性能带来任何额外的开销(代码原来就会隐性的创建三个变量出来)。改进后的代码变得非常的简洁,便于阅读、调试。
小结
让阅读代码比编写代码更加方便,阅读代码的次数远比编写代码的次数多得多,即使在开发的初期,因此为了让编写代码更方便而降低代码的可读性是非常不经济的。
编写代码时,应该更依赖于代码编写的技术,而不是依赖于注释。在维护控制得非常严格的代码或由于一些其他的原因使你无法改进代码本身的情况下,使用注释来弥补代码的不足是最合适的。