[学习笔记] 《代码整洁之道》— 第7章 错误处理
错误处理很重要,但如果它搞乱了代码逻辑,就是错误的做法。
使用异常而非返回码
-
很久以前,许多语言都不支持异常。这些语言处理和汇报错误的手段有限。
- 要么设置一个错误标识,
- 要么返回给调用着检查的错误码。
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. Uable to shut down"); } }else{ logger.log("Invalid handle for: " + DEV1.toString()); } } ... }
- 这类手段搞乱了调用者代码。调用者必须在调用之后即刻检查错误。
- 很不幸,这个步骤很容易遗忘。
- 遇到错误,最好抛出一个异常。
- 调用代码很整洁,其逻辑不会被错误处理搞乱。
- 采用异常处理的代码:
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语句
缺少基础知识储备,无法理解!留后再学习!
使用不可控异常
给出异常发生的环境说明
- 你抛出的每一个异常,都应当提供足够的环境说明,以便判断错误的来源和处所。
- 应创建信息充分的错误消息,并和异常一起传递出去。
- 包括失败的操作和失败类型
- 应创建信息充分的错误消息,并和异常一起传递出去。
依调用者需要定义异常类
-
对错误分类有很多方式
- 来源分类
- 类型分类
-
在应用程序中定义异常类时,最重要的考虑应该该是它们如何被捕获。
ACMEPort port = new ACMEPort(12); try{ port.open(); }catch(DeviceResponseException e){ reportProtError(e); logger.log("Device response exception", e); }catch(ATM1212UnclockedException e){ reportProtError(e); logger.log("Unlock exception", e); }catch(GMXError e){ reportProtError(e); logger.log("Device response exception"); }finally{ ... }
-
本例中,既然知道我们所做的事不外如此,就可以通过打包调用 API、确保他返回通用异常类型,从而简化代码。
LocalPort port = new LocalPort(12); try{ port.open(); }catch(PortDeviceFailure e){ reportProtError(e); logger.log(e.getMessage(), e); }finally{ ... }
-
LocalPort 就是个简单的打包类,捕获并翻译由 ACMEPort 类抛出的异常。
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(ATM1212UnclockedException e){ throw new PortDeviceFailure(e); }catch(GMXError e){ throw new PortDeviceFailure(e); } } ... }
-
-
-
对于代码的某个特定区域,单一异常类通常可行。伴随异常发送出来的信息能够区分不同错误。
定义常规流程
-
如果在业务逻辑和错误处理之间有良好的区隔,大量代码会爱事变的像是整洁而简朴的算法。然而,这样做却把错误检测推到了程序的边缘地带。
-
某记账应用的开支总计模块:业务逻辑是,如果消耗了餐食,则计入总额中。如果没有消耗,则员工得到当日餐食补帖。
try{ MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal(); }catch(MealExpensesNoFound e){ m_total += getMealPerDiem(); }
异常打断了业务逻辑。如果不去处理特殊情况,代码看起来更简洁。如下:
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotal();
修改 ExpenseReportDAO ,使其总返回 MealExpenses 对象。如果没有餐食消耗,就返回一个返回餐食补贴的 MealExpenses 对象。
public class PerDiemMealExpenses implements MealExpenses{ public int getTotal(){ // return the per diem default } }
- 特例模式(Special Case Pattern)
- 创建一个类活配置一个对象,用来处理特例。
- 特例模式(Special Case Pattern)
-
null 值
- 别返回 null 值
- 别传递 null 值
- 在方法中返回 null 值是糟糕的做法,但将 null 值传递给其他方法就更糟糕了。
- 除非 API 要求你向它传递 null 值,否则就要尽可能的避免传递 null 值。
小结
- 整洁代码是可读的,但也要强固。可读和强固并不冲突。
- 将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。
参考文献
[1] Robert C. Martin 著,韩磊 译,《代码整洁之道》,北京:人民邮电出版社,2010.1(2018.9 重印), ISBN 978-7-115-21687-8。