7 Deadly Sins of Ruby Metaprogramming
Bài đăng này đã không được cập nhật trong 6 năm
Với tư cách là một nhà phát triển ruby on rails, bạn dành 90% thời gian cho các hoạt động liên quan tới code như đọc và duy trì code hiện tại. Với một khoảng thời gian dài dành cho những tác vụ này, và điều quan trọng là đảm bảo rằng mọi thứ bạn làm (và code) đều hiệu quả. Trong khi metaprogramming
với Ruby có thể cực kỳ mạnh mẽ, nhưng cũng có thể khiến cho việc đọc trở nên khó khăn hay tạo ra sự cân bằng không tốt, cuối cùng sẽ làm tăng chi phí bảo trì trong dài hạn. Vì vậy, hôm nay tôi muốn chia sẻ những sai lầm có thể bạn đã từng gặp phải trong quá trình làm dự án của mình.
1. Sử dụng method_missing
như sự lựa chọn đầu tiên
Paolo Perrotta
, tác giả của cuốn Metaprogramming Ruby
có nói: "Method_missing ()
là một mắt xích: nó mạnh mẽ, nhưng cũng rất nguy hiểm." Cách tốt nhất để sử dụng nó là gì? Hãy để tôi chỉ cho bạn một ví dụ sử dụng method_missing
để thực thi Null Object pattern
cho một order, nó sẽ có 1 method để xử lý các tiền tệ khác nhau.
class NullOrder
def price_euro
0.0
end
def price_usd
0.0
end
# ...
# more methods needed to handle all other currencies
end
Bây giờ để tránh viết nhiều method, chúng ta có 2 lựa chọn: Chúng ta hoặc có thể sử dụng define_method
hoặc method_missing
. So sánh giữa 2 method này như sau:
require 'benchmark'
iterations = 100_000
Benchmark.bm do |bm|
bm.report('define_method') do
class NullOrder
['usd', 'euro', 'yen'].each do |method|
define_method "price_#{method}".to_sym do
0.0
end
end
end
iterations.times do
o = NullOrder.new
o.price_euro
o.price_usd
end
end
bm.report('method_missing') do
class NullOrder2
def method_missing(m, *args, &block)
m.to_s =~ /price_/ ? 0.0 : super
end
end
iterations.times do
o2 = NullOrder2.new
o2.price_euro
o2.price_usd
end
end
end
user system total real
define_method 0.050000 0.000000 0.050000 (0.062126)
method_missing 0.460000 0.000000 0.460000 (0.582257)
Thống kê này chỉ ra rằng define_method
nhanh gấp 10 lần sử dụng method_missing
. Chúng ta không cần nhiều sự linh hoạt để xử lý bất kỳ loại tiền tệ nào. Miễn là chúng ta có thể liệt kê tiền tệ sẽ được xử lý trong code của chúng ta, chúng ta có thể sử dụng define_method
để giảm sự trùng lặp và đạt được mã hiệu suất tốt hơn.
2. Không ghi đè respond_to_missing
Bạn phải ghi đè lên respond_to_missing?
mỗi khi bạn ghi đè method_missing
. Nếu bạn đã làm việc trong một dự án Rails, bạn sẽ làm quen với việc kiểm tra môi trường hiện tại bằng cách sử dụng:
Rails.env.production?
thay vì Rails.env == ‘production’
Vì vậy, chúng ta hãy xem cách thực hiện điều này trong ActiveSupport string_inquirer.rb
trong Rails 4.2:
module ActiveSupport
# Wrapping a string in this class gives you a prettier way to test
# for equality. The value returned by Rails.env is wrapped
# in a StringInquirer object so instead of calling this:
#
# Rails.env == 'production'
#
# you can call this:
#
# Rails.env.production?
class StringInquirer < String
private
def respond_to_missing?(method_name, include_private = false)
method_name[-1] == '?'
end
def method_missing(method_name, *arguments)
if method_name[-1] == '?'
self == method_name[0..-2]
else
super
end
end
end
end
Việc thực hiện method_missing
kiểm tra để đảm bảo phương pháp kết thúc bằng một dấu hỏi. Nếu có, nó sẽ chèn dấu chấm hỏi đó từ tên phương thức và so sánh nó với đối tượng hiện tại (giá trị của bản thân nó). Và nếu chúng giống nhau, nó sẽ trả về true và ngược lại.
Bạn có thể thấy các điều kiện được sử dụng để bẫy một số cuộc gọi là giống như trong respond_to_missing?
thực hiện, và đó là chính xác cách chúng ta muốn nó. Nếu bạn không ghi đè answer_to_missing ?
, đối tượng sẽ không trả lời bất kỳ phương pháp nào được tạo ra tự động. Điều này sẽ gây ngạc nhiên khi các developer thử nghiệm với thư viện của bạn trong console irb, và một thư viện tốt hoạt động như mong đợi với rất ít bất ngờ, nếu có.
3. Quên để xử lý các trường hợp không rõ
Trong ví dụ trước, bạn có thể thấy cách Rails sử dụng super
để truyền bá một cuộc gọi mà phương pháp hiện tại không biết làm thế nào để xử lý. Trong lớp StringInquirer
ở trên, nếu phương pháp không kết thúc với một dấu chấm hỏi, thì nó cho phép cuộc gọi được truyền đi lên bằng cách gọi cho super
.
Nếu bạn không trở lại để super
, sau đó nó có thể dẫn bạn đến lỗi mà thực sự khó có thể theo dõi. Hãy nhớ rằng, method_missing
là nơi mà các lỗi bị ẩn đi. Vì vậy, không quên fallback trên BasicObject # method_missing
khi bạn không biết làm thế nào để xử lý một cuộc gọi.
4. Sử dụng define_method
khi nó không cần
Đây là một ví dụ từ Restclient gem (phiên bản 2.0.0.alpha). Trong bin / restclient, bạn sẽ tìm thấy:
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']
POSSIBLE_VERBS.each do |m|
define_method(m.to_sym) do |path, *args, &b|
r[path].public_send(m.to_sym, *args, &b)
end
end
def method_missing(s, * args, & b)
if POSSIBLE_VERBS.include? s
begin
r.send(s, *args, & b)
rescue RestClient::RequestFailed => e
print STDERR, e.response.body
raise e
end
else
super
end
end
Tại sao lại là lỗi? Bởi vì bạn đang hy sinh khả năng đọc và hiểu của mã mà không trả về cái gì. Danh sách các hoạt động HTTP ổn định - hầu như không bao giờ thay đổi. Nhưng bằng cách sử dụng metaprogramming
, bạn đã tăng tính phức tạp bằng cách tự động định nghĩa các phương thức cho HTTP. Chúng ta không có một vụ nổ các vấn đề methods ở đây, do đó, không cần bất kỳ metaprogramming
nào ở đây.
5. Thay đổi ngữ nghĩa khi mở các class
Bạn nên kiểm tra xem liệu phương pháp đã tồn tại trước khi bạn mở một lớp hiện có và thêm một phương pháp. Nếu bạn không, bạn sẽ thay đổi ngữ nghĩa của một phương pháp hiện có do nhầm lẫn. Điều này sẽ gây bất ngờ cho người dùng thư viện của bạn. Vì vậy, sàng lọc các class global để giảm bớt ô nhiễm trên global namespace. Một ví dụ tốt cho điều này là JSON gem. Nó mở ra các lớp được xây dựng trong Ruby như Range, Rational, Symbol, và nhiều nữa để xác định phương pháp to_json
.
6. Phụ thuộc sai địa chỉ
Trong một kiến trúc theo lớp, lớp dưới cùng có thể phụ thuộc vào nhiều thư viện khác, nằm trên nó. Vì vậy, nó phải là agnostic cho bất kỳ lớp nào ở trên có thể dùng lại được. Có hướng phụ thuộc hướng lên phía trên là sai, và một trong những lỗi kinh khủng mà một lập trình viên có thể gặp. Mặc dù nó liên quan đến khía cạnh của Ruby hơn metaprogramming
, tôi nghĩ rằng tác động là rất lớn và đáng nói đến.
Tôi đã nhìn thấy lỗi này được thực hiện trên các dự án tôi đã làm việc với khách hàng - các thư viện ở lớp thấp nhất không nên sử dụng được define? some_constant
để xem bối cảnh thực hiện trong đó nó đang chạy để thay đổi hành vi. Các thư viện ở lớp thấp nhất phải được độc lập với ngữ cảnh thực thi của chúng. Tuy nhiên, thư viện có thể cung cấp API cho việc sử dụng tùy biến trong một ngữ cảnh cụ thể. Một tùy chọn khác là sử dụng các tệp cấu hình để tùy chỉnh hành vi. Sự phụ thuộc phải theo một hướng, và luôn luôn phải hướng tới sự trừu tượng ổn định.
7. Có quá nhiều mức lồng nhau
Sử dụng metaprogramming
trong code của bạn buộc client phải sử dụng quá nhiều khối lồng nhau, và thật không may, bạn có thể thấy nhiều dự án nguồn mở sử dụng RSpec mắc lỗi này. Dưới đây là một ví dụ từ Spree gem
mà làm cho nó khó hiểu code. Đoạn mã sau đây là một phần của
backend/spec/controllers/spree/admin/payments_controller_spec.rb.
require 'spec_helper'
module Spree
module Admin
describe PaymentsController, :type => :controller do
stub_authorization!
let(:order) { create(:order) }
context "order has billing address" do
before do
order.bill_address = create(:address)
order.save!
end
context "order does not have payments" do
it "redirect to new payments page" do
spree_get :index, { amount: 100, order_id: order.number }
expect(response).to redirect_to(spree.new_admin_order_payment_path(order))
end
end
context "order has payments" do
before do
order.payments << create(:payment, amount: order.total, order: order, state: 'completed')
end
it "shows the payments page" do
spree_get :index, { amount: 100, order_id: order.number }
expect(response.code).to eq "200"
end
end
end
end
end
end
Đây là một lỗi vì nó làm tăng ngữ cảnh mà bạn cần phải giải thích về một đoạn mã cụ thể. API đơn giản hơn, thì thanh lịch và dễ sử dụng hơn. Ví dụ điển hình là các validations
của ActiveModel
- đây là một trong các tài liệu Rails cho một class person:
class Person
include ActiveModel::Validations
attr_accessor :name
validates_presence_of :name
end
Và đây là một ví dụ khác - một cấp lồng nhau trong một tệp routes.rb
trong Rails.
resources :articles do
resources :comments
end
Metaprogramming
là vô cùng có giá trị, và có thể giải quyết các vấn đề phức tạp dễ dàng hơn. Nhưng hãy nhớ, nó chỉ có giá trị sử dụng khi có một sự cân bằng - như khả năng đọc và hiểu để đổi lấy giải quyết các vấn đề phức tạp với ít code hơn. Miễn là bạn giữ những tips trong tâm trí, bạn sẽ thấy mình trở thành một nhà phát triển tốt hơn và hiệu quả hơn trong thời gian không xa.
Tài liệu tham khảo https://www.codeschool.com/blog/2015/04/24/7-deadly-sins-of-ruby-metaprogramming/
All rights reserved