读《代码整洁之道》

一、有意义的命名

命名的原则就是

1、名副其实

其实很简单,就是命名上就可以告诉代码的阅读者它为什么存在,它做什么事情,应该怎么用。书中还补充了一点,如果名称需要注释来补充,那就不算名副其实。但我觉得这一点有点儿绝对了。不过用能代表其存在意义的名字来命名是很重要的。

例如

int d; //消逝的时间
int daysSinceCreation;]]></ac:plain-text-body>

上面两个,明显第一个就有问题,因为离开了定义的地方,后面谁也看不懂这个“d”是干什么用的

2、避免误导

命名上要避免使用会或者可能会掩藏代码本意的错误线索。

例如

1)不要用一些专有名词的缩写来代表自己的变量(我觉得除非项目约定好的,否则就不要用任何缩写。当然,一些广泛使用的除外,例如 URL )

2)不要把类型名加在变量当中,即便其就是这个类型。例如用accountList来代表一组帐号列表就是不好的,直接用accounts或者accountGroup都不错(想起自己经常会使用类似于XXXArray…..以后会改正)

3)不要出现命名差异很小,命名又长的情况,例如XYZControllerForEfficientHandlingOfStrings与XYZControllerForEfficientStorageOfStrings这样….乍一看谁能看出来这两个是不同的变量?!(我觉得命名这么又臭又长的很不好啊….虽说有代码补全并不会浪费时间,但是读这个代码像看个句子一样很累啊…命名还是要短小精悍的好)

4)不要用小写l或者大写O之类的来命名,很容易和数字1和0搞混

3、做有意义的区分

因为同一个作用范围内不能出现两个同名的东西,所以在一系列的代码上做区分的时候要有一定意义,不然让人意义不明

1)以数字后缀来区分的行为就很不成熟…..a1,a2,a3…这样的区分谁知道哪个是干嘛的?

public static void copyChars(char a1[], char a2[]){
    for (int i = 0; i < a1.length(); i++) {
        a2[i] = a1[i];
    }
}

比如上面这种乍一看就有点意义不明,比如把a1,a2改成 sourceChars 和 destinationChars 就好很多

2)区分的时候不要用什么同义词什么的…例如ProductInfoProductData

还有什么类似于

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

以上三个就很奇怪好么?

4、使用读得出来的名称

命名的时候要用能够轻易读出来的名称,书上举了一个极端的例子

genymdhms(但是我觉得这样的变量命名简直丧心病狂,不止违背这一条吧)

它的实际意义是生成日期,格式为年月日时分秒

generate year month day hour minute seconds

改成

generateTimestamp就一目了然了

5、使用可搜索的名称

命名不要太短太常见,或者不要有很长的统一的前缀,不然就全局搜索某个名称的时候就会发现结果太多而失去搜索的意义。

6、避免使用编码

之前看过同学做windows编程的时候,命名方式就是匈牙利命名法,完全意义不明好么?反正记着用驼峰法命名就好了…

7、避免思维映射

很多情况下的命名不要与大众背道而驰吧,例如大多数人都习惯用单字母i, j, k来作为循环的索引,就不要用什么其它奇怪的单字母了(我觉得用index,idx 之类的也无伤大雅吧…),但是注意,除了这种情况,其它大多数情况一般都不用单字母来命名的,至于为什么请见第五条!

总结为五个字:明确是王道

8、类名,方法名

类名的对象一般命名是名词或者名词短语:Customer, WikiPage, Account, AddressParser, PageDao之类

方法名应该是动词或者动词短语:postPayment, deletePage, insertAccount, saveChange之类的,以及Java中的标准,在变量的setter 和 getter前面加上 set 和 get,或者is来标识

9、其它

后面的几条感觉或多或少都和前面重复了,反正命名要短小精悍,意义明确,取词规范,符合大众标准

二、函数

函数是非常重要的部分,程序中充斥着函数,所以函数的命名与实现的规范也是至关重要滴

1、短小

函数的第一条规则就是短小,因为因为函数的第二条规则,函数逻辑要简单明了,如果写得太冗长的话,可读性就很差,也很容易出错,出错了也不容易定位错位在何处。如果一个功能不得不写得很长,就尽量去拆分为不同的函数。作者提出了,函数二十行封顶为最佳。

2、只做一件事

要判断函数是否不止做了一件事,有一个方法是判断其是否能再拆出一个函数。

3、每个函数一个抽象层级

要确保函数只做一件事,应该将函数的语句都在同一抽象层上。应该让代码拥有自顶向下的阅读顺序:我们要让每个函数后面都跟着位于下一抽象层的函数。这样一来,在查看函数列表时,就能按照抽象层级向下阅读。

这里的抽象层级我的理解是,比如此函数是用于组织一系列小功能来实现大功能的话,那么前面的代码所调用的子函数在逻辑上位于后面的代码,保证其一个流程性的感觉吧。

4、switch语句

switch语句由于其得天独厚的原因,要写短小的switch语句就很困难,因为它天生就要做很多并列的事情。

比如这样一段代码

public Money calculatePay(Employee e)
    throws InvalidEmployeeType
{
    switch(e.getType())
    {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.getType());
    }
}

上面这段代码就有很多问题,首先,它比较长了,其次,如果日后有了新的雇员类型进来之后,就必须改动这里的代码,还会变得更长,很容易出错。总的来说,它违背了单一权责问题(Single Responsibility Principle, SRP)以及开放闭合原则(Open Closed Principle, OCP)

书中给出了一个解决方案,用抽象工厂的方式,把Switch语句放到抽象工厂下,不让任何人看见,该工厂使用Switch语句来为Employee的派生类创建实体,而不同实体的各种类似的方法可以统一用Employee接口来多态的实现。

Talk is Cheap, Show me the code

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 EmployeeFactoryImp implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType
    {
        switch(r.getType())
        {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvalidEmployeeType(r.getType());
        }
    }
}

5、描述性的名称

这一点在前面第二章命名当中已经提到

6、函数参数

最理想的参数数量是0,其次为1,尽量避免3及以上的参数。参数传递中<span style="color: rgb(255,0,0);">不应该传入布尔值</span>的参数,传入布尔值的参数,说明这个函数做了不止一件事情。如果标识为true会这样做,如果标识为false会那样做。对于这种情况我们应该把函数拆成两个函数。

例如

render(true);

函数的调用者就会不明所以,不知道这个true到底是干嘛的。

虽然说函数的申明可以写成

public void render(Boolean isSuite)]]></ac:plain-text-body>

这样看起来稍微好一些,

但是遇到布尔值,还是写成两个函数好一些

public void renderForSuite();
public void renderForSingleTest();

其次

若需要3个或3个以上的参数,最好将其封装成一个类。例如:

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double);

7、无副作用

什么是副作用?就是说函数的调用一次对全局的状态有影响。例如改变了某个全局变量,初始化了某个类在函数结束的时候没有消除掉等等。对函数的实现尽量保持无副作用的情况,如果必须产生副作用,则在函数的名称中体现出来

8、分割指令与询问

函数要么做什么事,要么回答什么事,二者不可都做,两样都做将会引起混乱,例如:

public boolean setValue(String key, Object value);]]></ac:plain-text-body>

上面这个函数,把键为key的值设置为value,成功返回true,失败返回false。但是失败的原因多种多样,可能key不存在,可能设置不成功。这样就造成了意义不明


9、使用异常替代返回错误码

若返回错误码就要求调用者立即进行如下错误处理,这样会导致整个代码结构混乱,大量篇幅在进行错误码的判断:

if (deletePage(page) == E_OK) {
    if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makekey()) == E_OK) {
            log.error("page deleted");
        }
        log.error("configKey not deleted");
    }
    log.error("deleteReference from registry failed");
} else {
    log.error("delete failed");
    return E_ERROR;
}
//--------------------以上为错误码方式--------------------

try{
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makekey());
} catch (Exception e) {
    log.error(e.getMessage);
}
//--------------------以上为异常方式--------------------

异常方式就显得简单得多

而且,应该把try/catch的逻辑单独抽离出来,内部的实现单独使用一个函数来包装,就会显得好很多。

public void delete(Page page) {//只与错误处理相关
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {//只与完全删除一个page相关
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makekey());
}

private void logError(Exception e) {
    log.error(e.getMessage());
}

错误处理不应该与其他事情混合到一起来做。