Cách "try again" khi gặp exception trong Ruby
Bài đăng này đã không được cập nhật trong 8 năm
Trong lập trình, chắc hẳn bạn đã không ít lần gặp những lỗi "khó đỡ" mà cách giải quyết duy nhất là chạy lại đoạn code một lần nữa. May thay, các lập trình viên Ruby được cung cập một công cụ để xử lý tình huống này. Bài viết này sẽ nghiên cứu về cơ chế này và cách mà nó hoạt động.
Từ khóa "retry"
Từ khóa "retry" được build sẵn trong cấu trúc rescue của Ruby. Cách sử dụng "retry" rất đơn giản: Chỉ cần đặt nó vào trong rescue block sẽ giúp cho chương trình chạy lại đoạn code ở block trước đó.
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
Những điều cần lưu ý khỉ sử dụng "retry":
- Khi chương trình chạy đến "retry", tất cả đoạn code từ "begin" đến "rescue" sẽ được chạy lại từ đầu.
- Nếu không có cơ chế để giới hạn số lần "retry", rất có thể chương trình sẽ rơi vào vòng lặp vô hạn.
- Code trong cả block "begin" và "rescue" đều có thể truy cập đến các biến chung của block cha.
Các vấn đề với "retry"
Với cách đặt giới hạn hợp lý, "retry" trở thành một công cụ hữu dụng. Vấn đề chính ở đây là đôi khi chạy lại cả một block không phải là một ý hay.
Ví dụ như trong trường hợp sử dụng một gem hoặc service giúp ta post bài lên Twitter, Facebook,... chỉ bằng một hàm, code của ta sẽ giống như phía dưới:
SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
# ...posts to Twitter API
# ...posts to Facebook API
# ...etc
Nếu một trong các API bị lỗi, chương trình sẽ raise exception SocialMedia::TimeoutError. Nếu ta bắt exception và retry, các API đã post thành công sẽ phải post lại lần nữa. Chắc chắn không ai đồng ý với ý tương này.
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 phải đặt những điều kiện, tham số phức tạp, ta phải tìm được một cách nào đó để retry lại block từ dòng lệnh lỗi chứ không phải từ đầu begin. Rất may mắn là Ruby hoàn toàn cho phép ta làm điều đó.
Chú ý: Tất nhiên giải pháp chuẩn của vấn đề này là viết lại thư viện kết nối. Nhưng đối với những người không hiểu rõ về thư viện hoặc những người lười (như tôi) thì đây không phải một ý hay.
Continue trong khối rescue
Trong khối "begin" ta có thể đặt một vài "save point", giống như trong game. Ta có thể đi ra và làm các công việc khác, rồi khi muốn thì "load" lại "save point" và "chơi" tiếp. Dưới đây là một ví dụ:
require "continuation"
counter = 0
continuation = callcc { |c| c } # create savepoint
puts(counter += 1)
continuation.call(continuation) if counter < 5 # jump back to savepoint
Mỗi lần quay trở về save point, các biến của continuation sẽ là các tham số truyền vào trong hàm "call". Vì vây, ta nên dùng cú pháp continuation.call(continuation)
Thêm continuation vào exception
Bây giờ ta sẽ sử dụng continuation để thêm các phương thức skip vào các exception. Đoạn code dưới là những gì ta đang mong muốn thực hiện. Khi một exception bị raise lên, hàm e.skip
làm cho code tiếp tục chạy tiếp như không có gì sảy ra.
begin
raise "the roof"
puts "The exception was ignored"
rescue => e
e.skip
end
# ...outputs "The exception was ignored"
Để làm được điều này, ta cần thêm hàm vào class Exception:
class Exception
attr_accessor :continuation
def skip
continuation.call
end
end
Bây giờ ta sẽ định nghĩa continuation cho các exception:
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ờ ta có thể skip các exception và chạy tiếp code như chưa hề có cuộc chia li! Tôi đã thành công còn bạn thì sao?
All rights reserved