[读书笔记] 代码整洁之道(一): 有意义的命名和函数定义
第二章 有意义的命名
- 名副其实
变量、函数或类的名称能够发福所有的大问题,不需要注释来补充。 - 避免误导
避免留下掩藏代码本意的错误线索,避免使用与本意相悖的词。 - 做有意义的区分
不要用废话来代替有意义的名称。 - 使用能读得出来的、可搜索的名称
- 避免使用编码
不需要用前缀来表明成员变量。 - 避免思维映射
避免一些聪明人定向思维,根据你的名称直接翻译为他们熟知的名称。 - 类名
类名和对象名应该是名词或名词短语,不应当是动词。 - 方法名
方法名应当是动词或动词短语。 - 每一个概念对应一个词,别用双关语
第三章 函数
-
短小
函数的第一规则是短小。 -
只做一件事情
函数应该做一件事;做好这件事;只做这一件事。要判断函数是否不止做了一件事,就是看能否再拆出一个函数(该函数不仅只是单纯的重新诠释其实现)。 -
每个函数一个抽象层级
自顶向下读代码:向下规则,要让代码有自顶向下的阅读顺序,让每个函数后面都跟着位于下一抽象层级的函数。 -
switch语句
switch语句很难(包括if/else),天生要做N件事,可以将其埋藏在较低的抽象级层,且永不重复。但是可以利用多态来实现。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); } }
这个函数有几个问题:1)太长,当出现新的雇员类型时会更长;2)明显不止做了一件事;3)违反了单一权责原则SRP,因为有好几个修改它的理由;4)违反了开放闭合原则OCP, 每当添加新的类型时,就必须修改。
解决方案:将switch语句埋到抽象工厂底下,不让任何人看到。详见
设计模式——抽象工厂模式(Coming out...)
5. 使用描述性名称
描述性名称能清理关于模块的设计思路。
6. 函数参数
函数参数最理想是零,其次是一,再次是二,尽量避免三,有特殊理由才能用三个以上的参数。
* 一元函数有两种普遍理由:1)转换:例如将String转换为InputStream类型的返回值;2)事件:使用参数修改系统状态。
* 标识参数:标识函数丑陋不堪,向函数传入布尔值更是骇人听闻,直接表明该函数不止做了一件事。
* 二元函数:一定要注意参数的顺序,很可能被忽略。二元函数可以通过一些机制转换成一元函数。
* 三元函数:更加容易忽略参数。
* 参数对象:如果函数看上去需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
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函数可以被理解为用来检查密码的。该名称未暗示它会初始化该次会话。所有调用者只是为了检查用户有效性,而误操作抹除会话数据的风险。
* 输出参数
应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态。
8. 分隔指令与询问
函数要么做什么事,要么回答什么事,不可兼得。函数应该修改某对象的状态,或是返回该对象的有关信息。解决方案:将指令与询问分隔开来。
9. 使用异常替代返回错误码
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;
}
if/else导致了更深层次的嵌套结构。当返回错误码时,就是要求调用者立刻处理错误。使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}catch (Exception e) {
logger.log(e.getMessage());
}
* 抽离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函数只与错误处理有关,deletePageAndAllReferences函数只与删除page有关,错误处理可以忽略掉。
* 错误处理就是一件事。
* 返回错误码就会通过一个类或者枚举来定义所有错误码,很多类都会产生依赖,当这些类产生修改时,其他类都需要重新编译。使用异常代替错误码,新异常就可以从异常类派生出来。
- 不可重复自己——DRY原则
面向对象过程中将代码集中到基类,从而避免冗余。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】凌霞软件回馈社区,携手博客园推出1Panel与Halo联合会员
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步