Rails là một framework lớn và nó phát triển lớn hơn hàng năm. Điều này làm cho một số tính năng hữu ích dễ dàng bị bỏ qua khi không được chú ý. Trong loạt bài này, chúng ta sẽ xem xét một số chức năng ít được biết đến được tích hợp trong Rails cho các tác vụ cụ thể.
Trong bài đầu tiên của loạt bài này, chúng ta sẽ xem xét cách gọi Rails.env.test?
thực sự hoạt động ngầm bằng cách sử dụng StringInquirer
ít được biết đến lớp từ ActiveSupport. Đi thêm một bước nữa, chúng ta cũng sẽ đi qua StringInquirer
mã nguồn để xem nó hoạt động như thế nào, (cảnh báo spoiler) là một ví dụ đơn giản về lập trình siêu hình của Ruby thông qua method_missing
đặc biệt phương pháp.
Trình trợ giúp Rails.env
Bạn có thể đã thấy mã kiểm tra môi trường Rails:
if Rails.env.test?
# return hard-coded value...
else
# contact external API...
end
Tuy nhiên, test?
thực sự làm, và phương pháp này đến từ đâu?
Nếu chúng tôi kiểm tra Rails.env
trong bảng điều khiển, nó hoạt động giống như một chuỗi:
=> Rails.env
"development"
Chuỗi ở đây là bất kỳ biến môi trường RAILS * ENV nào được đặt thành; Tuy nhiên, nó không phải _ chỉ * một Chuỗi. Khi đọc lớp trong bảng điều khiển, chúng ta thấy:
=> Rails.env.class
ActiveSupport::StringInquirer
Rails.env
thực sự là một StringInquirer.
StringInquirer
Mã cho StringInquirer vừa ngắn gọn vừa được ghi chép đầy đủ. Tuy nhiên, nó hoạt động như thế nào có thể không rõ ràng trừ khi bạn đã quen với khả năng lập trình siêu hình của Ruby. Chúng tôi sẽ đi qua những gì đang xảy ra ở đây.
class StringInquirer < String
...
end
Đầu tiên, chúng ta thấy rằng StringInquirer là một lớp con của String. Đây là lý do tại sao Rails.env
hoạt động giống như một Chuỗi khi chúng ta gọi nó. Bằng cách kế thừa từ Chuỗi, chúng tôi tự động nhận được tất cả các chức năng giống như Chuỗi, vì vậy chúng tôi có thể coi nó như một Chuỗi. Rails.env.upcase
cũng như vậy ActiveRecordModel.find_by(string_column: Rails.env)
.
Trong khi Rails.env
là một ví dụ tiện dụng, được tích hợp sẵn của StringInquirer, chúng tôi cũng có thể tự do tạo của riêng mình:
def type
result = "old"
result = "new" if @new
ActiveSupport::StringInquirer.new(result)
end
Sau đó, chúng tôi nhận được các phương thức dấu chấm hỏi trên giá trị trả về:
=> @new = false
=> type.old?
true
=> type.new?
false
=> type.testvalue?
false
=> @new = true
=> type.new?
true
=> type.old?
false
method_missing
method_missing
là nước sốt bí mật thực sự ở đây. Trong Ruby, bất cứ khi nào chúng ta gọi một phương thức trên một đối tượng, Ruby bắt đầu bằng cách xem xét tất cả các tổ tiên của đối tượng (tức là các lớp cơ sở hoặc các mô-đun được bao gồm) cho phương thức. Nếu không tìm thấy, Ruby sẽ gọi method_missing
, truyền vào tên của phương thức mà nó đang tìm kiếm, cùng với bất kỳ đối số nào.
Theo mặc định, phương pháp này chỉ nêu ra một ngoại lệ. Tất cả chúng ta có lẽ đã quen thuộc với NoMethodError: undefined method 'test' for nil:NilClass
loại thông báo lỗi. Chúng tôi có thể triển khai method_missing
của riêng mình điều đó không tạo ra một ngoại lệ, đó chính xác là những gì StringInquirer
hiện:
def method_missing(method_name, *arguments)
if method_name.end_with?("?")
self == method_name[0..-2]
else
super
end
end
Đối với bất kỳ tên phương thức kết thúc bằng ?
, chúng tôi kiểm tra giá trị của self
(tức là chuỗi) so với tên phương thức không có ?
. Nói một cách khác, nếu chúng ta gọi StringInquirer.new("test").long_test_method_name?
, giá trị trả về là "test" == "long_test_method_name"
.
Nếu tên phương thức không không kết thúc bằng dấu chấm hỏi, chúng ta quay trở lại method_missing
ban đầu (cái sẽ đưa ra một ngoại lệ).
response_to_missing?
Có một phương thức nữa trong tệp:respond_to_missing?
. Chúng tôi có thể nói rằng đây là một phương thức đồng hành với method_missing
. Mặc dù method_missing
cung cấp cho chúng tôi chức năng, chúng tôi cũng cần một cách để nói với Ruby rằng chúng tôi chấp nhận các phương thức kết thúc bằng dấu chấm hỏi này.
def respond_to_missing?(method_name, include_private = false)
method_name.end_with?("?") || super
end
Điều này có tác dụng nếu chúng ta gọi respond_to?
trên đối tượng này. Nếu không có điều này, nếu chúng tôi gọi StringInquirer.new("test").respond_to?(:test?)
, kết quả sẽ là false
bởi vì chúng tôi không có phương pháp rõ ràng nào được gọi là test?
. Điều này rõ ràng là gây hiểu lầm vì tôi muốn nó trả về true nếu tôi gọi Rails.env.respond_to?(:test?)
.
respond_to_missing?
là thứ cho phép chúng ta nói "có, tôi có thể xử lý phương pháp đó" với Ruby. Nếu tên phương thức không kết thúc bằng dấu chấm hỏi, chúng ta quay lại phương thức lớp cha.
Các trường hợp sử dụng thực tế
Bây giờ chúng ta đã biết cách StringInquirer hoạt động, hãy xem xét các trường hợp mà nó có thể hữu ích:
1. Các biến môi trường
Các biến môi trường đáp ứng hai tiêu chí khiến chúng trở thành lựa chọn tuyệt vời cho StringInquirer. Đầu tiên, chúng thường có một tập hợp giới hạn, đã biết về các giá trị có thể có (như enum
) và thứ hai, chúng ta thường có logic có điều kiện phụ thuộc vào giá trị của chúng.
Giả sử ứng dụng của chúng tôi được kết nối với một API thanh toán và chúng tôi có thông tin đăng nhập được lưu trữ trong các biến môi trường. Trong hệ thống sản xuất của chúng tôi, đây rõ ràng là API thực, nhưng trong phiên bản giai đoạn hoặc phát triển của chúng tôi, chúng tôi muốn sử dụng API hộp cát của họ:
# ENV["PAYMENT_API_MODE"] = sandbox/production
class PaymentGateway
def api_mode
# We use ENV.fetch because we want to raise if the value is missing
@api_mode ||= ENV.fetch("PAYMENT_API_MODE").inquiry
end
def api_url
# Pro-tip: We *only* use production if MODE == 'production', and default
# to sandbox if the value is anything else, this prevents us using production
# values if the value is mistyped or incorrect
if api_mode.production?
PRODUCTION_URL
else
SANDBOX_URL
end
end
end
Lưu ý rằng trong phần trên, chúng tôi đang sử dụng String#inquiry
của ActiveSupport phương thức này có thể chuyển đổi một cách thủ công một Chuỗi thành một StringInquirer cho chúng tôi.
2. Phản hồi API
Tiếp tục ví dụ về API thanh toán của chúng tôi ở trên, API sẽ gửi cho chúng tôi phản hồi bao gồm một số trạng thái thành công / thất bại. Một lần nữa, điều này đáp ứng hai tiêu chí khiến nó trở thành ứng cử viên cho StringInquirer:một tập hợp giới hạn các giá trị có thể có và logic có điều kiện sẽ kiểm tra các giá trị này.
class PaymentGateway
def create_charge
response = JSON.parse(api_call(...))
result = response["result"].inquiry
if result.success?
...
else
...
end
# result still acts like a string
Rails.logger.info("Payment result was: #{result}")
end
end
Kết luận
StringInquirer là một công cụ thú vị để có trong túi sau của bạn, nhưng cá nhân tôi, tôi sẽ không tiếp cận nó quá thường xuyên. Nó có một số cách sử dụng, nhưng hầu hết thời gian, một phương thức rõ ràng trên một đối tượng sẽ cho bạn kết quả tương tự. Một phương thức được đặt tên rõ ràng cũng có một vài lợi ích; nếu giá trị cần thay đổi, bạn chỉ phải cập nhật một nơi duy nhất, và nó làm cho cơ sở mã dễ dàng hơn để tìm kiếm nếu một nhà phát triển đang cố gắng xác định phương pháp.
Mặc dù nó tập trung vào StringInquirer, nhưng bài viết này nhằm mục đích giới thiệu nhẹ nhàng hơn về một số khả năng lập trình siêu hình của Ruby bằng cách sử dụng method_missing
. Tôi muốn nói rằng bạn có thể không muốn sử dụng method_missing
trong ứng dụng của bạn. Tuy nhiên, nó thường được sử dụng trong các khuôn khổ, chẳng hạn như Rails hoặc các ngôn ngữ dành riêng cho miền do gems cung cấp, vì vậy có thể rất hữu ích nếu bạn biết "xúc xích được tạo ra như thế nào" khi bạn gặp sự cố.