哪些网站是php做的武汉大学人民医院光谷院区
目录
3.1 短小
3.2 只做一件事
3.3 每个函数一个抽象层级
3.4switch语句
3.5 使用描述性的名称
3.6 函数参数
3.6.1 一元函数的普遍形式
3.6.2标识参数
3.6.3 二元函数
3.6.4 三元函数
3.6.5参数对象
3.6.6参数列表
3.6.7动词与关键字
3.8分隔指令与询问
3.9使用异常替代返回错误码
3.9.1抽离TryLatch代码块
3.9.2错误处理就是一件事
3.11结构化编程
3.1 短小
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近40年来,我写过各种不同大小的函数。
我写过令人憎恶的长达3000行的厌物,也写过许多100行到300行的函数,我还写过20行到30行的。经过漫长的试错,经验告诉我,函数就该小。
在20世纪80年代,我们常说函数不该长于一屏。当然,说这话的时候,VT100屏幕只有24行、80列,而编辑器就得先占去4行空间放菜单。如今,用上了精致的字体和宽大的显示器,一屏里面可以显示100行,每行能容纳150个字符。每行都不应该有150个字符那么长。函数也不该有100行那么长,20行封顶最佳。
3.2 只做一件事
函数应该做一件事。做好这件事。只做这一件事。
3.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
自顶向下读代码:
向下规则我们想要让代码拥有自顶向下的阅读顺序。'我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。
3.4switch语句
写出短小的switch语句很难'。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。
请看代码清单34。它呈现了可能依赖于雇员类型的仅仅一种操作。
代码清单3-4 Payroll,javapublic Money calculatePay (Employee e)
throws InvalidEmployeeType {switch (e.type){case COMMISSIONED:return calculateCommissionedPay(e);case HOURLY:return calculateHourlyPay(e);case SALARIED:return calculateSalariedPay(e);default:throw new InvalidEmployeeType(e.type)}
}
该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则(Single Responsibility Principle,SRP),因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle,OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有
isPayday (Employee e, Date date),
或
deliverPay (Employee e, Money pay),
如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将swit©h语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单3-5.Employee与工厂public abstract class Employee {public abstract boolean isPayday();public abstract Money calculatePay();public abstract void deliverPay(Money pay);
}public interface EmployeeFactory {public Employee makeEmployee(EmployeeRecord r)throws InvalidEmployeeType;
}public class EmployeeFactoryImpl implements EmployeeFactory {public Employee makeEmployee (EmployeeRecord r)throws InvalidEmployeeType {switch (r.type){case COMMISSIONED:return new CommissionedEmployee(r);case HOURLY:return new HourlyEmployee(r);case SALARIED:return new SalariedEmploye(r);default:throw new InvalidEmployeeType(r.type);}}
}
3.5 使用描述性的名称
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在Eclipse或Intelli山等现代DE中改名称易如反掌。使用这些DE测试不同名称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages,includeSetupPages,includeSuiteSetupPage includeSetupPage等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages和includeTeardownPage又会如何?”这就是所谓“深合己意”了。
3.6 函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)一所以无论如何也不要这么做。
参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不加参数。比如,以StringBuffer为例,我们可能不把它作为实体变量,而是当作参数来传递,那样的话,读者每次看到它都得要翻译一遍。阅读模块所讲述的故事时,includeSetupPage()includeSetupPageinto(newPage-Content)易于理解。参数与函数名处在不同的抽象层级,它要求你了解目前并不特别重要的细节(即那个StringBuffer)。
3.6.1 一元函数的普遍形式
向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参数的问题,就像在boolean fileExists("MyFile'")中那样。也可能是操作该参数,将其转换为其他什么东西,再输出之。例如,InputStream fileOpen("MyFile")把String类型的文件名转换为InputStream类型的返回值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种理由的名称,而且总在一致的上下文中使用这两种形式。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件(event)。在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如void passwordAttemptFailedNtimes(int attempts)。小心使用这种形式。应该让读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。
尽量避免编写不遵循这些形式的一元函数,例如,void includeSetupPagelnto(StringBufferpageText)。对于转换,使用输出参数而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。实际上,StringBuffer transform(StringBuffer in)要比voidtransform(StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循了转换的形式。
3.6.2标识参数
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为true将会这样做,标识为false则会那样做!
在代码清单3-7中,我们别无选择,因为调用者已经传入了那个标识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用render((true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到render((Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数一分为二:reanderForSuite()和renderForSingleTest()。
3.6.3 二元函数
有两个参数的函数要比一元函数难懂。例如,writeField(name)比writeField(outputStream,name)'好懂。
尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代码。忽略掉的部分就是缺陷藏身之地。
当然,有些时候两个参数正好。例如,Pointp=new Point(0,O);就相当合理。笛卡儿点天生拥有两个参数。如果看到new Point(O),我们会倍感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而output--Stream和name则既非自然的组合,也不是自然的排序。
即便是如assertEquals(expected,.actual)这样的二元函数也有其问题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。
二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一元函数。例如,可以把writeField方法写成outputStream的成员之一;从而能这样用:outputStream.writeField(name)。或者,也可以把outputStream写成当前类的成员变量,从而无需再传递它。还可以分离出类似FieldWriter的新类,在其构造器中采用outputStream,并且包含一个write方法。
3.6.4 三元函数
有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议你在写三元函数前一定要想清楚。
例如,设想assertEquals有三个参数:assertEquals(message,expected,.actual)。有多少次,你读到message,.错以为它是expected呢?我就常栽在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后学会了忽略message参数。
另一方面,这里有个并不那么险恶的三元函数:assertEquals(l.0,amount,,.001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相对而言”的提示总是好的。
3.6.5参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
例如,下面两个声明的差别:
circle makecircle(double x,double y,double radius);
Circle makecircle(Point center,double radius);
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
3.6.6参数列表
有时,我们想要向函数传入数量可变的参数。例如,String.format方法:
String.format ("%s worked &.2f hours.",name,hours);
如果可变参数像上例中那样被同等对待,就和类型为Ls的单个参数没什么两样。这样
一来,String.formate实则是二元函数。下列String.format的声明也很明显是二元的:
public String format(String format,Object...args)
同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数量就可能要犯错了。
void monad(Integer...args);
void dyad(String name,Integer...args);
void triad(String name,int count,Integer...args);
3.6.7动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。
不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我们,“name”是一个“field”。
最后那个例子展示了函数名称的关键字(keyword).形式。使用这种形式,我们把参数的名称编码成了函数名。例如,assertEqual改成assertExpectedEqualsActual(expected,.actual)可能会好些。这大大减轻了记忆参数顺序的负担。
3.8分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:
public boolean set(String attribute,String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回fals。这样就导致了以下语句:
if (set ("username","unclebob"))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为st是动词还是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果username属性值之前已被设置为uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然后…”。要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists("username")) {
(setAttribute("username","unclebob");
}
3.9使用异常替代返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在f语句判断中把指令当作表达式使用。
if (deletePage(page)==E_OK)
这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在
要求调用者立刻处理错误。
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:
3.9.1抽离TryLatch代码块
Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一淡。最好把ry和catch代码块的主体部分抽离出来,另外形成函数。
3.9.2错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字y在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
3.11结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则。Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。
所以,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,gto只在大函数中才有道理,所以应该尽量避免使用。