Cái giá phải trả cho Metaprogramming
Bài đăng này đã không được cập nhật trong 3 năm
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 đã từng nghe hoặc sử dụng nó rồi. Metaprogramming là phương pháp viết code động, những đoạn code được sinh ra không phải do lập trình viên 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 số trường hợp.
Nhưng nhiều người lại không nhận ra rằng, việc sử dụng Metaprogramming cũng có cái giá của nó, khiến chúng ta phải đánh đổi. (Em cũng thế :v)
Trong Rails, nếu dùng Metaprogramming, ta sẽ sử dụng một số hàm kiểu như:
- Thay đổi 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 chúng 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).
- Khả năng tìm kiếm (searchability).
Ngoài ra, 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 làm điều đó bằng tay.
Ok, giờ ta sẽ đi chi tiết vào từng 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 phương pháp thông thường.
Dưới đây là Benchmark so sánh:
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_method
và method_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 hiện 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 chú ý 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ừ phiên bản Ruby 2.3.
Đó là nguyên nhân 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 sử 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.
Chúng ta đã thiếu file name và số dòng code chỉ ra errors.
Tuy nhiên có thể khắc phục điều đó bằng cách thêm 2 parameters vào method eval
:
- File name
- Line number
Sử dụng constant __FILE__
và __LINE__
như parameters cho class_eval
, bạn sẽ có được thông tin 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 lưu ý nếu sử dụng method kiểu như vậy.
3. Khả năng tìm kiếm (Searchability):
Metaprogramming method làm code của bạn khó tìm hơn, khó truy cập tới và khó để debug.
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 định nghĩa thông qua metaprogramming, đặc biệt khó nếu tên method được build run-time.
Ví dụ ta viết 3 methods sử 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 đó sử 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 theo 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 sử 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
Ngoài ra nếu bạn có sử dụng những tool như grep
, ack
, hoặc editor để search ra nơi method được định nghĩa, 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 sử dụng chúng đúng trường hợp, 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 lựa chọn tốt nhất.
Nguồn:
All rights reserved