Ruby 容错和速错

hooopo for Shopper+ · 发布于 2015年1月24日 · 最后由 fleuria 回复于 2015年3月24日 · 5250 次阅读
8
本帖已被设为精华帖!

程序出现异常,不想让用户看到,会给一个友好的提示,这种做法一般称作“容错”。另一方面,我们希望自己的程序能够把隐藏错误尽早暴露出来,及时修复,这种思路称之为“速错”。拼写错误、源数据错误、逻辑错误都需要速错。当然,容错和速错也并不总是对立的,分清何时应用哪种策略非常重要。

Hash#fetch over Hash#[]

Hash的key值是已知的情况,比如状态枚举。优先使用fetch可以避免拼写错误带来的意外。举例:

class Event < AR
  STATUS = {
    :open => 1,
    :closed => 0
  }
end
Event.update_all(:status => Event::STATUS[:close], "created_at > '2011-1-1'") # => update status to null
Event.update_all(:status => Event::STATUS.fetch(:close), "created_at > '2011-1-1'") # => raise KeyError: key not found: :close

上面例子由于错误拼写,把closed拼成 close,造成了一个Silent failure,而使用fetch方法就会在拼错时直接抛出异常,避免了之后的错误。

使用常量

除此之外,声明常量也可以带来同样效果,本质是给输入加上了拼写检查。

class Event < AR
  CLOSED = 0
  OPEN = 1
end
Event.update_all(:status => Event::CLOSE, "created_at > '2011-1-1'")       # => raise NameError: uninitialized constant CLOSE

善用attr method

经常看到有人问,attr method有什么用,直接使用实例变量不好么,这样的问题。

如果不是对gettersetter 有额外的封装,两者是一样的,但用attr method调用和上面两个例子一样,也起到了错误检查的作用。我们知道Ruby的实例变量有一个隐藏特性,实例变量不需要定义就可以使用,不会报错。当然,在启动Ruby加 -w 参数是可以warning提示的。

class Event
  def initialze(attrs)
    @closed = attrs[:closed] || true
  end

  def xxx
    if @close
      # some code will not run
    end
  end
end

如果我们使用attr reader,调用close方法直接就会抛出undefind method异常,让拼写错误尽早现形。

save! over save

经常看到一段事物代码里用save的情况,save不会抛异常,这样事物的意义就失去了。显然,这属于不理解事物运作机制的错误用法。在没有事物的代码里,如何选择save还是save!也是非常困难的。我个人的习惯是,在不需要错误回显的情况,一律使用save!update_attributes!这样能够Fail Fast 的方法。

不要滥用 rescue

如果你的代码里经常见到begin rescue,这就是一种bad smell。当然,调用外部接口时,异常一定要捕获,但有一部分新手会在自己写的一堆应用逻辑外面套上begin rescue,并且不明确捕获的错误类型。当你问他,你要捕获哪种错误的时候,他一定回答不出。对于自己代码里的逻辑错误不应该去捕获错误,而是查出错误的来源,从源头上解决问题。即使要捕获,也应该有一个明确的类型:

begin
rescue KeyError => e
  ...
end

用异常做控制流的做法也不少见,但不是本文讨论的话题..

数据源错误

容错的思想带来的一个问题是总想隐藏问题,不是直接去从源头解决。当一个产品数据被误删导致用户订单页面出错,不应该去容错,到处写order_item.product.try(:name),而是应该去恢复被删数据。

同理,数据库出现脏数据,不应该去改变代码的写法,比如,把save改成save(false),去跳过验证。应该做的也是清理数据源,否则就需要无穷无尽的“容错”代码。

VIA:http://shopperplus.github.io/blog/2015/01/24/rong-cuo-he-su-cuo.html

共收到 33 条回复
490

bad small => bad smell??? 说的很好,现在很少见到使用rescue的了,基本上都是Fail-fast.

8

#1楼 @046569 感谢,已更正。

332

好文!程序逻辑错误一定要及早暴露出来,而且是在发生问题的地方直接暴露出来,这样能避免日后花费数倍精力查找问题和弥补错误的麻烦,这一点在开发中深有体会。

121

学到 Hash#fetch

begin resuce -> begin rescue

5489

:plus1:

262

踩过 Hash#fetch ...

4755

:plus1:

15

赞 +1

7072

:plus1: :plus1:

8

#4楼 @lyfi2003 感谢,已修。

14358

用fetch的话,指定默认值莫非不是标配?

14837

赞+1~

8

#13楼 @est

默认值其实是在保证值一定存在,但不管参数是不是合法。 只fetch其实是在验证参数是否合法,从源头解决问题。

指定默认值在某些场景是一个很好的容错手段,但也会制造“Silent failure”。

2880

如果 hash 在函数的参数中, 那么除了 Hash#fetch 还有个好办法:

def foo opts
  bar = opts.fetch :bar
end

可以这么写 (Ruby 2.2)

def foo bar: bar
  bar
end

foo({}) # bar 未给定
foo baz: 3 # keyword baz 未定义
foo bar: 3 # :)
5255

:thumbsup:

7848

#11楼 @flowerwrong 带logo得赞 也是醉了... :plus1:

9618

:thumbsup:

594

👏 :plus1:

3253

:thumbsup:

22楼 已删除
2408

:thumbsup:

96

有些思路在大多数的编程语言中应该都会有共鸣,而且楼主的表述形式我很欣赏。直接了当,感谢

1107

话是这么说,但是抛出异常的方式对交互会造成很大困扰(遇到异常后回转向到500页面,或者在控制器中增加rescue_from blcok) 当然也是有很多好处,AirBrake能够接到异常然后汇报。

我的话,一直在重要的业务逻辑中使用fail-fast方式来编写(比如涉及金钱),外部IO也可以使用这个手法,好处是容易收集到现场信息,便于调试

17076

容错和速错,用编程和文字语言加以实证表述~赞

8347

:thumbsup:

594

:plus1: 我喜欢 用常量 替代 符号

13225

《最强大脑》:精确性与容错性

8744

#16楼 @luikore

def foo(name: n, value: v)
  {n: v}
end  

foo name: "li", value: 3

#NameError: undefined local variable or method `v' for main:Object from (pry):14:in `foo'

这样好像不行,能说说这个语法是怎么来的吗?

2880

#30楼 @lithium4010

你得这么写:

def foo(name: name, value: value)
  {name => value} # 注意 => 和 : 的区别
end

因为 value: 的写法会声明局部变量 value, 所以你才能用 value

8744

#31楼 @luikore

还是不太明白。。。 我是觉得写成 def foo(name: name, value: value) 太罗嗦了才会想到用简写的。这个语法的一般使用场景是什么呢?

2880

#32楼 @lithium4010 场景就是楼主说的场景. 你这样写短是短了, 但多起了两个名字 -_-

96

感觉 “容错” 容的只是网络超时这种通信的错误,而逻辑的错误一般都要速错。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册