如何优雅地进行错误处理(clean code阅读笔记之六)

程序出了问题怎么办

注:正文中的引用是直接引用作者的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。


错误处理是十分必要的,但是如果对错误处理使用不当则会让代码变得十分臃肿,让阅读者看不清代码的逻辑,更严重的是,这也会让程序变得十分脆弱。本文中将列出一些使用错误处理的技巧,帮助你写出既简洁又健壮的代码。

使用Exception而不是返回码

返回码是一个历史遗留问题,在以前的没有Exception的语言(比如c语言)中,它是有效且必要的,但是在有Exception的语言中使用返回码是没有任何益处的。

对于使用返回码的函数,调用者在得到调用结果(这里是返回码)之后要立即去验证返回码,这对于代码的可读性和结构的合理性都是极大的挑战,使用「异常处理」能让业务逻辑和错误处理在代码结构上分离,代码的结构和逻辑会更清晰。

首先把try-catch-finally块写出来

当遇到需要做异常处理的时候,首先把try-catch-finally块写出来,这能帮助你写出更好的异常处理代码。

永远使用unchecked异常


这又是一个非常有争议性的话题,到底是使用checked异常,还是unchecked异常。

_checked异常(通常是Exception的子类)是Java中的一个概念,它是指如果一个方法抛出此异常,那么它就必须被显式地捕获或者传递,通常是在方法上添加声明。

unchecked异常指的是可能随时抛出的异常(通常是RuntimeExceptionError,和他们的子类)。

推荐阅读:Unchecked Exceptions — The Controversy,在这篇Oracle的官方指南中讲到:如果你要从某个异常中恢复,那么就使用checked异常,否则使用unchecked异常,意思就是unchecked的异常表示系统出问题了,那么必须停下来,去检查到底什么问题,然后解决。_


作者的观点是:如果在某些特殊的情况下必须要捕获异常并作出处理,那么不得不使用checked异常。但是在通常的开发过程中应当避免使用checked异常。

checked异常的使用确实带来了一些好处,但是也同时也造成了一些问题,最重要的是它违反了「开闭」原则。比如如果一个方法抛出某个checked异常,而这个异常的处理(通常是catch)是在3个方法调用层级之上,那么当你修改最底层这个方法的抛出异常时,所有在这个调用链中的方法签名都需要被修改。这明显违反了「对修改关闭」的原则,也违反了面向对象中「封装内部实现」的特性。

提供异常的上下文

在异常中一定要提供可供这个异常的捕获者(通常是在catch中)用来分辨这个异常的上下文信息,使之可以通过log或其他方式最终呈现出来。

按照调用者的需求定义异常类

当我们定义一个异常类时,可以按照这个异常类所属的模块,抑或是这个异常类的类型来定义,但是当我们在程序中定义一个异常类时,更多的则应该是在考虑「这个异常会在什么情况下被捕获」。代码6-1中展示了普通的按照类型定义的异常类,

//代码6-1
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 {...
}

代码6-2中展示了使用一个包装类定义的一个新的异常类,不仅代码看起来更加清晰简单,而且还会带来更多其他的好处。

//代码6-2
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);
        }
    }
    ...
}

像代码6-2中那样对于第三方的类库中的API进行封装会带来很多好处。实际上,封装第三方API可以算是一种最佳实践。

当你在你的项目中对第三方类库进行了自己的封装之后,相当于解耦了你的项目和这个第三方类库,你可以轻易的将这个类库更换掉,更重要的是,你可以不需要一定遵循这个类库的设计来使用它,比如代码6-2中所示的,本来类库中定义了多个重合

定义常规流程

虽然我们可以写出很好的错误处理代码,它们外形优雅、结构清晰,但是有时候我们并不是想要「终止代码的逻辑流程」而抛出异常,这种情况就不要使用异常类来处理,应该使用更常规的流程来处理,比如代码6-3中异常处理。

//代码6-3
try {
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}

代码6-3中的错误处理只是为了处理一种特殊状况(可能是数据库中找不到此次吃饭的消费情况),而不是要中止计算而抛出异常,那么它可以被重构为代码6-4中的样子。

//代码6-4
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

//这里引入一个新的特殊情况的类
public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
        // return the per diem default
    }
}

这种编程模式被叫做特殊情况模式,它使用一个新的类来处理这种特殊情况,那么就不需要抛出异常类,因为在异常的情况下也会返回一个MealExpenses对象,那么整个流程看起来就和普通的业务流程一样了。

不要返回Null

Null是邪恶了,不要在代码中返回Null

如果你的代码中有返回Null的情况,那么在代码的任何地方都可能抛出NullPointerException而导致系统崩溃退出,你的代码中也可能会存在一大堆的判断「一个对象是否为Null」的代码,这可能是所有开发者都不愿意看到的情况,可以使用代码6-4中所使用的特殊情况模式来避免返回Null,永远返回一个有值的对象。

不要传递Null

在函数调用的参数中传递Null是比返回Null更邪恶的事情。所以在你自己的代码中,永远不要传递Null

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,884评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,212评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,351评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,412评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,438评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,127评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,714评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,636评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,173评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,264评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,402评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,073评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,763评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,253评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,382评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,749评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,403评论 2 358

推荐阅读更多精彩内容