Cái giá phải trả cho Metaprogramming

I. Lời nói đầu:

Xin chào các bác.

Đến hẹn lại lên, bài viết hôm nay sẽ chia sẻ về Metaprogramming. (dance2)

Chắc hẳn các bạn đã đã có lần nghe hoặc dùng nó rồi.
Metaprogramming là cách thức viết code động, những đoạn code được sinh ra không phải do coder viết từ đầu đến cuối, mà nó được dựa trên những đoạn string, variable, data …

Nghe thì có vẻ rất hay và pro, nhưng nó có thực sự tốt không?

Metaprogramming có thể rất hữu dụng cho một vài tình thế.

Nhưng nhiều người lại không nhận ra rằng, việc dùng Metaprogramming cũng có cái giá của nó, khiến ta phải đánh đổi. (Em cũng thế :v)

Trong Rails, nếu dùng Metaprogramming, ta sẽ dùng một vài hàm kiểu như:

đổi khác cấu trúc code – define_method Chạy một string như thể nó là đoạn Ruby code – instance_eval Phản ứng đối với event – method_missing

Vậy ta phải đánh đổi những gì cho Metaprogramming? Tôi gạch ra 3 cái đầu dòng như sau:

Tốc độ (speed). Tính dễ đọc (readability). tuyệt kỹ tìm kiếm (searchability).

thêm nữa, nếu tính cả method eval bạn sẽ có gạch đầu dòng thứ 4 – tính bảo mật (security). Vì bản thân method đó không có một chút gì để check security những thứ chạy bên trong, mà bạn phải hành động điều đó bằng tay.

Ok, giờ ta sẽ đi cụ thể vào đã có lần mục một. (honho)

II. Costs

1. Tốc độ (speed):

Cái giá phải trả đầu tiên của Metaprogramming chính là tốc độ, các methods của Metaprogramming sẽ chạy chậm hơn những method khai báo theo cách thức thông thường.

sau đây là Benchmark đối chiếu:

require 'benchmark/ips'   class Thing   def method_missing(name, *args)   end     def normal_method   end     define_method(:speak) {} end   t = Thing.new   Benchmark.ips do |x|   x.report("normal method")  { t.normal_method }   x.report("missing method") { t.abc }   x.report("defined method") { t.speak }     x.compare! end 

Và kết quả từ Ruby 2.2.4

normal method:   7344529.4 i/s defined method:  5766584.9 i/s - 1.34x  slower missing method:  4777911.7 i/s - 1.54x  slower 

Bạn có thể thấy cả 2 method của metaprograming (ở đây là define_methodmethod_missing) đều chạy chậm hơn normal_method một chút.

Còn một điều nữa tôi phát giác ra.

Kết quả bên trên là chạy với Ruby 2.2.4, nhưng nếu bạn thử benchmark trên Ruby 2.3 hoặc 2.4 thì những methods đó còn chạy chậm hơn nữa.

Ruby 2.4 benchmark:

normal method:   8252851.6 i/s defined method:  6153202.9 i/s - 1.39x  slower missing method:  4557376.3 i/s - 1.87x  slower 

Tôi đã chạy thử benchmark nhiều lần để chắc rằng không phải do máy bị choke (yaoming).

Nhưng nếu bạn quan tâm và nhìn vào iterations per second (i/s) thì có vẻ như method bình thường chạy nhanh hơn kể từ version Ruby 2.3.

Đó là Tại sao mà method_missing trông có vẻ chạy chậm hơn.

2. Tính dễ đọc (Readability):

Error messages có thể khá phế nếu ta dùng method instance_eval hoặc class_eval

Hãy thử nhìn vào đoạn code sau đây:

class Thing   class_eval("def self.foo; raise 'something went wrong'; end") end  Thing.foo 

Kết quả sẽ dẫn đến lỗi sau:

(eval):1:in 'foo': 'something went wrong...' (RuntimeError) 

Nhìn vào cái errors này bắn ra, cảm giác có cái gì đó thiếu thiếu :v.

ta đã thiếu file name và số dòng code chỉ ra errors.

mặc dù vậy có thể giải quyết điều đó bằng cách thêm 2 parameters vào method eval:

File name Line number

dùng constant __FILE____LINE__ như parameters cho class_eval, bạn sẽ có được tin tức chính xác của error message.

class Thing   class_eval(     "def foo; raise 'something went right'; end",     __FILE__,     __LINE__   ) end 

Sao mấy cái parameters này không phải là default mà mình lại phải xử lý bằng tay?

… Chịu, nhưng đó là điều bạn cần quan tâm nếu dùng method kiểu như vậy.

3. tuyệt kỹ tìm kiếm (Searchability):

Metaprogramming method làm code của bạn khó tìm hơn, khó vào web tới và khó để sửa lỗi.

Nếu bạn đang cần tìm method vừa gọi, bạn sẽ không thể dùng CTRL + F để tìm tới method được được hiểu thông qua metaprogramming, đặc sắc khó nếu tên method được build run-time.

Ví dụ ta viết 3 methods dùng metaprogramming:

class RubyBlog   def create_post_tags     types = ['computer_science', 'tools', 'advanced_ruby']       types.each do |type|       define_singleton_method(type + "_tag") { puts "This post is about #{type}" }     end   end end   rb = RubyBlog.new   rb.create_post_tags rb.computer_science_tag 

Những tool để generate document như kiểu Yard hay RDoc sẽ không thể tìm và list ra những method đó.

Các tool đó dùng kỹ thuật gọi là Static Analysis để tìm class và method.

Kỹ thuật này chỉ có thể tìm method được viết trực tiếp nữa cách thông thường.

Hãy thử chạy yard doc với cái ví dụ trên, bạn sẽ thấy nó chỉ tìm ra method là create_post_tags

Cũng có 1 cách để yard có thể in ra những methods kia, bằng cách dùng @method tag, nhưng không phải lúc nào cũng áp dụng được.

class Thing   # @method build_report   define_method(:build_report) end 

thêm nữa nếu bạn có dùng những tool như grep, ack, hoặc editor để search ra nơi method được được hiểu, việc tìm metaprogramming methods cũng khó hơn methods thông thường là cái chắc.

Như creator của Sidekiq – Mike Perham đã nói:

I don’t think Sidekiq uses any metaprogramming at all because I find it obscures the code more than it helps 95% of the time.

Túm cái váy lại

Metaprogramming không phải lúc nào cũng tệ. Nó sẽ cực kỳ hữu dụng nếu bạn dùng chúng đúng tình thế, giúp đỡ code của bạn gọn và linh hoạt hơn nhiều.

Chỉ cần nhận ra những nhược điểm của nó, bạn sẽ đưa ra được chọn lựa tốt nhất.

Nguồn:

http://www.blackbytes.info/2017/06/costs-of-metaprogramming/

Nguồn viblo.asia