0.写在最前
- 目前笔者还在实习,在平时的开发工作中,不仅需要扎实的技术硬实力,专业软实力也同样不可或缺。
- 在每次的"Code Review"会议中,成熟的代码不仅仅是实现了功能需求,而且代码结构和简洁性非常OK(code review就是体现在这里!)
- 所以,我最近在读《代码简洁之道》这本书,目前还在进行中,但是已经获益匪浅。
1.什么是代码整洁
- 代码应该易于阅读和理解
- 代码应该简洁明了,避免过度工程化
- 代码应该有自己的风格和规范
2.命名
2.1 名副其实
变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么会存在, 它做什么事,应该怎么用。
// 错误示例,名称d什么也没说明。它没有引起对时间消逝的感觉,更别说以日计了。
int d; //消逝的时间,以日计
// 正确示例,我们应该选择 指明了计量对象和计量单位的名称:
int elapsedTimelnDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgelnDays;
// ------------------------------------------------------------------------
// 错误示例
public List<int[]> getThem() {
List<int[]> listl = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
listl.add(x);
return listl;
)
//正确示例
public List<int[]> getFlaggedCells() (
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
2.2 避免误导
- 程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。如:别用accountList来指称一组账号,除非它真的是List类型。List 一词对程序员有特殊意义。如果包纳账号的容器并非真是个List,就会引起错误的判断。所以,用accountGroup或bunchOfAccounts,甚至直接用accounts都会好一些。
2.3 做有意义的区分
- 如果程序员只是为满足编译器或解释器的需要而写代码,就会制造麻烦。例如,因为同一作用范围内两样不同的东西不能重名,你可能会随手改掉其中一个的名称。有时干脆以错误的拼写充数,结果就是出现在更正拼写错误后导致编译器出错的情况。
// 错误示例
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[j] = a1[i];
}
)
//正确示例,如果参数名改为source和destination,这个函数就会像样许多。
public static void copyChars(char source[], char destination[]) {
for (int i = 0; i < source.length; i++) {
destination[j] = source[i];
}
)
- 废话是另一种没意义的区分。假设你有一个Product类。如果还有一个ProductInfo 或ProductData类,那它们的名称虽然不同,意思却无区别。Info和Data就像a、an和the 一样, 是意义含混的废话。
2.4 使用读得出来的名称
- 人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种耻辱。
// 错误示例,自己造词
class DtaRcrdlO2 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102”;
/* ... */
};
// 正确示例
class Customer (
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordld = "102";
/* ... */
};
2.5 使用可捜索的名称
- 单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。例如:找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了。我以为单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。
// 错误示例
for (int j=0; j<34; j++) {
s += (t[j]*4)/5;
}
// 正确示例
int realDaysPerldealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerldealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
- 注意,上面代码中的sum并非特别有用的名称,不过它至少搜得到。釆用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比数字5好找得多, 而列表中也只剩下了体现作者意图的名称。
2.6 避免使用编码
- 编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞懂另一种编码“语言”。这对于解决问题而言,纯属多余的负担
2.8 避免思维映射
- 不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时。
- 单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循环计数器自然有可能被命名为i或j或k。(但千万别用字母" l (大写是L)"!!!)。这是因为传统上惯用单字母名称做循环计数器。 然而,在多数其他情况下,单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。仅仅是因为有了a和b,就要取名为c,实在并非像样的理由。
2.9 类名
- 类名和对象名应该是名词或名词短语,如Customer、WikiPage、Account和AddressParser0 避免使用Manager、Processor、Data或Infb这样的类名。类名不应当是动词!
2.10 方法名
- 方法名应当是动词或动词短语,如postPayment、deletePage或save。属性访问器、修改器和断言应该根据其值命名,并依Javabean标准'加上get、set和is'前缀。
// 示例
String name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted() ){}
// 重载构造器时,使用描述了参数的静态工厂方法名,可以考虑将相应的构造器设置为private,强制使用这种命名手段。
Complex fulcrumPoint = Complex.FromRealNumber (23.0);
// 好于
Complex fulcrumPoint = new Complex(23.0);
2.11 每个概念对应一个词
- 给每个抽象概念选一个词,并且一以贯之。例如,使用fetch、retrieve和get来给在多个类中的同种方法命名。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余的浏览就找到正确的方法。
2.12 别用双关语
- 避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有add方法。只要这些add方法的参数列表和返回值在语义上等价,就一切顺利。这样做貌似和其他add方法保持了一致,但实际上语义却不同,应该用insert 或append之类词来命名才对。把该方法命名为add,就是双关语了。
2.13 添加有意义的语境
- 很少有名称是能自我说明的——多数都不能。反之,你需要用有良好命名的类、函数或 名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。
// 初始版
private void printGuessStatistics(char candidate, int count) {
String number;
String verb;
String pluralModifier;
if (count == 0) {
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "l";
verb = "is";
pluralModifier = "";
} else (
number = Integer.toString(count); .
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier);
print(guessMessage);
}
// 进阶版
public class GuessStatisticsMessage {
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count) {
createPluralDependentMessageParts(count);
return String.format(
"There %s %s %s%s",
verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count) {
if (count == 0) {
thereAreNoLetters();
} else if (count == 1) {
therelsOneLetter ();
}
else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void therelsOneLetter() (
number = "1";
verb = "is";
pluralModifier ="";
}
private void thereAreNoLetters() {
number = "no";
verb = "are”;
pluralModifier = "s";
}
}
2.14 本章小节
- 我们有时会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改得更好,那大家真的会感激你。多数时候我们并不记忆类名和方法名。我们使用现代工具对付这些细节,好让自己集中精力于把代码写得就像词句篇章、至少像是表和数据结构(词句并非总是呈现数据的最佳手段)。改名可能会让某人吃惊,就像你做到其他代码改善工作一样。别让这种事阻碍你的前进步伐。
3.函数
3.1 短小
函数应该短小精悍,不超过20行
3.2 只做一件事
函数应该做一件事。做好这件事。只做这一件事。
3.3 每个函数一个抽象层级
3.4 switch 语句
- 写出短小的switch语句很难。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。
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);
}
该函数有好几个问题。首先,它太长,当岀现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则。因为有好几个修改它的理由。第四,它违反了开放闭合原则。因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。
该问题的解决方案:是将switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对雾,而且隐藏在某个继 承关系中,在系统其他部分看不到,就还能容忍。当然也要就事论事,有时我也会部分或全部违反这条规矩。
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 EmployeeFactorylmpl 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.6 函数参数
- 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。
3.7 无副作用
- 副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
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函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。如果一定要时序性耦合,就应该在函数名称中说明。在本例中,可以重命名函数为checkPasswordAndlnitializeSession, 虽然那还是违反了 “只做一件事”的规则。
3.8 分隔指令号询问
- 函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。
public boolean set(String attribute, String value);
if (set("username", "unclebob")){}
从读者的角度考虑。它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还是形容词并不清楚。
3.9 使用异常替代返回错误码
- 从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在if语句判断中把指令当作表达式使用。这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。
// 错误示例
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(nconfigKey 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.10 别重复自己
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。
3.11 结构化编程
- 每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
- 所以,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto只在大函数中才有道理,所以应该尽量避免使用。
3.12 本章小结
本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
4 注释
4.1 注释不能美化糟糕的代码
- 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样得多。与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。
4.2 用代码来阐述
有时,代码本身不足以解释其行为。不幸的是,许多程序员据此以为代码很少。如果有的话,能做好解释工作。
// 错误示例
if ((employee.flags == HOURLY.FLAG) && (employee.age > 65)){}
//正确示例
if (employee.isEligibleForFullBenefits()){}
只要想上那么几秒钟,就能用代码解释你大部分的意图。很多时候,简单到只需要创建一个描述与注释所言同一事物的函数即可。
4.3 好注释
有些注释是必须的,也是有利的。来看看一些我认为值得写的注释。不过要记住,唯一真正好的注释是你想办法不去写的注释。
4.4 坏注释
大多数注释都属此类。通常,坏注释都是糟糕的代码的支撑或借口,或者对错误决策的修正,基本上等于程序员自说自话。
4.5 本章小结
本章主要解释了好注释的重要性,当然,最好的注释就是不写注释,直接用代码代替注释。