代码整洁之道-马丁-第3章 函数
总结
-
函数应该短小,包括代码块和缩进。
-
确保每个switch都埋藏在较低的抽象层级,而且永远不重复。
-
分隔指令和询问:函数要么做什么事,要么回答什么问题
-
3.9 使用异常替代返回错误代码
3.1 短小
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近40年来,我写过各种不同大小的函数。我写过令人憎恶的长达3000行的厌物,也写过许多100行到300行的函数,我还写过20行到30行的。经过漫长的试错,经验告诉我,函数就该小。
代码块和缩进
if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。
3.2 只做一件事
代码清单3-1显然想做好几件事。它创建缓冲区、获取页面、搜索继承下来的页面、渲染路径、添加神秘的字符串、生成HTML,如此等等。代码清单3-1手忙脚乱。而代码清单3-3则只做一件简单的事。它将设置和拆解包纳到测试页面中。过去30年以来,以下建议以不同形式一再出现:
函数应该做一件事。做好这件事。只做这一件事。
问题在于很难知道那件该做的事是什么。代码清单3-3只做了一件事,对吧?其实也很容易看作是三件事:
(1)判断是否为测试页面;
(2)如果是,则容纳进设置和分拆步骤;
(3)渲染成HTML。
那件事是什么?函数是做了一件事呢,还是做了三件事?注意,这三个步骤均在该函数名下的同一抽象层上。可以用简洁的TO [4] 起头段落来描述这个函数:
TO RenderPageWithSetupsAndTeardowns, we check to see whether the page is a test page and if so, we include the setups and teardowns. In either case we render the page in HTML。
(要RenderPageWithSetupsAndTeardowns,检查页面是否为测试页,如果是测试页,就容纳进设置和分拆步骤。无论是否测试页,都渲染成HTML)
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
代码清单3-1明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单3-2也有两个抽象层,这已被我们将其缩短的能力所证明。然而,很难再将代码清单3-3做有意义的缩短。可以将if语句拆出来做一个名为includeSetupAndTeardonws IfTestpage的函数,但那只是重新诠释代码,并未改变抽象层级。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现[G34]。
函数中的区段
请看代码清单4-7。注意,generatePrimes函数被切分为declarations、initializations和sieve等区段。这就是函数做事太多的明显征兆。只做一件事的函数无法被合理地切分为多个区段。
3.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。一眼就能看出,代码清单3-1 违反了这条规矩。那里面有 getHtml( )等位于较高抽象层的概念,也有 String pagePathName = PathParser.render(pagePath)等位于中间抽象层的概念,还有.append("\n")等位于相当低的抽象层的概念。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
自顶向下读代码:向下规则
我们想要让代码拥有自顶向下的阅读顺序。 [5] 我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。
换一种说法。我们想要这样读程序:程序就像是一系列 TO起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续TO起头段落。
To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.(要容纳设置和分拆步骤,就先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步骤。)
To include the setups, we include the suite setup if this is a suite, then we include the regular setup.(要容纳设置步骤,如果是套件,就纳入套件设置步骤,然后再纳入普通设置步骤。)
To include the suite setup, we search the parent hierarchy for the“SuiteSetUp”page and add an include statement with the path of that page.(要容纳套件设置步骤,先搜索“SuiteSetUp”页面的上级继承关系,再添加一个包括该页面路径的语句。)
To search the parent. . . (要搜索……)
程序员往往很难学会遵循这条规则,写出只停留于一个抽象层级上的函数。尽管如此,学习这个技巧还是很重要。这是保持函数短小、确保只做一件事的要诀。让代码读起来像是一系列自顶向下的TO起头段落是保持抽象层级协调一致的有效技巧。
3.4 switch语句
写出短小的switch语句很难 [6] 。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。
请看代码清单3-4。它呈现了可能依赖于雇员类型的仅仅一种操作。
代码清单3-4 Payroll.java
public 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 [7] , SRP),因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle [8] , OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有
isPayday(Employee e, Date date),
或
deliverPay(Employee e, Money pay),
如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将switch语句埋到抽象工厂 [9] 底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍[G23]。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单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 使用描述性的名称
在代码清单3-7中,我把示例函数的名称从testableHtml改为SetupTeardownIncluder.render。这个名称好得多,因为它较好地描述了函数做的事。我也给每个私有方法取个同样具有描述性的名称,如isTestable或includeSetupAndTeardownPages。好名称的价值怎么好评都不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”要遵循这一原则,泰半(大半的意思)工作都在于为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在Eclipse或IntelliJ等现代IDE中改名称易如反掌。使用这些IDE测试不同名称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages和includeTeardownPage又会如何?”这就是所谓“深合己意”了。
3.6 函数参数
最理想的参数数量是零(零参数函数),最好不要超过三个参数。
有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。
3.6.2 标识参数
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为true将会这样做,标识为false则会那样做!
在代码清单3-7中,我们别无选择,因为调用者已经传入了那个标识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用render(true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到render(Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数一分为二:reanderForSuite( )和renderForSingleTest( )。
不要在参数定义有标识意义的参数,这样使函数不止做一件事。
3.6.3 二元函数
有两个参数的函数要比一元函数难懂。例如,writeField(name)比writeField(outputStream,name) [10] 好懂。
尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代码。忽略掉的部分就是缺陷藏身之地。
当然,有些时候两个参数正好。例如,Point p = new Point(0,0);就相当合理。笛卡儿点天生拥有两个参数。如果看到new Point(0),我们会倍感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而output-Stream和name则既非自然的组合,也不是自然的排序。
即便是如 assertEquals(expected, actual)这样的二元函数也有其问题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。
所以 assertEqual改成assertExpectedEqualsActual(expected, actual)可能会好些。
3.6.5 参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。例如,下面两个声明的差别:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
x和y应该是 center 中心点概念的一部分。
3.6.6 参数列表
有时,我们想要向函数传入数量可变的参数。例如,String.format方法:
String.format("%s worked %.2f hours.", name, hours);
如果可变参数像上例中那样被同等对待,就和类型为List的单个参数没什么两样。这样一来,String.formate实则是二元函数。下列String.format的声明也很明显是二元的:
public String format(String format, Object... args)
3.6.7 动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我们,“name”是一个“field”。
3.7 无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
以代码清单3-6中看似无伤大雅的函数为例。该函数使用标准算法来匹配 userName和password。如果匹配成功,返回 true,如果失败则返回 false。但它会有副作用。你知道问题所在吗?
代码清单3-6 UserValidator.java
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
当然了,副作用就在于对Session.initialize( )的调用。checkPassword函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
这一副作用造出了一次时序性耦合。也就是说,checkPassword只能在特定时刻调用(换言之,在初始化会话是安全的时候调用)。如果在不合适的时候调用,会话数据就有可能沉默地丢失。时序性耦合令人迷惑,特别是当它躲在副作用后面时。如果一定要时序性耦合,就应该在函数名称中说明。在本例中,可以重命名函数为checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的规则。
输出参数
参数多数会被自然而然地看作是函数的输入。如果你编过好些年程序,我担保你一定被用作输出而非输入的参数迷惑过。例如:
appendFooter(s);
这个函数是把s添加到什么东西后面吗?或者它把什么东西添加到了s后面?s是输入参数还是输出参数?稍许花点时间看看函数签名:
public void appendFooter(StringBuffer report)
事情清楚了,但付出了检查函数声明的代价。你被迫检查函数签名,就得花上一点时间。应该避免这种中断思路的事。
在面向对象编程之前的岁月里,有时的确需要输出参数。然而,面向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出函数的意味在内。换言之,最好是这样调用appendFooter:
report.appendFooter();
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。
3.8 分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回false。这样就导致了以下语句:
if (set("username", "unclebob"))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果username属性值之前已被设置为uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然后……”。要解决这个问题,可以将 set 函数重命名为setAndCheckIfExists,但这对提高 if 语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
3.9 使用异常替代返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中把指令当作表达式使用。
if (deletePage(page) == E_OK)
这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
logger.log("page deleted");
} else {
logger.log("configKey not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
3.9.1 抽离Try/Catch代码块
Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
在上例中,delete函数只与错误处理有关。很容易理解然后就忽略掉。deletePageAndAllReference函数只与完全删除一个page有关。错误处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。
3.9.2 错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
3.9.3 Error.java依赖磁铁
返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
这样的类就是一块依赖磁铁(dependency magnet);其他许多类都得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编译和部署。 [11] 这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧的错误码,而不添加新的。
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署 [12] 。
3.10 别重复自己
[13] 回头仔细看看代码清单3-1,你会注意到,有个算法在SetUp、SuiteSetUp、TearDown和SuiteTearDown中总共被重复了 4 次。识别重复不太容易,因为这 4 次重复与其他代码混在一起,而且也不完全一样。这样的重复还是会导致问题,因为代码因此而臃肿,且当算法改变时需要修改4处地方。而且也会增加4次放过错误的可能性。
使用代码清单3-7中的include方法修正了这些重复。再读一遍那段代码,你会注意到,整个模块的可读性因为重复的消除而得到了提升。
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。例如,全部考德(Codd)[14] 数据库范式都是为消灭数据重复而服务。再想想看,面向对象编程是如何将代码集中到基类,从而避免了冗余。面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming)多少也都是消除重复的一种策略。看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复。
3.11 结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则 [15] 。Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。
所以,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto只在大函数中才有道理,所以应该尽量避免使用。
3.12 如何写出这样的函数
写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。
3.13 小结
每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言设计的艺术。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们定义的与领域紧密相关的语言讲述自己那个小故事。
本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
3.14 SetupTeardownIncluder程序
代码清单3-7 SetupTeardownIncluder.java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;
public class SetupTeardownIncluder {
private PageData pageData;
private Boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, Boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
private String render(Boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private Boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception {
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception {
include("SetUp", "-setup");
}
private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("n");
}
}
3.15 文献
[KP78]:Kernighan and Plaugher, The Elements of Programming Style, 2d. ed., McGraw- Hill, 1978.
[PPP02]:Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices, Prentice Hall, 2002.
[GOF]:Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al., Addison-Wesley, 1996.
[PRAG]:The Pragmatic Programmer,Andrew Hunt,Dave Thomas,Addison-Wesley,2000.
[SP72]:Structured Programming, O.-J. Dahl, E. W. Dijkstra, C. A. R. Hoare, Academic Press, London, 1972.
[1]. 原注:一种开源测试工具。见http://www.fitnese.org。
[2]. 原注:一种开源Java单元测试工具。见http://www.junit.org。
[3]. 原注:我问肯特是否还保留这段程序,他说找不到了。我搜遍自己的电脑也没找到。现在只有在记忆中有这段程序了。
[4]. 原注:LOGO语言中的TO关键字,与Ruby和Python中def关键字的用法一致。所以,每个函数都以TO起头。这对函数的设计产生了有趣的影响。
[5]. 原注:[KP78]。
[6]. 原注:当然,这也包括if/else语句在内。
[7]. 原注:a. http://en.wikipedia.org/wiki/Single_responsibility_principle;b. http://www.objectmentor.com/resources/articles/srp.pdf。
[8]. 原注:a. http://en.wikipedia.org/wiki/Open/closed_principle;b. http://www.objectmentor.com/resources/articles/ocp.pdf。
[9]. 原注:[GOF]。
[10]. 原注:我刚重构了一个使用了二元形式的模块。现在就能把outputStream做成该类的一个字段,并把所有对writeField的调用都变作一元形式。结果就干净多了。
[11]. 原注:那些以为可以不重新编译和部署就扬长而去的家伙最终都自尝恶果。
[12]. 原注:这也是开放闭合原则(OCP)的一个范例 [PPP02]。
[13]. 原注:DRY原则。[PRAG]。
[14]. 译注:艾德加·F·考德(Edgar F. Codd),关系数据库之父。
[15]. 原注:[SP72]。
posted on 2020-03-05 15:50 aworkstory 阅读(390) 评论(0) 编辑 收藏 举报