1. 整洁代码
糟糕的代码
糟糕的代码会导致一个系统难以维护,软件故障迟迟无法修复,严重时会毁了一家公司。
混乱的代价
赶上期限的唯一办法就是尽可能保持代码整洁,否则当下一个任务来时你又会被上一次写的糟糕代码给拖后腿
童子军规则
让营地比你来时更干净。每次更新时的代码都比提交时的代码干净,那么代码就不会腐坏。清理也许只是改好一个变量名,拆分一个过长的函数,消除重复的代码,清理一个嵌套语句。
2. 有意义的命名
名副其实
如果名称需要注释来补充,那就不算名副其实。
int d; // 消逝的时间,以日计
名称 d 没有说明任何东西。应该选择指明了计量对象和计量单位的名称:
int elapsedTimeInDays;
int daysSinceCreation;
下面这段代码难以看出来是做什么的
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list.add(x);
return list1;
}
假如我们在开发一款扫雷游戏,盘面是名为 theList 的单元格列表,每个单元格都有状态值,状态值为 4 标识已标记,则修改后的代码如下
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
再进一步,不用 int 数组表示单元格, 而是另写一个类,修改后的代码如下
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<int[]>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
避免误导
当你用小写字母 l 和大写字母 O 作为变量名时,代码看上去是这样的
int a = l;
if (0 == l)
a = O1;
else
l = 01;
做有意义的区分
以数字系列(a1、a2、……、aN)命名只会误导其他人,没有提供作者意图的线索,比如下面的代码
pubic void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++) {
a2[i] = a1[i];
}
}
假如我们是要把源数组赋值给目标数组,那修改后的代码是这样的
pubic void copyChars(char source[], char destination[]) {
for (int i = 0; i < a1.length; i++) {
destination[i] = source[i];
}
}
假设你现在有 Product 类、ProductInfo类 和 ProductData 类,它们虽然名字不同,但意思确实相同的,Info 和 Data 是意义含糊的废话。
不要画蛇添足。Variable 不应该出现在变量名中,Table 不应该出现在表名中。NameString 不比 Name 好,因为 Name 不可能是一个浮点数。
如果缺少明确规定,那 moneyAmount 就和 money 没区别,customerInfo 与 customer 没区别,accountData 与 account 没区别。
假如你正在创建一个抽象工厂,那 ShapeFactory 就比 IShapeFactory 好,因为用户没必要知道这是一个接口,而接口的实现用 ShapeFactoryImpl 就好了。
使用读得出来的名称
你知道下面的代码怎么读吗?
class DtaRcrd102 {
private Date genymdhs;
private final String pszqint = "102";
}
再看看这个
class Customer {
private Date generateionTimstemp;
private final String recordId = "102";
}
“嘿,老王,看看这条记录,生成时间戳被设置成明天了,不能这样吧?”
使用可搜索的名称
你很难搜索到单字母名称和数字常量。找 MAX_CLASSES_PER_STUDENT 很容易,但想找数字 7 就麻烦了,它很可能是其他变量或常量的一部分。比如下面的代码
for (int j = 0; j < 34; j++) {
s += (t[j] * 4) / 5;
}
类名
类名应当是有意义的名词,比如 Csutomer、WikiPage 和 AddressParser,避免使用 Manage、Process、Data 或 Info 这样的动词和意义含糊的名词。
方法名
方法名应当是动词或动词短语,如 deletPage 或 save
别耍宝
holyShit 这样的方法名或许会博人一笑,但是没有人能从这个名字看出来它是做什么的。
每个概念对应一个词
fetch、retrieve 和 get 都有获取的意思,整个系统中应该只使用其中一个,否则难以记住哪个类中是哪个方法。
DeviceManager 和 ProtoolConroller 有什么区别呢?为什么不统一用 Manager 或 Controller 呢?一以贯之的命名对用到你代码的程序员来说就是天降福音。
最后的话
有时候我们会怕其他开发者反对重命名。但是如果讨论一下就知道,如果名称改好了,大家真的会感激你。
不妨在你的工作中试试上面这些规则,看看代码可读性是否有所提升。
3. 函数
下面代码想做的事情是测试 HTML 页面(省略前约为 50 行)
// 代码清单 3-1
public static String testtableHTML (
PageData pageData,
boolean includeSuiteSetup
) throws Exception {
WikiPage wikiPage = pageData.getWikipage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(SuiteResponder.SITE_SETUP_NAME, wikipage);
if (suiteSetup != null) {
WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
Strubg pagePathName = PathParser.render(pagePath);
buffer
.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
WikiPage setup = PageCrawlerImpl.getInheritedPage("Setup", wikiPage);
if (setup != null) ...;
if (pageData.hasAttribute("Test")) ...;
if (tearDown != null) ...;
if (includeSuiteSetup) ...;
}
}
return pageData.getHtml();
}
上面这段代码做了太多事情,抽离和重命名的方式重构后,代码如下
public static String renderPageWithSetupAndTeardowns (
PageData pageData, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
WikiPage testPage = pageData.getWikipage();
StringBuffer newPageContent = nwe StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageCOntent.append(pageData.getCOntent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString);
}
return pageData.getHtml();
}
函数要短小
比如上面的代码可以缩短成
// 代码清单 3-3
public static String renderPageWithSetupAndTeardowns (
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
if 、else 和 while 等语句中的代码块应该只有一行,函数的缩进层级不该多于一层或两层。
函数应该只做一件事
代码清单 3-1 做了好几件事。创建缓存区、获取页面、渲染路径等等。而代码清单 3-3 则只做了一件事,将对测试的设置和拆解包含在页面中。
只做一件事的函数有个特征,就是无法被切分成多个区段。
函数参数
最理想的参数数量是无参数,其次是单参数,再次是双参数,应避免三参数。
参数封装
如果函数需要两个、三个或三个以上,那就说明其中一些参数应该封装成类了。比如下面的代码
图14Circle makeCircle(double x, double y);
Circle makeCircle(Point center);
动词与关键词
一元函数的函数名和参数应当形成一种良好的动词/名词对形式,比如 write(name) 就很清晰。
无副作用
下面的代码匹配 username 和 password,但是它有副作用,你看得出来吗
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String username, String password) {
User user = UserGateway.findByName(username);
if (user != USER.NULL) {
String phrase = cryptographer.decrypt(password);
if ("Valid Password".equal(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
上面代码的副作用就在于 Session.initialize() 的调用。函数名叫做 checkPassword,但是却进行了会话初始化。因此当你误信它时,就会有抹除会话数据的风险。
你可能想到了把它重名为 checkPasswordAndInitializeSession,但是这样却违反了 “只做一件事” 的规则。
分开操作和判断
如何设置某个属性成功则返回 true,不成功则返回 false,那就会写出这样的语句
public boolean set(String attribute, String value);
if (set("username", "userclebob"))...
这是什么意思?问的是 username 是否已经是 unclebob?还是 username 是否设置成功为 unclebob 呢?下面是把操作与判断分开后的代码
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
使用异常替代返回错误码
不使用异常的代码是这样的
if (deletePage(page) == OK) {
if (registry.deleteReference(page.name) == OK) {
logger.log("page deleted");
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return ERROR;
}
而使用异常的代码是这样的
try {
deletePage(page);
registry.deleteReference(page.naem);
logger.log("page deleted");
} catch (Exception e) {
logger.log(e.getMessage());
}
抽离 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);
}
4. 注释
注释不能美化糟糕的代码
写注释的常见动机之一就是解释糟糕的代码。但是糟糕代码最好的解决办法是把代码弄干净。
用代码阐述
你是想看到这样的代码
// 检查雇员是否有权享受员工福利
if ((employee.flags & HOURLY_FLAG) && (employye.age > 65))
还是这个
if (employee.isEligibleForFullBenifits())
好注释
法律信息
版权和著作权声明就有理由放在每个源文件开头处,例如下面的
// Copyright (C) 2003, 2004, 2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public Licence version 2 or later.
警示
用警告的方式告诉其他程序员会出现某种后果的注释也是有用的,比如下面的的注释解释了为什么要关闭某个特定的测试用例
// 除非你闲得慌,否则别运行
public void testWithReallyBifFile() {
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToTsend(this);
String responseString = output.toString();
asert...
}
TODO 注释
有时用 TODO 的行驶在源代码中放置要做的工作,比如下面的注释解释了为什么该函数的实现部分无所作为,应该是怎么样的
// TODO
// 这个函数在我们检查模型的时候不需要
protected VersionInfo makeVersion() throws Exception {
return null;
}
公共 API 中的 Javadoc
如果你在编写公共 API ,就该为它编写良好的 Javadoc。但是要注意,和其他注释一样,Javadoc 也可能误导或提供错误信息
坏注释
循规式注释
要求每个函数都有 Javadoc 是多余的,比如下面的代码
/**
* @param title CD 标题
* @param author CD 作者
*/
public void addCd(String title, String author) {
Cd cd = new Cd();
cd.title = title;
cd.author = author;
}
废话注释
一些不言而喻的代码是不需要注释的,比如下面的
// 默认构造器
protected AnnualDateRule() {
}
/* 某月天数 */
private int daysOfTheMonth;
注释掉的代码
你注释掉的代码可能别人不敢删,然后就一直留在那里。但是我们已经有像 Git 这种良好的代码管理工具,因此需要注释的代码直接删除即可,它们肯定丢不了。
5. 格式
每个程序员都有自己喜欢的代码风格,但如果在一个团队中工作,则团队成应该使用一种统一的代码风格。
6. 错误处理
别返回 null 值
返回 null 值时有可能导致下面这样的代码,只要一处没检查 null,应用就会崩溃。
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = persistentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItme(item.getID());
if( existing.getBillingPrioed().hasRetailOwner()) {
existing.register(Item);
}
}
}
}
Java 提供了 emptyList 方法,在需要返回 List 对象的视乎可以这样返回
public List<Employee> getEmployees() {
if(no employee) return Collections.emptyList();
}
别传递 null 值
比如用下面的方法计算两点的映射
public class MetricsCalculator {
public dobule projection(Point p1, Point p2) {
return (p2.x - p1.x) 1.5;
}
}
这时候如果传入一个 null 值就会抛出空指针异常。null 值几乎永远避免,一般的处理是禁止传入 null 值。
public class MetricsCalculator {
public double projection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
7. 类
类应该短小
单一职责原则
单一职责指的是类或模块应有且只有一条修改的理由。
内聚
类应该只有少量实体变量,类中每个方法都应该操作这些变量中的一个或多个。通常而言,方法操作的变量越多,就越内聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内举行。
但是一般来说,创建这种极大化内聚类是不可取也不可能的。下面有一个 Stack 类的实现,该内有很高的内聚性。
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack ++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStakc == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
为了修改而组织
对于多数系统,修改将一直延续。在整洁的系统中,我们对类加以组织,降低修改的风险。下面是的 Sql 类用来生成 SQL 格式化字符串,但是它还不支持 update 语句,当 Sql 类支持 update 语句时,我们就得对这个类进行修改,随之而来的风险是破坏类中的其他代码。
public class Sql {
public Sql(String table, Column[] columns){
...
}
public String create() {
...
}
public String insert(Object[] fields) {
...
}
}
当新增一条语句时,就要修改 Sql 类,这说明 Sql 违反了 SPR(单一职责)原则。下面的代码把 Sql 中的方法拆分成了几个类。
public abstract class Sql {
public Sql(String table, Column[] columns){
...
}
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns) {
...
}
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields) {
...
}
}
现在如果需要增加 update 语句,已有的类无需修改,只需要增加新类。
网友评论