整洁代码设计

前言

阅读本书有两种原因,第一,我是个程序员;第二,我想成为更好的程序员

(一)整洁代码的艺术

写整洁代码,需要建循大量的小技巧,贯彻刻苦习得的这种“代码感”就是关键所在.有些人生而有之.有些人费点劲才能得到.它不仅让我们看到代码的优劣,还予我们以借戒规之力化劣为优的攻略.
缺乏“代码感"的程序员,看混乱是混乱,无处着手.有“代码感”的程序员能从混乱中看出其他的可能与变化.“代码感”帮助程序员选出最好的方案,并指导程序员制订修改行动计划,按图索骥,简言之,编写整洁代码的程序员就像是艺术家,他能用一系列变换把一块白版变作由优雅代码构成的系统.

(二)什么是整洁的代码

优雅和高效的代码.代码辑应当直接了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善代码;性能调至最优,省得引诱别人做没规矩的优化,捣出一堆混乱来.整洁的代码只做好一件事.
完善错误处理代码.往深处说就是在细节上花心思.敷衍了事的错误处理代码
只是程序员忽视细节的一种表现.此外还有内存泄漏,还有竞态条件代码.还有前后不一致的命名方式.结果就是凸现出整洁代码对细节的重视
整洁的代码力求集中.每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染.
整洁的代码应可由作者之外的开发者阅读和增补.它应当有单元测试和验收测试.它使用有意义的命名.它只提供一种而非多种做一件事的途径.它只有尽量少的依赖关系,而且要明确地定义和提供清晰尽量少的 API.代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可通过代码自身清晰表达.整洁系于测试之上.遵循测试驱动开发(Test Driven Development)规程.没有测试的代码不干净.不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也.
简单代码,依其重要顺序:
— 能通过所有测试;
— 没有重复代码;
— 体现系统中的全部设计理念;
— 包括尽量少的实体,比如类、方法、函数等

(三)有意义的命名

1:见名知意,变量和函数等命名应该见名知意,能体现本意,能够让其他程序员能够根据名称了解函数和变量的意义

2:避免误导:应当避免使用与本意相悖的词,不要使用一些特定的专有名词.减少名称带有List之类的命名,缺乏实际的意义.比如使用accountGroup就比使用accountList更有实际意义. 避免使用单字母的命名变量方式,即使该变量作用域很小或者只使用一次。避免使用带数字式的命名方式

3:做有意义的区分;日常中通用的名词不要使用,比如Variable、name、table、id因为这些词的适用场景太广了,让我们无法区分;方法和变量的命名要有一定的区分和限制,减少相近的函数命名,比如下面,调用者将无法区分该调用那个,似乎功能是一致的
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

4:使用读得出来的名称
人类长于记忆和使用单词。大脑的相当一部分就是用来容纳和处理单词的。单词能读得出来。人类进化到大脑中有那么大的一块地方用来处理言语,若不善加利用,实在是种耻辱。

5:使用可搜索的名称
采用能表达意图的名称, 貌似拉长了函数代码,但要想想看,WORK_DAYS_PER_WEEK 要比数字 5 好找得多,而列表中也只剩下了体现作者意图的名称。

6:匈牙利语标记法

7:消除对成员前缀的需要

8:接口和实现
有时也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)。该工厂是个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?IShapeFactory 和 ShapeFactory 吗?我喜欢不加修饰的接口。前导字母 I 被滥用到了说好听点是干扰,说难听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就想让他们知道那是个 ShapeFactory。如果接口和实现必须选一个来编码的话,我宁肯选择实现。ShapeFactoryImp,甚至是丑陋的 CShapeFactory,都比对接口名称编码来得好

9:类名
类名和对象名应该是名词或名词短语,如 Customer、WikiPage、Account 和 AddressParser。 避免使用 Manager、Processor、Data 或 Info 这样的类名。类名不应当是动词。

10:方法名
方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。属性访问器、修改器 和断言应该根据其值命名,并依 Javabean 标准[18]加上 get、set 和 is 前缀。

11:别用双关语 :避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有 add 方法。只要这些 add 方法的参数列表和返回值在语义上等价,就一切顺利。 但是,可能会有人决定为“保持一致”而使用 add 这个词来命名,即便并非真的想表示这种意思。比如,在多个类中都有 add 方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到群集(collection)中。该把这个方法叫做 add吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应该用 insert 或 append 之类词来命名才对。把该方法命名为 add,就是双关语了。 代码作者应尽力写出易于理解的代码。我们想把代码写得让别人能一目尽览,而不必殚精竭 虑地研究。我们想要那种大众化的作者尽责写清楚的平装书模式;我们不想要那种学者挖地三尺 才能明白个中意义的学院派模式

12:使用解决方案领域名称 :记住,只有程序员才会读你的代码。所以,尽管用那些计算机科学(Computer Science,CS) 术语、算法名、模式名、数学术语吧。依据问题所涉领域来命名可不算是聪明的做法,因为不该 让协作者老是跑去问客户每个名称的含义,其实他们早该通过另一名称了解这个概念了。 对于熟悉访问者(VISITOR)模式的程序来说,名称 AccountVisitor 富有意义。哪个程序员 会不知道 JobQueue 的意思呢?程序员要做太多技术性工作。给这些事取个技术性的名称,通常是 最靠谱的做法。

13:添加有意义的语境,不要添加没用的语境
很少有名称是能自我说明的—多数都不能。反之,你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。 设想你有名为 firstName、lastName、street、houseNumber、city、state 和 zipcode 的变 量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零 零一个 state 变量呢?你会理所当然推断那是某个地址的一部分吗?可以添加前缀 addrFirstName、addrLastName、addrState 等,以此提供语境。至少,读者会 明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address 的类。这样,即 便是编译器也会知道这些变量隶属某个更大的概念了。

(四)函数设计

1:代码短小
函数的第一规则是要短小。第二条规则是还要更短小.函数也不该有 100 行那么长,20 行封顶最佳.每个函数都一目了然。每个函数都只说一件事。而且,
每个函数都依序把你带到下一个函数。这就是函数应该达到的短小程度

public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}

2:代码块和缩进
if 语句、else 语句、while 语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。
备注:比如上面图片中的代码,每个分支只有一行代码,每行代码调用一个功能函数.

3:只做一件事
函数应该做一件事。做好这件事。只做这一件事。
比如任何一个函数都可以简化下面的模板:
(1)判断是否为XXXX;
(2)如果是,则进行XXXX操作,否则进行XXX操作;
(3)最终需要完成XXX,并确认是否返回

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);
} }

该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则(SingleResponsibility Principle[29], SRP), 因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle[30], OCP), 因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。 例如,可能会有

isPayday(Employee e, Date date),deliverPay(Employee e, Money pay)

如此等等。它们的结构都有同样的问题。 该问题的解决方案(如代码清单 3-5 所示)是将 switch 语句埋到抽象工厂[31]底下,不让任何人看到。该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如 calc
ulatePay、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);
}} }}

5:使用描述性的名称
沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”要遵循这一原则,泰半工作都在于为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。

6:函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),
再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参
数(多参数函数)—所以无论如何也不要这么做。

7:抽离 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 有关。错误处理可以忽略掉。有了这样美妙的区隔,代码就更易于理解和修改了。

发布了67 篇原创文章 · 获赞 9 · 访问量 5150

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/104425855