【随笔·技术】重构错误处理程式

原网页


有人研究过,程式中可能会有高达90%的比率在管理与处理错误,Bob大叔在《Clean Code》中谈到「许多程式码[1]完全由错误处理所主宰」,90%的比率是真的在管理与处理错误的逻辑吗?还是只是如Bob大叔说的,根本就是散乱的错误处理程式码?商务逻辑相关的程式码需要重构,对于错误处理程式码的重构,我们也有许多需要学习的地方。

错误处理就是一件事

在重构或程式码可读性的概念中有个共同特性,就是函式(方法)应该只做一件事,避免函式中的程式码陷入逻辑泥块(Logical clump)。在没有例外处理的语言中,透过回传错误码,让函式客户端可以检查执行结果,确认后续是要进行正常或错误处理流程,如果客户端必须呼叫多个函式来完成一项任务,检查错误码、正常与多个错误处理流程夹杂的情况下,容易使得客户端程式码变得混乱。

例外[2]处理机制可以在错误发生的时候抛出例外,让错误处理能推到想要的边界进行处理。以Java来说,客户端可以在try区块中处理正常流程,在catch区块处理呼叫各函式时可能抛出的例外,让原先纠缠在错误处理流程中的正常流程清楚地呈现出来,try区块中的流程亦可抽取为函式独立地做一件事,那么目前的try-catch就能专心地做错误处理这件事,如同Bob大叔说的「函式应该只做一件事,而错误处理就是一件事」。

有时例外处理流程会形成一种模式,例如涉及资源建立、使用与关闭的操作若会抛出例外,为了有限资源在各种错误发生时都能确实释放,不免要撰写类似的try-catch-finally流程,在具有受检例外[3]的Java中更是难以避免这类情况,像是JDBC的处理流程就是如此,此时可以采用样版回呼(Template callback)模式,适当地让资源相关操作从错误处理流程中独立出来,Spring的JdbcTemplate就是这类实现,因为这类资源建立、关闭的操作模式太频繁出现,JDK7就提出了try-with-resources语法来解决这类需求,确实地让资源建立、使用与关闭的操作与错误处理分离,若进一步地结合JDK8的Lambda语法,还可让资源的使用从建立与关闭中分离。例如设计一个open方法,就可以专心在FileInputStream的使用,让开启档案的意图显而易见:

open(fileName, fileInputStream -> {
    // 操作FileInputStream实例
});

多个捕捉做相同处理时的重构

如果多种例外捕捉后,做的都是相同的错误处理,像是日志,或者是将程式库的例外封装为自定义例外等,错误处理的程式码必然就出现重复,自然就会呈现需要重构的讯号。因为多种例外做的都是相同的事,可将有继承关系的例外处理程式码,合并在父类别的捕捉区块中,但不建议使用catch-all的方式,例如使用ExceptionThrowable来捕捉所有例外,因为对于其他不相关的例外,这是一种隐藏错误的做法。

然而在合并有继承关系的例外处理程式码之后,仍会发现没有继承关系的例外处理程式码出现重复,Bob大叔在《Clean Code》中提出的作法是包裹呼叫的API,确保它在捕捉各种例外后,能转换为(自定义的)共同例外型态,如此客户端就只需要捕捉一种例外,因而可让客户端程式码大幅简化,如果使用的是第三方API,也可以同时降低了对它的依赖。

如果多种例外在捕捉之后,做完相同处理就将原例外重新抛出,可以参考guava-libraries的作法,你可以使用catch-all的方式捕捉各种型态的例外,做完相同错误处理之后,使用Throwables.propagateIfInstanceOf以指定的例外型态重新抛出(通常是受检例外),或者是使用Throwables.propagate,将原例外以RuntimeException包裹后重新抛出,既消除了重复的错误处理程式码,又避免了隐藏错误。

虽然实际上,Throwables.propagateIfInstanceOf只是将型态判断与转型的逻辑封装并予以重用,但对客户端程式码的简化确实有所帮助,不过,这种方式对于错误处理时进行例外型态转换,或者是不重新抛出的情况并不适用,guava-libraries的〈ThrowablesExplained〉文中也解释了其他一些不适用的场合。JDK7中,对于多个捕捉做同一件事的情况,提出了Multi-catch语法,算是为这问题提出了较好的解决方案。

多个捕捉做不同处理时的重构

如果多种例外捕捉后,分别进行不同的错误处理,此时得检视多种例外是由单一方法抛出,或多个方法操作而分别抛出不同例外,最常见的情况是一个try区块进行了数个会抛出例外的操作,然后底下连续多个catch区块逐一针对不同例外作处理。实际上每个会抛出例外的方法发生错误时,理由应该是各不相同的,应试着让这些方法各有一个try-catch区块,让每个方法的错误处理流程各自显露出来。

一旦你根据不同方法引发的例外,将一个try搭配多个catch的程式码,分解为数个try-catch区块之后,应当立即想到「错误处理就是一件事」,而两个以上的try-catch时,无论那些try-catch是形成巢状或者是瀑布式流程,都意谓着你的程式码做了两件以上的事,重构的方式之一,就是每个try-catch重构至独立的方法之中,让每个方法都只会出一个try陈述。

当发现一个方法中会出现多个try-catch时,而每个try-catch都做类似模式(但细节不同)的转换或错误处理时,如果你接触过函数式的错误处理风格,例如我先前专栏〈函数式风格错误处理〉中谈过的OptionEitherTry等概念,就有可能进行Monad风格的错误处理,我在专栏〈神秘的Monad不神秘〉中谈到OptionalflatMap可连续处理null与物件值转换的问题,实际上,Mario Fusco在〈Monadic Java〉中就以类似风格,设计了Validation等类别,可以用Monad风格对使用者进行如下的程式码验证与验证失败讯息之收集,而又不会迷失在瀑布式的ValidationException捕捉程式码之中:

Validation<List<String>, Person> validation = success(person)
    .failList()
    .flatMap(Validator::validAge)
    .flatMap(Validator::validName);

重构是看待错误处理的一个角度

既然程式中可能会有高达90%的比率在管理与处理错误,我们真的该认真且从不同角度去看待,像是受检或非受检例外的运用、例外应捕捉或抛出、避免隐藏错误、换个典范风格思考错误处理的可能性等,都该有所思考,我的专栏〈Shit Happens!该抓还是该丢?〉、〈避免隐藏错误的防御性设计〉与〈函数式风格错误处理〉都曾做过一些探讨。

从重构角度出发来看待错误处理程式码,你会发现Martin Fowler的《Refactoring》中揭露的重构原则,对待错误处理程式码也是适用的,错误处理之所以重要,就在于它是处理不对的事情,本身必须正确,然而就如Bob大叔说的「如果它糢糊了原本程式码的逻辑,那就不对了」


  1. 代码

  2. 异常

  3. Checked Exception

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,537评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,279评论 25 707
  • 不管使用的是哪种语言进行程序设计,都会产生各种各样的错误。Java提供了强大的异常处理机制。在Java中所有的异常...
    残月雨纷纷阅读 1,238评论 0 2
  • 在这个竞争激烈的时代,毫无疑问现在很多公司都在想着如何去完善自己的品牌,让自己的品牌发声,让品牌成为产品的代言词,...
    51运营阅读 342评论 0 1
  • 我住院的第三天,苗天华他们三个都来到了医院看我,他们的神情严肃。眼中充满了凝重之色。 “我说,你们一个个这么严肃,...
    浮生万梦星耀烛天阅读 133评论 0 1