Không phải lỗi nào cũng fatal. Một số chỉ báo hiệu bạn cần thử lại. May mắn thay, Ruby cung cấp vài cơ chế thú vị khiến "thử lại" dễ dàng - dù không phải tất cả đều rõ ràng hoặc nổi tiếng. Trong bài này chúng ta sẽ xem các cơ chế đó và cách chúng hoạt động trong thực tế.
Giới thiệu retry
Ok - cái này hơi rõ ràng, nhưng chỉ nếu bạn biết nó tồn tại. Cá nhân tôi, đã khá lâu trong sự nghiệp Ruby trước khi biết từ khóa "retry" tuyệt vời này.
Retry được tích hợp trong hệ thống rescue ngoại lệ của Ruby. Nó khá đơn giản. Nếu bạn dùng "retry" trong rescue block, nó khiến đoạn code bị rescue chạy lại. Hãy xem ví dụ.
begin retries ||= 0 puts "try ##{ retries }" raise "the roof" rescue retry if (retries += 1) < 3 end # ... outputs the following: # try #0 # try #1 # try #2
Có vài điều cần lưu ý:
Khi retry được gọi, tất cả code giữa begin và rescue được chạy lại. Nó chắc chắn không "tiếp tục từ nơi dừng" hoặc gì đó kiểu đó.
Nếu bạn không cung cấp cơ chế giới hạn retry, bạn sẽ rơi vào vòng lặp vô tận.
Code trong cả begin và rescue block đều truy cập được biến retries cùng scope cha.
Vấn đề với retry
Mặc dù retry tuyệt vời, nó có hạn chế. Chính là toàn bộ begin block được re-run. Nhưng đôi khi điều đó không lý tưởng.
Ví dụ, tưởng tượng bạn dùng gem cho phép post status lên Twitter, Facebook và nhiều trang khác bằng một method call. Nó có vẻ như sau.
SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger") # ...posts to Twitter API # ...posts to Facebook API # ...etc
Nếu một API không respond, gem raise SocialMedia::TimeoutError và abort. Nếu chúng ta catch exception và retry, chúng ta sẽ có duplicate post vì retry bắt đầu lại từ đầu.
begin SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger") rescue SocialMedia::TimeoutError retry end # ...posts to Twitter API # facebook error # ...posts to Twitter API # facebook error # ...posts to Twitter API # and so on
Không hay nếu chúng ta có thể nói với gem "Chỉ bỏ qua Facebook, và tiếp tục danh sách API để post."
May mắn thay, Ruby cho phép chúng ta làm chính xác điều đó.
LƯU Ý: Tất nhiên giải pháp thực sự là re-architect thư viện social media. Nhưng đây không phải use-case duy nhất cho kỹ thuật tôi sẽ chỉ.
Continuations đến giải cứu
Continuations xuýt xoa làm sợ người. Nhưng chỉ vì chúng không dùng thường xuyên và trông hơi kỳ lạ. Nhưng một khi hiểu cơ bản, chúng khá đơn giản.
Continuation giống như "save point" trong code, như trong game video. Bạn có thể đi làm việc khác, rồi nhảy về save point và mọi thứ như bạn để lại.
...ok, đây không phải analogy hoàn hảo, nhưng nó hoạt động. Hãy xem code:
require "continuation" counter = 0 continuation = callcc { |c| c } # define our savepoint puts(counter += 1) continuation.call(continuation) if counter < 5 # jump back to our savepoint
Bạn có thể nhận ra vài điều kỳ lạ. Hãy đi qua chúng:
Chúng ta dùng phương thức callcc để tạo Continuation object. Không có cú pháp OO sạch cho việc này.
Lần đầu biến
continuationđược gán, nó được set thành giá trị trả về của blockcallcc. Đó là lý do block phải có.Mỗi lần nhảy về savepoint, biến
continuationđược gán bất kỳ đối số nào chúng ta pass cho phương thứccall. Đó là lý do cú pháp kỳ lạcontinuation.call(continuation).
Thêm Continuations vào Exceptions
Chúng ta sẽ dùng continuations để thêm phương thức skip vào tất cả ngoại lệ. Ví dụ dưới cho thấy cách nó nên hoạt động. Mỗi khi tôi rescue ngoại lệ, tôi có thể gọi skip, khiến code raise ngoại lệ hoạt động như thể nó chưa bao giờ xảy ra.
begin raise "the roof" puts "The exception was ignored" rescue => e e.skip end # ...outputs "The exception was ignored"
Để làm này, tôi phải phạm vài lỗi. Exception chỉ là một class. Nghĩa là tôi có thể monkey-patch nó để thêm phương thức skip.
class Exception attr_accessor :continuation def skip continuation.call end end
Bây giờ cần set thuộc tính continuation cho mọi ngoại lệ. Rất may raise chỉ là một phương thức, chúng ta có thể override.
BTW, code dưới gần như lấy nguyên văn từ slide deck tuyệt vời của Advi "Things You Didn't know about Exceptions". Tôi chỉ không nghĩ ra cách implement tốt hơn:
require 'continuation' module StoreContinuationOnRaise def raise(*args) callcc do |continuation| begin super rescue Exception => e e.continuation = continuation super(e) end end end end class Object include StoreContinuationOnRaise end
Bây giờ tôi có thể gọi phương thức skip cho bất kỳ ngoại lệ nào và nó sẽ như thể ngoại lệ chưa bao giờ xảy ra.