预防错误的设计
上周参加了一个 Michael Feathers 的 workshop,整个 workshop 内容非常多,其中大部分是围绕他那本著名的《修改代码的艺术》所阐述,不过除此之外 Michael 还以 Beyond Error Handling, Using Design to Prevent Errors 为主题,讲了不少如何优雅地处理错误的做法和思路。其中的一个例子涉及并融合了面向对象和函数式的相关知识,引发我的一些思考,本文就此做一些总结。
例子的代码很典型也很简单,简化后,主要涉及三个类,分别是 Inventory, Item, 和 Sale,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
publicclassInventory{
privateHashMap<String,Item>items=newHashMap<String,Item>();
publicInventory(){
items.put("1",newItem("Preserved Duck Eggs",150000));
items.put("2",newItem("Milk",7000);
items.put("3",newItem("Tomato",5500));
}
publicItem itemForBarcode(Stringbarcode){
returnitems.get(barcode);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
publicclassItem{
privateStringname;
privateintprice;
publicItem(Stringname,intprice){
this.name=name;
this.price=price;
}
publicStringgetName(){
returnname;
}
publicintgetPrice(){
returnprice;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
publicclassSale{
privateSaleEventListener listener;
privateInventory inventory=newInventory();
privateArrayList<Item>items=newArrayList<Item>();
publicSale(SaleEventListener listener){
this.listener=listener;
}
publicvoidaddBarcode(Stringbarcode){
Item item=inventory.itemForBarcode(barcode);
items.add(item);
listener.itemAdded(item);
}
publicvoidsubtotal(){
Money sum=newMoney();
for(Item item:items){
sum=sum.add(item.getPrice(items));
}
listener.subtotaled(sum);
}
}
|
想象我们去超市购物,然后到收银台结账,当我们把一车东西交给收银员的时候,她会对每件东西(Item)逐一扫描条形码(barcode),扫描条形码的时候系统会去库存(Inventory)中查询这件东西的名称和价格,扫描完成后,收银员会计算总价格(subtotal)。上述代码所表述的就是这个场景。
读者不妨花5分钟理解一下上述代码,接着考虑一个问题,如果扫描的条形码在库存中不存在怎么办?具体来说 inventory.itemForBarcode(barcode); 返回 null 怎么办?下面的所有讨论都围绕这个问题展开。
null 判断
如果我们随意把 null 传来传去,什么都不干,那必然早晚会出现 NullPointerException ,那么最简单的做法是加上 null 判断以保证错误的 null 不会再被传出去:
1
2
3
4
5
6
7
8
9
|
publicvoidaddBarcode(Stringbarcode){
Item item=inventory.itemForBarcode(barcode);
if(item==null){
return;
}
items.add(item);
listener.itemAdded(item);
}
|
这可能是我们能见到的最常见的做法,代码中到处充满了 null 判断,在某种程度上把代码主要想表达的主干逻辑给污染了,过多 null 判断影响了代码的可读性。如果所有使用变量的地方都要去检查 null,那代码必然会恶心的不行,实际中我们不会这么干,因为变量的值什么来的,很多时候也是由我们自身控制的。但如果我们在使用第三方的代码,那为了保护自己,检查 null 则是比较明智的选择了。因此,在自己的代码库内部,尽量避免传 null 并去掉 null 判断;而在集成第三方代码的时候,则多防御性地使用 null 判断。
如果我们设计一个 API,那显式地告诉别人是否可以传入 null,是否会返回 null,则是很好的习惯,例如:
1
2
3
|
@Nullable
publicItem itemForBarcode(@Notnull Stringbarcode)
|
上述代码表示传入的 barcode 不能为 null(如果是 null,不保证会发生什么后果),而返回的 Item 则可能是 null,这样实现的时候我就不需要去判断 barcode 是否为 null,另外用这个接口的人也明确知道自己要检查返回是否为 null。@Nullable 和 @Notnull 都已经在 jsr-305 中明确定义。
异常
避免 null 判断的一种最直接做法就是抛出异常,例如:
1
2
3
4
5
6
7
8
|
publicvoiditemForBarcode(Stringbarcode)throwsItemNotFoundException{
if(items.containsKey(barcode)){
returnitems.get(barcode);
}else{
thrownewItemNotFoundException();
}
}
|
遇到 ItemNotFoundException 怎么办?继续往上抛?处理掉?记个日志然后吃掉?在这个上下文中,我无法给出明确的答案,具体选择哪种方式还得看具体场景,通常来说,**先想想当你遇个异常的时候,你的用户希望得到怎样的结果?然后基于这点来指导你的异常处理方式。**
异常在很多情况下是很适用的,例如磁盘坏了,那抛出 IOException 让系统 Fail Fast 往往是一种很好的处理方式,然后异常也有不少问题,首先 Java 中的 Checked Exception 很容易让 API 变得丑陋无比;其次异常的跳跃式的抛来抛去,也让有 goto 的感觉,很难 follow 。
有没有更好的办法保持 API 的简洁,同时也能避免 null 判断呢?
Null Object 模式
在面向对象设计中,我们常常可以使用 Null Object Pattern 来去掉 null 检查逻辑或者异常捕捉,该例中,我们可以创建一个 NotFoundItem ,它继承自 Item,不过 name 和 price 比较特殊:
1
2
3
4
5
6
7
|
publicclassNotFoundItemextendsItem{
publicNotFoundItem(){
super("Item not found",0);
}
}
|
然后再 Invetory 中适当地返回 NotFoundItem:
1
2
3
4
5
6
7
8
|
publicvoiditemForBarcode(Stringbarcode){
if(items.containsKey(barcode)){
returnitems.get(barcode);
}else{
returnnewNotFoundItem();
}
}
|
这样,所有使用 Inventory.itemForBarcode() 的地方都不需要特殊的错误处理了,例如在 Sale 类中,addBarcode() 和 subtotal() 都能正常工作。想象一下,如果有五个、十个、或者更多的类使用 Inventory.itemForBarcode(),这样可以简化多少代码!因此,如果有可能,尽量在下层完成错误处理,因为越往上层,需要的错误处理代码往往越多。这实际上是契合 Robustness Principle 的,这条原则是这么说的:
Code that sends commands or data to other machines (or to other programs on the same machine) should conform completely to the specifications, but code that receives input should accept non-conformant input as long as the meaning is clear.
Inventory.itemForBarcode() 能够接受不合法的 barcode ,但是它返回的 Item 是符合接口定义的,没有特殊性。
到目前为止一切似乎看起来很美好,但实际上 Null Object 模式不是完美的,想象一下,如果我们要在 Sale 类中加入这样一个逻辑:如果购买物品的数量达到了10,则有5%的折扣。显然 NotFoundItem 会破坏这样的逻辑,扫描1个合法 barcode 加9个不合法的 barcode 也会造成 5% 折扣。
Option
我们花了大量的精力对付 null,事实上 null 这个概念的发明人 Tony Hoare 也说:
I call it my billion-dollar mistake.
是否有其他更好的方案来解决这个问题呢?让我们来到函数式的世界,看看 Scala 是怎么对付 null 的。Scala 内置了一个特殊类型,叫做 Option,顾名思义,可以认为 Option 可能包含一个值,也可能不包含,例如我们可以修改 Inventory.itemForBarcode() 如下:
1
2
3
4
5
|
def itemForBarcode(barcode:String):Option[Item]={
if(items.contains(barcode))Some(items(barcode))
elseNone
}
|
这一段 Scala 代码也比较好理解,*itemForBarcode* 接收一个 String 参数,返回 Option[Item],而*Option[Item]* 有两种子类型,一种是 Some[Item],表示有实际的 Item,另外一种是 None,表示没有。
现在 Sale.addBarcode() 是这么处理 Option[Item] 的:
1
2
3
4
5
6
7
|
def addBarcode(barcode:String){
inventory.itemForBarcode(barcode).foreach(item=>{
items:+item
listener.itemAdded(item)
})
}
|
代码中对 Option[Item] 进行了迭代访问,与迭代一个集合的做法完全一样,这么做的优雅之处在于,如果 Option[Item]是 None,迭代中的逻辑不会被执行,也不会有任何副作用,与我们迭代一个空的集合一样。当然,如果 Option[Item]是 Some[Item],则 item 会被取出来并执行相应的逻辑。因此我们可以简化地把 Option 认为是一个包含1个或者0个元素的集合类。
事实上 Scala 的 Library 中大量使用了 Option,例如,由于 Scala 中的 Map 实际上有方法返回 Option,因此 Inventory.itemForBarcode() 可以简化成:
1
2
3
4
|
def itemForBarcode(barcode:String):Option[Item]={
items.get(barcode)
}
|
现在,*Inventory.itemForBarcode()* 接口的协议是:我会返回一个 Option[Item],您自己去迭代里面的内容。有没有可能把接口简化下?让用户(这里的 Sale )不必关心迭代呢?让接口的协议变成:我会去找 Item,找到的话帮你执行逻辑 blabla ……
高阶函数( Lambda 表达式)
要回答上面的问题,我们得看一下另一个 Scala 的函数式特性,那就是高阶函数,如果是 Java 8 ,那就是 Lambda 表达式,我们可以这样定义 Inventory.itemForBarcode(),它接收一个 barcode 和一个接收 Item 的函数 f :
1
2
3
4
|
def itemForBarcode(barcode:String)(f:Item=>Unit){
items.get(barcode).foreach(f)
}
|
如果能找到 item ,f 就会执行,现在 Sale.addBarcode() 变成了:
1
2
3
4
5
6
7
|
def addBarcode(barcode:String){
inventory.itemForBarcode(barcode){item=>
items:+item
listener.itemAdded(item)
}
}
|
面向对象设计中有一条著名的原则:Tell, Don’t Ask,《Smalltalk by Example》一书的作者这样描述该法则:
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.
从 Inventory 获取数据( item 也好,null 也好,Option[Item] 也好),然后根据其内容是否存在再做操作,更多是过程式思想;相对的,扔给 Inventory 一个 barcode 和一段函数则是所谓的 Tell :去查一查,查到就干这个!
上述的代码用 Java 8 Lambda 表达式也能轻松实现,但如果是 Java 6/7 ,就比较麻烦了,你得搞一个 interface,然后传一个匿名内部类,非常麻烦。
小结
整个过程我接触了 null, 异常, Null Object Pattern, Option, 高阶函数等概念,这里有过程式编程范式、面向对象编程范式、函数式编程范式,这是最让我惊异的,各种编程范式对于错误处理有着不同的方式,你会发现 Java 作为一门面向对象语言其实糅合了不少过程式的处理方式,而 Scala 或者 Java 8 则有函数式的处理方式,而且总体上来说函数式的处理方式更加优雅。
原创文章,转载请注明出处, 本文地址: http://www.juvenxu.com/2014/10/28/using-design-to-prevent-errors/
相关推荐
自动错误预防(Automated Error Prevention,简称 AEP)是一种通过自动化手段在整个软件开发周期中预防错误,以提高产品质量的方法。随着软件系统的复杂度不断增加,传统的手动检查方式越来越难以满足对软件质量的...
24. **Krug可用性三大定律**:别让我思考、不要让用户犯错、一次只做一件事,不包括通过页面设计防止错误发生,这是预防错误设计原则。 以上就是从提供的笔试题中提取的用户体验与研究的相关知识点,涵盖了设计原则...
因此,在FPGA设计中,如何有效地预防和解决亚稳态问题是一个非常重要的研究课题。 亚稳态问题产生的机理,主要是因为在触发器或锁存器的时钟输入端,如果信号变化发生在时钟边沿附近,那么触发器可能无法在该时钟...
#### 第三章:设计与开发中的错误预防与修复 **设计阶段的错误预防** - **遵循最佳实践**:采用成熟的设计模式和原则来指导设计工作。 - **设计审查与评估**:定期进行设计审查,确保设计方案满足需求并具有良好的...
在进行PCB设计时,尽管设计师们会尽最大努力以确保设计的准确无误,但仍然有可能犯一些常见错误。...通过注意这些问题并采取相应的预防措施,可以最大限度地减少错误,提高设计的可靠性和生产效率。
6. **不合格原因分析**:当发现潜在问题时,需要深入分析其产生的原因,可能是设计疏忽、数据错误、沟通不畅等。通过分析,可以找出根本原因,从而制定有效的纠正措施。 7. **纠正措施负责人**:指定责任人负责执行...
对于设计人员来说,深刻理解这些潜在问题,并在设计阶段采取相应的预防措施,是确保USB系统可靠和高效的关键。随着USB技术的不断发展,遵循规范和最佳实践将有助于减少设计错误,提高产品的市场竞争力。
异常处理是编程中的一种机制,用于预防和应对程序运行时可能出现的错误。在易语言中,异常处理通常包括try、catch和finally等语句块。try块用来包含可能会抛出错误的代码,如果try块中的代码出现错误,程序会立即...
5. **错误预防**:除了处理错误,设计装置也应考虑如何避免错误的发生,例如,通过输入验证、异常预防和代码审查等手段。 标签中的“行业文档”表明这些内容可能是针对特定行业或应用场景的标准或最佳实践,强调了...
因此,分析、预防和降低内存错误带来的风险是嵌入式系统开发中至关重要的任务。 首先,内存泄漏和堆损坏是内存错误的两大类别。内存泄漏指的是程序在分配内存后未能正确释放,导致可用内存随时间递减,最终可能导致...
总的来说,预防设计差错需要综合运用多种方法和技术,从源头上避免设计错误,并在生产流程的各个环节中实施防错措施,确保产品的质量和用户体验。通过持续改进和创新,我们可以将人为差错降至最低,从而提高生产效率...
"预防装机误动作的绝缘纸板装机设备"是这样一种专为解决特定问题而设计的机械设备,它主要用于防止在安装过程中由于操作不当或机械故障导致的误动作。这种设备的应用领域可能涵盖电子制造、电力工程、自动化生产线等...
八年级的学生对传染病有一定的生活经验和感性认识,但在科学理解上可能存在不足,比如对病因、传播途径和预防措施的理解不深入,容易受到错误观念的影响。因此,教学过程中应鼓励学生参与课前预习,如调查本地传染病...
《教练车用错误鸣叫装置》是一份详细探讨教练车安全技术的专业文档,它主要针对的是驾驶员培训领域中的一种创新设备——错误鸣叫...通过这样的设计,我们看到了技术在预防事故、保障人们生命财产安全方面的巨大潜力。
在IT领域,尤其是在数据库管理与维护中,遇到错误代码是家常便饭。对于使用IBM DB2数据库系统的管理员和技术人员而言,...同时,定期备份数据、优化数据库设计、加强权限管理和监控,都是预防和减少DB2错误的有效手段。
### 基于Java的郑州市儿童预防接种信息管理系统的设计与实现 #### 摘要解析与研究背景 本文档详细介绍了“基于Java的郑州市儿童预防接种信息管理系统的设计与实现”这一课题的研究背景、目标、意义及主要内容。在...
3. 错误预防:通过检查和验证输入数据,模块可以在问题发生前预防某些错误,提高程序的健壮性。 4. 自定义错误处理:允许开发者注册自定义的错误处理函数,以适应特定项目的需要。 总之,“易语言源码错误处理模块...
书中涉及的99个常见编程错误,覆盖了从基本语法错误到复杂的编程技巧和设计问题。例如,有些错误可能涉及到对C++语言特性理解不足,如运算符优先级的错误使用、内存管理不当等。有些则可能是设计模式应用不恰当,...
21. **不完善的错误处理**:良好的错误处理机制可以预防系统崩溃,确保在出现问题时能够恢复。 22. **过早优化**:优化应在对系统有全面理解后进行,过早优化可能导致设计变得复杂且难以维护。 23. **未充分测试...
9. 预防策略:了解错误代码可以帮助预防问题的发生,通过优化SQL语句、正确设计表结构、实施适当的安全策略等方法,可以减少错误的出现。 10. 版本差异:虽然Oracle9i是较早的版本,但理解其错误代码对理解后续版本...