Seven Deadly Sins : Những sai lầm thường gặp của Ruby Metaprogramming
Bài dịch từ 7 Deadly Sins of Ruby Metaprogramming Developer chúng ta thường dành phần lớn thời gian của mình cho việc coding , cũng như những hoạt động liên quan như đọc hay maintain code. Vì thế , việc tạo thói quen code một cách thật hiệu quả mang lại cho ta rất nheièu lợi ích. Tuy nhiên, dù ...
Bài dịch từ 7 Deadly Sins of Ruby Metaprogramming
Developer chúng ta thường dành phần lớn thời gian của mình cho việc coding , cũng như những hoạt động liên quan như đọc hay maintain code. Vì thế , việc tạo thói quen code một cách thật hiệu quả mang lại cho ta rất nheièu lợi ích. Tuy nhiên, dù metaprogramming trong Ruby rất mạnh, nhưng cũng có rất nhiều bad-practice dễ gặp phải có thể làm giảm hiệu quả của nó . Trong bài viết này, chúng ta hãy cùng tìm hiểu về những sai lầm thường gặp đó.
Sai lầm 1 : Lạm dụng method_missing
Paolo Perrotta, tác giả cuốn Metaprogramming Ruby có nhận xét
The method_missing() is a chainsaw: it’s powerful, but it’s also potentially dangerous.
Chúng ta hãy cùng tìm hiểu một ví dụ đơn giản để thấy tác hại của việc lạm dụng method này. Hãy tưởng tượng, chúng ta đang muốn implement Null Object pattern cho class Order, trong đó có method để xử lí những loại 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
Từ đây, nếu muốn xử lí thêm những loại tiền tệ khác ,nhưng ko muốn viết thêm method,ta có thể dùng method_missing() hoặc define_method , hãy thử benchmark 2 cách làm này
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
Kết quả
user system total real define_method 0.050000 0.000000 0.050000 (0.062126) method_missing 0.460000 0.000000 0.460000 (0.582257)
Ta có thể thấy , define_method nhanh hơn tới gần 10 lần. Đồng thời , nếu ta có thể list được hết danh sách những loại tiền tệ cần xử lí, cách làm này hoàn toàn có thể thỏa mãn yêu cầu ta đặt ra. Lạm dụng method_missing trong trường hợp này sẽ dẫn tới performance ko tốt.
Sai lầm 2 : Không override respond_to_missing?
Mỗi khi dùng method_missing, ta nên override respond_to_missing? . Khi làm việc trong project Rails, bạn sẽ quen với việc kiểm tra môi trường hiện tại bằng Rails.env.production? thay vì Rails.env == ‘production’ Hãy cùng tìm hiểu xem, việc này đc implement thế nào trong ActiveSupport trong Rails4.2. Hãy cùng xem file string_inquirer.rb
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
Ở đây, method_missing kiểm tra xem method có kết thúc với kí tự ? không, nếu có thì cắt bỏ khỏi tên method, và đem so sánh với self. Nếu giống nhau thì trả về true, nếu không trả về false Nếu ta không override respond_to_missing?, object sẽ không nhận những method được tạo động.
Sai lầm 3 : Không xử lí unknown case
Trong các ví dụ trước, ta đã thấy cách Rails dùng super để truyền những truy vấn mà method hiện tại không biết xử lí. Như trong class StringInquirer, nếu method không kết thúc với kí tực ?, thì truy vấn sẽ được truyền lên lớp cao hơn bằng cách gọi super. Nếu không dùng super, rất có thể sẽ dẫn tới những bug rất khó phát hiện.
Sai lầm 4 : Sử dụng define_method khi không cần thiết
Ta hãy dùng gem Restclient(version 2.0.0.alpha) làm ví dụ. Trong file bin/restclient ta sẽ 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
Điều gì không ổn ở đây? Bạn đã làm cho code của mình trở nên khó đọc, trong khi không thu được lợi ích gì. Những method của HTTP là ổn định, gần như không bao giờ thay đổi. Dùng metaprogramming ở đây, bạn đã tự làm phức tạp hóa vấn đề, khai báo động cho những phương thức của HTTP.
Sai lầm 5 : Thay đổi ý nghĩa method của class
Trước khi bạn tác động vào một class và thêm mới method, hãy kiểm tra xem method đó đã tồn tại hay chưa. Nếu không, vô tình bạn đã làm thay đổi ý nghĩa của method đó. Điều này sẽ khiến cho người khác , khi dùng code của bạn, rất dễ bị nhầm lẫn và gây sự khó hiểu không đáng có . Bạn có thể học cách mà JSON gem đã làm. Gem này mở từng class built-in của Ruby như Range, Rational, Symbol ... và khai báo method to_json trong từng class, thay vì khai báo global.
Sai lầm 6 : Sai chiều phụ thuộc
Trong một kiến trúc nhiều tầng, tầng dưới cùng thường được gọi đến bởi rất nhiều các library nằm phía trên. Vì thế, chúng phải hoàn toàn ko dính dáng tới các lớp ở trên, để có thể sử dụng lại. Lớp ở dưới phụ thuộc vào lớp ở trên là một trong những sai lầm nghiêm trọng mà programmer có thể phạm phải. Mặc dù sai lầm này không hẳn chỉ riêng trong metaprogramming, nhưng ảnh hưởng của nó khá lớn, nên tôi thấy cần phải nhắc đến ở đây. Những thư viện ở lớp dưới cùng, không nên dùng kiểu như defined? some_constant. Nếu thật sự cần thiết, có thể dùng những file config để khai báo constant cần dùng. Tóm lại, sự phụ thuộc nên là một chiều duy nhất, từ trên phụ thuộc xuống dưới, và không bao giờ nên có chuyện ngược lại.
Sai lầm 7 : Nested quá nhiều tầng
Sử dụng metaprogramming có thể sẽ khiến phát sinh qúa nhiều nested block. Điều này sẽ khiến cho code của bạn trở nên rất khó đọc. Ví dụ như trong Spree game, file backend/spec/controllers/spree/admin/payments_controller_spec.rb có đoạn sau
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
Nested quá nhiều khiến đoạn code trên rất khó đọc. API nên đơn giản và dễ sử dụng. Một ví dụ về cách viết tốt là validation method trong ActiveModel, ví dụ như
class Person include ActiveModel::Validations attr_accessor :name validates_presence_of :name end
Tóm lại , Metaprogramming rất mạnh, và có thể giúp bạn dễ dàng giải quyết những vấn đề phức tạp. Tuy nhiên, nó trả giá bằng việc khiến code bạn trở nên khó đọc và khó hiểu hơn. Vì thế , nên cân nhắc khi quyết định sử dụng.