1 有意义的命名
1.1 名副其实
变量、函数或类的名称应该已经答复了所有的大问题,她该告诉你,为什么存在,做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。
int d; // 消失的时间
名称d什么也没说明。但是下面的名称是否会好一点呢
int elapsedTimeIndays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeIndays;
1.2 做有意义的区分
作为反面教材,没有意义的区分通常分为以下两类:
- 以数字系列命名,例如在下面的例子中,如果将参数改为source和destination,这个函数就会像样很多。
public static void copyChars(char a1[], char a2[]) {
for (int i = 0; i < a1.length; i++){
a2[i] = a1[i];
}
}
- 废话是另一种没有意义的区分,例如有一个Product类,还有一个名为ProductData或ProductInfo的类,那她们的名称虽然相同,意义却无区别。variable一词永远不应当出现在变量名中,table一词永远不应当出现在表名中。还有就是像下面的三个函数,你应该调用哪一个呢?
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
1.3 使用可搜索的名称
对于单字母名称和数字常量,有一个问题,就是很难在一大篇文字中找出来。找MAX_CLASSES_PER_STUDENT很容易,但是找m就麻烦了。名称的长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,则应赋予其便于搜索的名称。再比较
for (int i = 0; i < 34; i++){
s += (t[j] * 4) / 5;
}
和
int realDaysPerIdealDay = 4;
int WORK_DAYS_PER_WEEK;
int sum = 0;
for (int i = 0; i < NUMBER_OF_TASKS; i++) {
int realTaskDays = taskEstimate[i] * realDaysPerIdealDay;
int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
采用能表达意图的名称,貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK要比5好找得多,另外,当你需要在一个工程中,快速定位代码位置,起一个易于搜索的名称,也会大大提高效率。
1.4 类名与方法名
类名和对象名应该是名词或名词短语,例如Customer、WikiPage、Account或AddressParser。
方法名应当是动词或动词短语,如postPayment、deletePage或save。
1.5 添加有意义的语境
假设你有名为firstName、lastName、street、houseNumber、city、state和zipcode的变量,把她们放一块儿的时候,很明显的构成了一个地址,但是假设只是在某个方法中看见孤零零的一个state变量呢?你会看出来她是某个地址的一部分呢。可以添加前缀,例如:addressFirstName等。当然更好的做法是创建Address类。
1.6 不要添加没用的语境
假设有一个名为“加油站豪华版”(Gas Station Deluxe)的应用,在其中每个类添加GSD前缀就不是什么好点子了,说白了,你是在和自己的idea过不去,输入G想要idea提示可用类的列表,列表恨不得有一英里那么长。为什么要搞得idea没有办法帮助你。
只要短名称足够清楚,就比长名称好。别给名称添加不必要的语境。
2 函数
2.1 短小
函数的第一条规则是要短小,第二条规则是还要更短小。
代码块和缩进
if、else、while语句等,其中的代码块应该只占一行,该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,所以增加了文档上的价值。
这也意味着函数不应该容纳嵌套结构。所以,函数的缩进层级,不应该多于一层或两层。当然,这样的函数易于阅读和理解。
2.2 只做一件事
函数应该做一件事。做好这件事。只做一件事。
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把较大的概念,拆分为另一抽象层上的一系列步骤。
public class ProductServiceImpl{
@Autowired
private ProductProertyRepository propertyRepository;
@Autowired
private ProductPriceReposiotry priceRepository;
public ProductBO getProduct(String skuId) {
if (StringUtils.isBlank(skuId){
return new ProductBO();
}
ProductPropertyBO property = propertyRepository.getProperties(skuId);
ProductPriceBO price = priceRepository.getPrice(skuId);
return enrichProductBO(property, price);
}
}
例如上面这段代码,其实做了四件事:
- 判断skuId是否为空,如果为空,返回空对象。
- 调用属性Repository,查询商品属性。
- 调用价格Repository,查询商品价格。
- 封装数据。
其实可以说,这四件事是在同一抽象层上。而具体的查询商品属性逻辑,不应该是和其他三步操作,在同一层级才对。
2.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句就要在同一层级上。函数中混杂不同的抽象层级,往往会让人迷惑,读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破窗效应,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
比如上面的getProduct方法中,属性和价格就是基础概念,具体的细节分别在propertyRepository和priceRepository中实现。
2.4 Switch语句
写出短小的Switch语句很难(当然,也包括if/else语句),即便是只有两种条件的Switch语句,也比我想要的单个代码块或函数大得多。
写出只做一件事的Switch语句也很难,Switch天生要做N件事。不得不写Switch语句的时候,要确保每个Switch都埋藏在较低的抽象层级,而且永远不重复。例如下面这段代码:
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return cauculateCommissionedPay(e);
case SALARIED:
return cauculateSalariedPay(e);
case HOURLY:
return cauculateHourlyPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
该函数有以下几个问题:
- 太长,当出现新的员工类型时,还会变得更长。
- 违反了单一职责原则(Single Responsibility Principle),因为有好几个修改她的理由。
- 违反了开闭原则(Open Closed Principle),每当添加新类型时,都必须修改该函数。
不过该函数最麻烦的是,可能到处都有类似结构的函数。例如可能会有判断是否发薪日,发放工资等函数。
isPayday(Employee e, Date date);
deliverPay(Employee e, Money pay);
该问题的解决方案是利用多态(或者工厂+策略模式),抽象出来一个员工顶层类Employee,然后分别定义股东员工CommissionedEmployee、普通员工SalariedEmployee、钟点工HourlyEmployee,如下所示:
public abstract class Employee{
public abstract Money calculatePay(Employee e);
public abstract isPayday(Employee e, Date date);
public abstract deliverPay(Employee e, Money pay);
}
public class CommissionedEmployee extends Employee{...}
public class SalariedEmployee extends Employee{...}
public class HourlyEmployee extends Employee{...}
2.5 使用具有描述性的名称
Ward原则:如果每个程序都让你感到符合预期,那就是整洁代码。
You know you are working on clean code when each routine turns out to be pretty much what you excepted
函数越小,功能越集中,就越便于起个好名字。长而具有描述性的名称,要比短而令人费解的名称好。
给函数起个好名字,能较好地解释函数的意图,对于单参数函数,函数和参数应当形成一种非常良好的动词/名词对形式,例如write(content)就表示我要调用write函数写入content。
2.6 函数参数
最理想的参数数量是0,其次是1,再次是2,有足够特殊的理由,才能用3个以上参数。
从测试的角度看,参数更叫人为难,如果你要为一个具有10个参数的函数,编写单元测试代码,想想都很费劲吧。
向函数传入布尔值,简直是骇人听闻。这样做相当于大声宣布,本函数职责不够单一,如果参数为true会这样做,如果为false会那样做。
如果函数看起来需要多个参数,就说明其中一些参数应该封装为类了。例如下面的差别:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
把多个参数创建为一个对象,看似是在作弊,其实不然,当一组参数被共同传递,往往就是这些参数共同组成了一个概念。就像上面的x和y一样,她们一起组成了坐标点Point的概念。
2.7 无副作用
函数承诺只做一件事,但还是会做其他被隐藏的事,例如下面这段代码,看似无伤大雅,就是查出来用户信息,然后对输入的密码进行加密,然后对比用户真实密码,以此校验密码。
public class UserValidator{
public boolean checkPassword(String userName, String password) {
User user = userRepository.getByName(userName);
if (user == null) {
return false;
}
String userPassword = user.getPassword();
String cryptedPassword = ctyptographer.crypted(password);
if (userPassword.equals(cryptedPassword)) {
Session.initialize();
return true;
}
return false;
}
}
显然,副作用就在于对Session.initialize()的调用,checkPassword函数,顾名思义,就是用来检查密码的,并未表明她会初始化session。
而且这一副作用造成了时序性的耦合,checkPassword只能在特定时刻调用。如果在不合适的时候调用,session数据就会莫名其妙的丢失。
2.8 分隔写操作与读操作
函数要么做点什么事情,要么回答什么事情,但是不要两件事情都做。函数应该修改某对象的状态,或是返回该对象的有关信息。如果两样都干,常会导致混乱。例如
public boolean set(String attribute, String value);
该函数设置某个属性值,如果成功就返回true,如果属性不存在,就返回false。这样就会导致下面的调用:
if (set("name", "Bryan"))...
当读者看到这个if语句的时候,就会有歧义:
- name属性是否之前已经设置为Bryan了;
- name属性是否成功设置为Bryan。
而解决方案就是把读操作和写操作分隔开来,防止混淆的产生。
if (attributeExsists("name")) {
setAttribute("name", "Bryan");
}
2.9 不要重复自己
它的英文描述为:Don’t Repeat Yourself。中文直译为:不要重复自己。
代码复用性:代码的可复用性表示一段代码可被复用的特性或能力,我们在编写代码的时候,让代码尽量可复用。
如何提高复用性:
- 少代码耦合,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。
- 满足单一职责原则,越细粒度的代码,代码的通用性会越好,越容易被复用。
- 模块化,这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
- 业务与非业务逻辑分离,越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。
- 通用代码下沉,从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。
- 继承、多态、抽象、封装,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
- 应用模板等设计模式,一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。
函数小结
写代码和写文章很像,初稿也许粗陋无序,你可以对其斟酌推敲,直至达成你心目中的样子。大师级程序员把系统当做故事来讲,而不是当做程序来写。真正的目标在于讲述系统的故事,而你编写的函数,必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
3 注释
“别给糟糕的代码加注释——重新写吧”,如果你发现自己需要写注释,就再想想看,是否有办法用代码来表达。
注释存在的时间越久,离其所描述的代码就越远,就越来越变得全然错误。原因很简单:程序员不能坚持维护注释。
带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而又复杂的代码,像样的多。与其花时间为糟糕的代码编写注释,不如花时间清理那堆糟糕的代码。
你愿意看到这个:
// 校验员工是否符合福利的条件
if (employee.flags & HOURLY_FLAG ** employee.age > 65)
还是下面这个:
if (employee.isEligibleForBenefits())
最好的代码是没有注释,代码已经清晰明了的表达了作者的意图;其次是代码没有那么的清晰,但是有一定的注释可以帮你了解;最差的就是代码看不明白,注释也混乱不清。
4 格式
你今天编写的功能代码,极有可能在下一版本中被修改,但代码的可读性却会对以后可能发生的修改行为产生深远影响。
4.1 向报纸学习
想想写的好的报纸文章,你从上往下阅读,在顶部,你期望有个头条,告诉你故事主题,好让你决定是否读下去。第一段是整个故事的大纲,给出粗线条概述,但隐藏了故事细节。接着读下去,细节渐次增加,直至你了解所有的日期、名字、说法及其他细节。
源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足以告诉我们是否在正确的模块中。源文件最顶部应该给出高层次的概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。
4.2 垂直格式
你是否曾经在某个类中探索,从一个函数跳到另一个函数,上下求索,想要弄清楚这些函数如何操作、如何互相相关,最后却被搞糊涂了。
关系密切的概念应该互相靠近,应避免迫使读者在源文件中跳来跳去。
变量声明。变量声明应尽可能靠近其使用位置。
实体变量。实体变量应该在类的顶部声明。这不会增加变量的垂直距离,因为在设计良好的类中,她们应该会被大多数方法使用。
相关函数。若某个函数调用了另外一个,就应该把她们放到一起,而且调用者尽可能放在被调用者上面。这样程序就会有自然的顺序。
概念相关。概念相关的代码应该放到一起。代码的相关性越强,彼此之前的距离就应该越短。
4.3 横向格式
程序员们都喜欢短代码行,应该尽力保持代码行短小。最好是无需拖动滚动条,就可以看到代码最右边。
横向的分隔可以参考《阿里巴巴Java开发手册》。
public static void main(String[] args) {
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}