+1

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ù 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.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí