7 lỗi chết người của Ruby Metaprogramming
Bài đăng này đã không được cập nhật trong 6 năm
Là một developer, bạn thường dành khoảng 90% thời gian cho việc đọc và bảo trì code có sẵn. Với một khoảng thời gian dài như vậy, việc quan trọng nhất là phải đảm bảo tất cả mọi thứ bạn làm phải thật chính xác và hiệu quả. Meta programming với Ruby thực sự rất mạnh, tuy nhiên việc sử dụng meta programming có thể làm code của chúng ta khó đọc và làm tăng chi phí bảo trì. Vì vậy, bài viết này muốn chia sẻ những sai lầm bạn có thể mắc phải trong project của mình.
Sai lầm 1: Sử dụng method_missing
như một lựa chọn đầu tiên
Paolo Perrotta, tác giả của cuốn Metaprogramming Ruby nói:
The method_missing() is a chainsaw: it’s powerful, but it’s also potentially dangerous.
Tạm dịch: **method_missing() ** là một cưa máy, nó rất mạnh mẽ nhưng cũng tiềm tàng nguy hiểm
Vậy làm thế nào để sử dụng chúng một cách tốt nhất ?
Chúng ta sẽ sử dụng method_missing
để cài đặt Null_object_pattern , mà class gồm 1 method xử lý các 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
Chúng ta có 2 lựa chọn, dùng define_method
hoặc method_missing
, để đánh giá, ta sẽ sử dụng benchmark
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)
Với kết quả này, chúng ta có thể thấy define_method
nhanh hơn 10 lần so với method_missing
Trong trường hợp này giữa việc linh hoạt trong việc xử lý tiền tệ và hiệu năng của code thì chúng ta nên chọn define_method
sẽ tốt hơn.
Sai lầm 2: Không overriding respond_to_missing
Bạn phải override method respond_to_missing?
mỗi khi bạn override method_missing
. Nếu bạn làm việc với Rails, bạn sẽ quen với việc kiểu tra biến môi trường bằng cách sử dụng:
Rails.new.production?
thay cho Rails.env == ‘production’
Hãy cùng xem cách cài đặt 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
Bạn có thể thấy điều kiện để sử dụng method_missing
giống như trong respond_to_missing?
.
Đó chính là vấn đề. Nếu bạn không override respond_to_missing?
, object sẽ không đáp ứng với bất kỳ method tạo động nào.
Sai lầm 3: Quên xử lý các trường hợp unknown
Trong ví dụ trước, bạn thấy Rails sử dụng super
để call các method mà không biết phải xử lý như thế nào. Trong class StringInquirer
, nếu method không kết thúc bằng ?
, nó cho phép call lên super.
Nếu bạn không dự phòng để super
, nó có thể gây ra lỗi mà rất khó để traking. Hãy nhớ method_missing
là nơi bug bị ẩn đi.
Sai lầm 4: Sử dụng define_method
khi không cần thiết
Ta cùng xem ví dụ từ Restclient gem . Trong file bin/restclient
ta 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
Đây là một cách viết sai lầm. Bởi bạn đang phải hy sinh khả năng đọc và hiểu mã mà không đạt được bất kỳ điều gì. Danh sách các HTTP verbs là cố định (bởi nó hầu như không thay đổi). Nhưng khi sử dụng meta programming, bạn đã tăng độ phức tạp bằng các phương pháp tự động xác định HTTP verbs => Tôi cho rằng ở đây không cần sử dụng meta programming.
Sai lầm 5: Thay đổi ngữ nghĩa khi open class
Bạn nên kiểm tra trước, nếu method đã tồn tại trước khi bạn open class có sẵn và thêm method mới.
Nếu bạn không kiểm tra, bạn sẽ thay đổi ngữ nghĩa của method đã tồn tại => nó sẽ gây ra lỗi
Vì vậy việc kiểm tra là cần thiết để giảm bớt ảnh hưởng xấu tới namspace global.
Một ví dụ cho việc này là gem JSON, nó tích hợp vào các lớp Range
, Rational
, Symbol
... method to_json
.
Sai lầm 6: Phụ thuộc sai hướng
Trong một kiến trúc theo layered , layer 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, các thư viện layer trên phải không được tái sử dụng lại nó được. Nếu thư viện layer trên có thể tái sử dụng được layer dưới thì ta gọi đó là phụ thuộc sai hướng.
Phụ thuộc sai hướng này sẽ dễ gặp phải nếu chúng ta sử dụng meta programming ở thư viện ở tầng thấp.
Sai lầm 7: Quá nhiều cấp nesting
Sử dụng method programming trong code buộc client sử dụng quá nhiều block nested. Ví dụ khi ta sử dụng Rspec
# 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
Có nhiều cấp nested làm tăng bối cảnh và khiến đoạn code khó hiểu hơn. Cách viết tốt là khiến code đơn giản và dễ hiểu hơn, một ví dụ tốt cho việc này đó là cách validation của ActiveModel
class Person
include ActiveModel::Validations
attr_accessor :name
validates_presence_of :name
end
Hoặc các viết 1 cấp nested như trong routes.rb
resources :articles do
resources :comments
end
Kết luận
Meta programming vô cùng giá trị, nó có thể giải quyết những vấn đề phức tạp dễ dàng hơn. Tuy nhiên nó chỉ có giá trị khi có được sự cân bằng giữa việc viết ít code và viết dễ đọc.
Tham khảo
https://www.codeschool.com/blog/2015/04/24/7-deadly-sins-of-ruby-metaprogramming/
All rights reserved