从字面意思上理解,整洁的代码,对于程序员来说非常的一目了然,简单、整洁,结构清晰,逻辑清楚。代码其实是一种语言,传递的是逻辑,如果这份代码可以像我们说话一样快速的将逻辑传递给读者,那么这样一份代码就是一份整洁的代码。可以借助沃德原则:"如果每段代码欧让你感到深合已意,那就是整洁代码"
首先便是要有保持代码整洁的意识,书中反复提到的提到的一条童子军军规:让营地比你来时更干净。要写好整洁的代码,需要我们心中有着一个意识,我要写好我的代码,我的代码要让自己和别人看着舒服。
软件中命名随处可见,包括变量、函数、参数、类、目录等等,我们就像是一个父亲,给自己的孩子取一个让别人看到就知道的名字。
通过名称就知道要表达的意思。例如:表示已经消逝的时间天数,使用elapsetimeInDays就比d好得多。表示插入数据成功,用insertSuccess就比flag好得多。
命名时避免使用与本意相悖的词。例如:hp,aix,sco不应做变量名,因为它们是UNIX平台的专有名词。使用accountList来指一组账号,除非它真的是List类型。如果容纳账号的容器是Set,那就会引起错误的判断。在命名时最好不要把容器内容带上,使用accountGroup或者bunchOfAccounts更好。
提防使用相差比较小的名称。例如:XYZControllerForEfficentHandingOfStrings和ZYZControllerForEfficientStorageOfStrings。
误导性真正可怕的例子:用小写字母l和大写字母O做变量。
光是添加数字或是废话来做区分是远远不够的。
添加数字的反例:Spring中关于属性的拷贝方法 BeanUtils.copyProperties(source,target),参数就使用source和target来表示,而不是a1,a2.
使用废话的反例:存在类名Product,ProductInfo,ProductData。名称不同,但意思无区别。废话都是冗余,Variable永远不该出现在变量名中。
例如: (生成日期,年月日时分秒) 变量命名时 gennerationTimeStamp 就比genymdhms好的多,前者比后者读起来好得多,也更好理解。
单字母名称和数字常量有个问题,很难在一大篇文字中找出。找MAX_CLASS_PER_STUDENT很容易,想找数字7就很麻烦。单字母名称仅用于短方法中的本地变量。名称长短应与其作用域相对应,比如在一个类中经常使用的,就可以通过大写字母的方式来表示。
反例: 变量名加入成员前缀 m_name 表示接口时加上I字母 接口名:IShapeFactory
在循环体中通过变量来判断条件时,使用i,j或者k就比其他字母好。千万别用小写字母l和打字字母O。
类名和对象名应该是名字或者名字短语。如:Customer、WikiPage、Account
方法名应该是动词或动词短语,如:postPayment,deletePage或save。
例如: 将数据一半进行删除 deleteHalfData 就比 ringingFinger(打响指) 好,只有看过复联的人才知道。
给每个抽象概念选一个词,并且一以贯之。例如:插入统一同insert或者save。不要有时用insert,有时用save.
例如:在一个类中有两个add方法。一个是求和,一个是添加数据。要么把求和改成sum,要么把添加数据改成append.
只有程序员才会阅读你的代码。根据所涉领域来命名不是最好的方法,可以用一些计算机科学术语,算法名,模式名,数学术语来命名。例如:AccountVisitor,JobQueue
如果不能使用程序猿熟悉的术语来命名,那就采用所设计问题领域的名称来命名吧。至少,维护代码的程序猿可以去请求领域专家。
很少有名称是能自我说明的。你需要用有良好命名的类,函数或名称空间来放置名称,给读者提供语境。
例如: 有名为firstName,lastName,street,houseNumber,city,state和zipcode的变量,当它们一起出现时,很明确构成一个地址。但如果是state单独出现时,就很难推断时表示地址了。可以通过addrState来标识。更好的方案是将类命名为Address。
假设有一个名为“豪华版加油站”(Gas Station Deluxe)的应用,在其中给每个类加上GSD前缀就不是一个好点子。只要短名称足够清楚,就不要添加不必要的语境。
例如:GSDAccountAddress 就可以改成 AccountAddress
我们有时候会怕其他开发者反对重命名。如果讨论一下就知道,如果名称改的更清楚,其实大家都会感激你。
函数的第一规则是要短小。第二条规则是还要更短小。
函数应该只做一件事,做好这件事。
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。函数中混杂不同抽象层级,往往让人迷惑。
反例: 一个方法中调用了其他方法.有getHtml()抽象层比较高的,也有PathParser.render(pathPath)抽象层级中等的,也有builder.append("xxxx")抽象层级底层的。
阅读代码时自顶向下阅读。每个函数后面都跟着位于下一个抽象层级的函数。
例如: A方法中调用了B方法。B方法应该跟在A方法后面,如果A中调用了多个方法,这些方法也应该按照一定逻辑顺序排在A后面。
例如:includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。看到这些方法名,我们就知道这个方法是在做什么事情了。函数越短小,功能越集中,那就越好取名字。别害怕长名称,长而具有描述的名称,比短小而令人费解的名称好。长而具有描述的名称,要比描述性的长注释好。
最理想的函数是零参数函数,其次是单参数函数,再次是二,应尽量避免三参数函数。参数不易对付,它带有太多概念性。
4.5.1 一元函数
向函数传入单个参数由三种普遍的理由。一个是问关于那个参数的问题,例如 boolean fileExist("MyFile").另一个可能是操作该参数,将其转化成其他东西。例如 UserDTO transfer(User user)。还有一个就是事件,有输出参数无输出参数。程序将函数看作一个事件。
4.5.2 二元函数
有两个参数的函数要比一元函数难懂。例如:writeField(name)比writeField(outputStream,name)好懂。如果可以的话,把outputstream提取成当前类的成员变量,从而无需再传递它。
4.5.3 三元函数
有三个参数的函数要比二元函数难懂的多,排序,琢磨,忽略的问题都会加倍提现,建议在写三元函数前一定要想清楚。
4.5.4 参数对象
如果函数需要三个,或三个以上的参数,就说明其中的一些参数应该封装为类了。例如:Circle makeCircle(double x,double y,double radius) 和Circle makeCircle(Point center,double radius);
给函数去个好名字,能较好地解释函数的意图,以及参数的顺序和意图。函数和参数应当形成一种非常良好的动词/名词对形式。例如:writeField(name)就比witre(name)表示的意图更加清晰
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。例如:
boolean checkPassword(userName,password){
User user = UserGateway.findByName(userName);
if(user == null){
return false;
}
if(password.equals(user.getPassword()){
Session.initialize();
return true;
}
}
在验证用户名和密码的方法中,进行了session的初始化操作,这就是该方法的副作用,方法名并没有暗示会进行session初始化的操作,当某个误信了函数名的调用者想要检查密码时,就可能导致当前session被抹除的风险。可以通过修改方法名为checkPasswordAndInitializeSession(),虽然哪还是违反了"只做一件事"的原则。可以将session的初始化放到另一个方法,将检查密码和初始化session分开,在上层来调用两个方法。
函数要么做什么事,要么回答什么事,但二者不可兼得。
反例:if(set("username","horizon")).该set方法的逻辑是先判断 是否有username属性,如果有,则设置horizon,返回true,如果没有则返回false.
应该要把判断属性和设置值分两步来做.
if(attributeExists("username")){
setAttribute("username","horizon");
}
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。
例如: if(deletePage(page) == E_OK) 当返回错误码时,就是在要求调用者根据返回值去处理逻辑。这样会导致深层次嵌套。
直接在deltePage方法中,对错误的逻辑直接抛出异常,就不用去挨个判断返回了.
例如:try{
deletePage(page);
}catch(Exception e){
}
例如:
try{
methodA();
}catch(Exception e){
logError();
}
函数应该只做一件事,错误处理就是一件事。
重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为空志与消除重复而创建。在我们的开发过程中,相同的逻辑可能在多处被使用。这时,我们就应该根据逻辑将相同的逻辑抽离出来。
遵循这些规则,函数就会短小,有个好名字,就可以被很好的归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,帮助你讲故事。
注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败,如果你发现自己需要写注释,再想想看是否有办法通过代码来表达。只有代码能忠实地告诉你它做的事情
写注释的常见动机是糟糕的代码的存在,与其花时间编写解释你搞出的糟糕的代码的注释,不如花时间清洁那堆糟糕的代码。
有时,代码本身不足以解释其行为。但是其可能只有那么一两行,许多程序猿就觉得没有单独抽出去的必要,这种观点纯属错误。
例如:
//check to see if the employee is eligible for full benefits
if((employee.flags & HOURLY_FLAG) && emplyee.age > 65)
代码中对逻辑的判断添加了注释,但是完全有更优的方案,直接把判断的代码抽离成一个方法
if(employee.isEligibleForFullBenefits())
有些注释是必须的,也是有利的。不过要记住,唯一真正好的注释是你想办法不去写的注释
5.4.1 法律信息
有时,公司代码规范要求编写与法律有关的注释。如果可能,就指向一份标准许可或其他外部文档,不要把所有条款放在注释中。
5.4.2 提供信息的注释
//Return an instance of the Responder being tested
protected abstract Responder responderInstance(); 这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。比如:将函数名改成responderBeingTested(),注释就是多余的了。
5.4.3 对意图的解释
有时,注释不仅提供了有关实现的有用信息,而且还提供了决定某个决定后面的意图。
//the status of extract,2 means requested,3 means extracting.
int getExtractStatus()
5.4.4 阐释
有时,注释把某些晦涩难懂的参数或返回值的意义翻译为某种刻度形式,也会是有用的。当然,更好的方法是尽量让参数或返回值自身就足够清楚
例如: assertTrue(a.compareTo(b) == 0) // a == b
5.4.5 警示
有时,用于警告其他程序猿会出现某种后果的注释也是有用的。
例如:
public static SimpleDateFormat makeStadardHttpDateFormat(){
//SimpleDateFormat is not thread safe, so we
//need to create each instance independently
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
}
5.4.6 todo注释
有时,有理由用//TODO形式在源代码中放置要做的工作列表。
放大:注释可以用来放大某种看来不合理之物的重要性
例如:
String listItemContent = match.group(3).trim();
//the trim is real important.It removes the starting
//spaces that could cause thr item to be recognized
//as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
公共API中的Javadoc:如果你在编写公共API,就该为它编写良好的Javadoc。就像其他注释一样,Javadoc也可能误导,不实用或者提供错误信息
大多数注释都属此类。通过,坏注释都是糟糕的代码的支撑或借口,或者对错误决策的修正。
5.5.1 喃喃自语
如果知识因为你觉得应该或者因为过程需要就添加注释,那就是无谓之举。如果要写注释,就要话必要的时间写出最好的注释。
5.5.2 多余的注释:
例如:class User {
//the name of user
private String name;
}
5.5.3 误导性注释
有时,尽管初衷可嘉,程序猿还是会写出不够精确的注释,最终导致误导了读者。
5.5.4 循规式注释
所谓每个函数都要有Javadoc或每个变量都要有注释的规矩是不可取的,这类注释徒然让代码变得散乱。
5.5.5 日志式注释
有人在每次编写代码时,在模块开始出添加一条注释,表示本次修改的原因和日期。很久之前没有代码控制仓库还可以理解,不过有了代码仓库之后,这种注释就完全没有必要了。
5.5.6 废话注释
有时,你会看到纯然是废话的注释。
例如:
/**
Default constructor
**/
protected User(){
}
5.5.7 位置标记
有时,程序猿喜欢在源代码中标记某个特别位置.
例如:
// Actions //////////////////////////////
5.5.8 括号后面的注释
有时,程序猿会在括号后面放置特殊的注释。尽管这对于含有深度嵌套的长函数可能有意义。不过,最好的方法应该是将长函数拆分成一个个短函数
5.5.9 归属与署名
代码仓库能够记住是谁在合适添加了什么,没必要用那些签名来搞脏代码。
5.5.10 注释掉的代码
直接把代码注释掉是讨厌的做法,不用了就直接删掉,有代码仓库帮我们保存,不用留着。
5.5.11 HTML注释
源代码注释中的HTML标记是一种错误的做法,在代码中应避免它。
5.5.12 信息过多
别在注释中添加有趣的历史性话题或无关的细节描述。
5.5.13 不明显的联系
注释及其描述的代码之间的联系应该显而易见。如果你要写注释,至少让读者能看着注释和代码,并且理解注释所谈何物
5.5.14 函数头
短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好
5.5.15 非公共代码中的Javadoc
虽然Javadoc对于公共API非常有用,但对于不打算作公公用途的代码就完全没有必要用Javadoc了。
代码格式很重要,必须严肃对待。或许你认为"代码能工作"才是专业开发着的头等大事。然而,事实并不是这样的,今天编写的功能,下一版可能被修改,原始代码修改之后很久,其代码风格和可读性仍会影响后续开发的人。即便代码不存在,你的风格和律条仍然存活下来。
大部分出色的系统中,单个文件长度差不多200行-500行左右,尽管这并非不可违背的原则,也应该接受。短文件比长文件易于理解。
6.2.1 向报纸学习
源文件要向报纸那样,名称应当简单且一目了然,源文件最顶部应该给出高层次的概念和算法,细节应该往下逐次展开,直至最底层的函数和细节。
6.2.2 概念间垂直方向上的区隔
代码都是从上往下,从左往右读。每组代码行展示一条完整的思路,这些思路用空白行区隔开。例如:封包声明,导入声明和每个函数之间,都由空白行隔开。
6.2.3 垂直方向上的靠近
如果说空白行隔开了概念,那么靠近的代码则暗示了它们之间的紧密关系,紧密相关的代码应该互相靠近。
例如:同一个概念的方法应该靠近在一起
6.2.4 垂直距离
对于那些关系密切、放置与同一源文件中的概念,它们之前的区隔应成为易懂性的衡量标准,应避免读者在源文件和类中跳来跳去。
1.变量声名:变量声明应尽可能靠近其使用位置
反例:
InputStream is = null;
//间隔了十多行代码
is = new FileInputStream("myFile");
2.实体变量:实体变量应该在类的顶部声明
3.相关函数:若某个函数调用了另外一个,就应该把它们放在一起,而且调用者应尽可能在被调用者上面。
4.概念相关:概念相关的代码应该放在一起,相关性越强,彼此之前的距离就该越短。
6.2.5 垂直顺序
我们想自上向下展示函数调用依赖顺序,被调用函数应该在执行调用函数的下面,这样就建立了一种自顶向下贯穿模块的良好信息流。
应该尽力保持代码行短小,一般来说,代码行的长度不要超过120个字符
6.3.1 水平方向上的区隔与靠近
我们使用空格字符将紧密相关的事物连接在一起,也用空格字符把相关性较弱的事物分隔开。
1.赋值:赋值语句的等号两边由空格隔开,因为赋值有两个确定的要素,左边和右边,空格字符加强了分隔效果。
例如: int lineSize = line.length();
2.函数名和参数:不在函数名和左圆括号之间加空格,因为函数与参数密切相关,如果隔开,就会显得没有关系
3.运算符:乘法因子之间不加空格,因为它们具有较高优先级。加减法之前用空格隔开,因为加减法优先级较低
6.3.2 缩进
大多数类声明,不缩进。类的方法相对该类缩进一个层级,方法的实现相对方法声明缩进一个层级。代码块的实现相对于其容器代码块索引一个层级,以此类推。不要在短小的if语句,while循环中违反缩进规则。
反例:public CommentWidget(ParentWidget parent, String text){super(parent, text);}
6.3.3 空范围
有时,while或for语句的语句体为空。我们很容易被while循环语句同一行末尾的分号欺骗,尽量不要这样使用。
反例: while(dis.read(buf, 0, readBufferSize) != -1);
每个程序猿都有自己喜欢的格式规则,但如果在一个团队工作,就是团队说了算。一组开发者应当认同一种格式风格,每个成员都应该采用那种规则。
对类的设计应该要隐藏具体的实现,隐藏实现关乎抽象。类并不是简单地用取值器和赋值器将其变量推向外间,而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
例如:
具象点:
public class Point{
public double x;
public double y;
}
抽象点:
public interface Point{
double getX();
double getY();
void setCartesian(double x, double y);
void setPolar(double r, double theta);
}
对象把数据隐藏于抽象之后,暴露操作数据的函数。而数据结构暴露其数据,不提供有意义的函数。它们其实是对立的。
对象和数据结构之间存在二分原理:过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。这其实就是一个抽象和具体的一个差异,对象其实是比较抽象的,而数据结构是具体的。
得墨忒耳律(The Law of Demeter)认为,模块不应了解它所操作对象的内部情形,对象隐藏数据,暴露操作。方法不应调用由任何函数返回的对象的方法,换言之,只跟朋友谈话,不与陌生人谈话。
反例:ctx.getOptions().getScratchDir();因为它调用了getOptions()返回值的getScratchDir()函数。
最为精炼的数据结构,是一个只有公共变量、没有函数的类,这种数据结构就是我们用的DTO。在数据库通信、解析传递的参数时,是最常用的。
对象暴露行为,隐藏数据。便于添加新对象类型而无需修改现有行为,同时也难以在既有对象中添加新行为。
数据结构暴露数据,没有明显的行为。便于向既有数据结构添加新行为,同时也难以向已有函数添加新数据结构。
如果我们在某些判断某些操作的执行情况时,手动返回操作成功或者失败。那么就会导致一个问题,我们必须在调用这个方法之后立即检查执行情况。不如直接在操作失败是直接抛出异常,直接调用时不抛异常,就表示操作是成功的
例如:
public Boolean handle(){
int x = operation();
return x > 0;
}
通过operation的返回值来判断是否操作成功,不如在operation内部操作失败时直接抛出异常。
public Boolean handle(){
operation();
return true;
}
异常的好处之一是,它们在程序中定义了一个范围。执行try部分的代码时,你是在表明可随时取消执行,并在catch语句中续接。在编写可能抛出异常的代码时,先写出try-catch-finally语句,这能帮你定义应该需要做什么,无论try代码块中执行的代码出什么错都一样。
当我们在方法里主动throw了一个异常之后,可能catch语句在这个方法的三个层级之上,这意味这每个调用该方法的函数都要修改,捕获新异常。以此类推,最终得到的就是一个从底层贯穿到顶层的修改链。
你抛出的每个异常,应创建信息充分的错误消息,和异常一起传递出去,包括失败的操作和失败类型。
按照8.1所说,使用异常而非返回码,这样的话代码会变得简洁,但是可能会出现下面这种情况。
try{
MealExpenses expenses = expenseReportDAO.getMeals(employee.getId());
total += expenses.getTotal;
}catch(MealExpensesNotFound e){
total += getMealPerDiem();
}
业务逻辑是,如果消耗了餐食,则计入总额。如果没有消耗,则员工得到当日餐食补贴。我们在判断是否消耗餐食时,通过抛出数据没有找到的异常来做逻辑判断,这其实是不合理的。应该在expenseReportDAO.getMeals()方法中,如果没有查询到对应的数据,就返回一个参数补贴的对象,而不是在异常里面去继续处理逻辑。
要讨论错误处理,就要提及到容易引发错误的做法,返回null值就是其中的一个。我们需要在每次调用都去判断是否为null,其实在增加我们的工作量,如果忘记检查,程序就会失控。不如直接抛出异常或者判处特例对象。
例如:
List
if(employees != null){
for(Employee e : employees){
System.out.println(e.getName());
}
}
其实,我们只要在getEmployees()方法中,如果结果为null时,返回一个空的List,就没有必要去判断是否为null了
如果将错误处理隔离看待,独立于主要逻辑之外,我们就能单独处理它,也极大提升了代码的可维护性和整洁性。
我知道了我要写好我的代码。