图片来源: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()); m_total += expenses.getTotal(); ... } catch(MealExpensesNotFound e) { m_total += 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值,不如抛出异常,或者返回特例对象。
例如下面的代码,
Listemployees = getEmployees(); if (employees != null) { for(Employee e : employees) { totalPay += e.getPay(); } }
此时,getEmployees可能返回null,但是是否一定要这么做呢?
如果修改getEmpolyees,使其返回空列表,则代码变得整洁多了:
Listemployees = getEmployees(); for(Employee e : employees) { totalPay += e.getPay(); }
问题:
1, 你公司是如何做错误处理的?有没有统一的办法?
2, 对null值的处理上,你是否同意本文的观点?你的看法是什么?
如何加入Root Cause读书会:请加我的微信brainstudio,我建立了一个群,方便通知各位写读后感及讨论。