Clean Code第七章 错误处理 --阅读与讨论

尼克徐 发布于 2015年12月01日 | 更新于 2015年12月01日
无人欣赏。

alt text

图片来源:Error Handling - Clean Code

错误处理是编程必须要做的事情。

输入可能出现异常,设备可能失效。当错误发生时,程序员有责任确保代码照常工作。

错误处理部分,也应保持整洁的代码风格。

许多程序,到处是凌乱的错误处理代码。

错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。

一, 使用异常而非返回码

很久以前,许多语言不支持异常。这些语言处理和回报错误的手段都有限。

例如如下代码:

public class DeviceController { ...
    public void sendShutDown() { 
        DeviceHandle handle = getHandle(DEV1); // Check the state of the device
        if (handle != DeviceHandle.INVALID) {
            // Save the device status to the record field 
            retrieveDeviceRecord(handle);
            // If not suspended, shut down
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle); 
                clearDeviceWorkQueue(handle); 
                closeDevice(handle); 
            } else {
                logger.log("Device suspended. Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString()); 
        }
    }
... 
}

这类代码的问题在于,它们搞乱了调用代码,调用代码必须在调用后立即检查错误。

遇到错误时,最好抛出一个异常,这样的调用代码就可以保持整洁。

例如以下代码中,调用部分与错误处理部分截然分开,错误处理部分被隔离到Exception的子类DeviceShutDownError中处理了。

public class DeviceController { 
    ...
    public void sendShutDown() { 
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e); 
        }
    }
    private void tryToShutDown() throws DeviceShutDownError { 
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);
        pauseDevice(handle); 
        clearDeviceWorkQueue(handle); 
        closeDevice(handle);
    }
    private DeviceHandle getHandle(DeviceID id) { 
        ...
        throw new DeviceShutDownError("Invalid handle for: " + id.toString());
        ... 
    }
... 
}

二,写Try-Catch-Finally语句

写Try-Catch-Finally语句的好处是,Try语句中的代码块随时可以中断执行,并在Catch中继续。

在抛出异常时,应该提供足够的环境说明,以便判断错误的来源和处所。

应创建信息充分的错误信息,并和异常一起传递出去。

三,定义异常类

对错误分类有很多方式。可以依来源分类,可以依类型分类等等。最重要的考虑,是它们如何被捕获。

先看一个不太好的异常分类例子,这是使用第三方代码库中ACMEPort类,来创建对象:


    ACMEPort port = new ACMEPort(12);
    
    try { 
        port.open();
    } catch (DeviceResponseException e) { 
        reportPortError(e);
        logger.log("Device response exception", e);
    } catch (ATM1212UnlockedException e) { 
        reportPortError(e); 
        logger.log("Unlock exception", e);
    } catch (GMXError e) { 
        reportPortError(e);
        logger.log("Device response exception");
    } finally { 
        ...
    }

以上代码的问题主要在于,错误处理时包含了很多重复的代码。

其次,以上代码对第三方API(ACMEPort类)过于依赖,造成以后修改代码的麻烦。

可以修改如下:

LocalPort port = new LocalPort(12);

try { port.open(); } catch (PortDeviceFailure e) { reportError(e); logger.log(e.getMessage(), e); } finally { ... }

public class LocalPort { private ACMEPort innerPort; public LocalPort(int portNumber) { innerPort = new ACMEPort(portNumber); } public void open() { try { innerPort.open(); } catch (DeviceResponseException e) { throw new PortDeviceFailure(e); } catch (ATM1212UnlockedException e) { throw new PortDeviceFailure(e); } catch (GMXError e) { throw new PortDeviceFailure(e); } } ... }

以上代码中,LocalPort类是个简单的打包类,捕获并转换由ACMEPort抛出的异常。

类似于为ACMEPort定义的打包类非常有用,这可以降低对它的依赖,未来可以不太痛苦地改用其他代码库。

这里为port定义了一个异常类型,这也是一种“打包”,这样能便于写出整洁的代码。

四,重新定义流程,减少异常处理

我们看以下代码:

try { MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); mtotal += expenses.getTotal(); ... } catch(MealExpensesNotFound e) { mtotal += getMealPerDiem(); }

业务逻辑是,如果消耗了餐食,则计入总额中。如果没有(未产生MealExpenses对象),则抛出MealExpensesNotFound异常,员工得到了餐食补贴(m_total += getMealPerDiem())。

但是,此异常的抛出,带来的副作用是中断了try中其他代码的业务逻辑。如果能够消除这种异常处理,则代码看起来会更整洁。

因此,我们可以修改ExpenseReportDAO类,使其总是返回MealExpense对象。如果没有餐食消耗,则返回一个餐食补贴的MealExpense子类对象:


public class PerDiemMealExpenses implements MealExpenses { 
    public int getTotal() {
        // return the per diem default 
    }
}

此时代码变为:

MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); 
m_total += expenses.getTotal();
异常类被取消,代码看起来很简洁。

这种手法叫特例模式(Special Case Pattern)创建一个类或者配置一个对象,用来处理特例。

处理了特例,客户代码就不用应付异常行为了。异常行为被封装到特例对象中。

五, 别返回和传递null值

要讨论错误处理,就一定要讨论容易引发错误的做法。

最容易引发错误的做法,就是返回null值了。

例如以下代码,里面到处是测试是否返回null值的if语句,基本上就是给自己增加工作量,给调用者添乱。

只要有一处没有检查null值,应用程序就会失控。

例如下面代码中,没有检查peristentStore对象是否存在,那么执行时,会不会出现异常,是否捕获了这个异常?

public void registerItem(Item item) { 
    if (item != null) {
        ItemRegistry registry = peristentStore.getItemRegistry(); 
        if (registry != null) {
            Item existing = registry.getItem(item.getID());
            if (existing.getBillingPeriod().hasRetailOwner()) {
                existing.register(item); 
            }
        }
    }
}

与其在方法中返回null值,不如抛出异常,或者返回特例对象。

例如下面的代码,

List employees = getEmployees(); 
if (employees != null) {
    for(Employee e : employees) { 
        totalPay += e.getPay();
    } 
}
此时,getEmployees可能返回null,但是是否一定要这么做呢?

如果修改getEmpolyees,使其返回空列表,则代码变得整洁多了:

List employees = getEmployees(); 
for(Employee e : employees) {
    totalPay += e.getPay(); 
}
问题:

1, 你公司是如何做错误处理的?有没有统一的办法?

2, 对null值的处理上,你是否同意本文的观点?你的看法是什么?

如何加入Root Cause读书会:请加我的微信brainstudio,我建立了一个群,方便通知各位写读后感及讨论。

Clean Code所有讨论章节链接

共2条回复
madwenoma 回复于 2015年12月01日

null确实不好处理啊 什么空对象模式实用性都比较差。。。

cnsoft 回复于 2015年12月01日 | 更新于 2015年12月01日

有时候我们就要抛异常. 而不去try... catch . 滥用try 也是邪恶的. 待更新...

登录 或者 注册