Ruby Metaprogramming - define_method
Bài đăng này đã không được cập nhật trong 2 năm
Chào các bạn, trong bài viết trước mình có giới thiệu về metaprogramming trong ruby và cách sử dụng hàm eval
. Để tiếp nối chủ đề về metaprogramming trong ruby, bài viết này mình sẽ đề cập đến define_method
và một số ứng dụng của nó.
Trước khi bắt đầu, mình có một ví dụ nhỏ về class:
class Dog
def initialize name
.@name = name
end
def is_dog?; true; end
end
Ở đây mình có một class Dog - còn gọi là chó. Mình có một con chó, tên nó là "mic". Để tạo ra "mic", ta sẽ gõ:
a = Dog.new "mic"
a.is_dog?
=> true
Như bạn đã thấy, mình đã tạo ra một con chó dog1 có tên là mic, nhưng mình lại không có phương thức name trong class Dog, mình chỉ biết có một biến instance có tên là name được gán cho con chó của mình. Vậy làm thế nào mình có thể biết được tên của con chó mình đã tạo ra? Câu trả lời là nhờ vào phương thức instance_eval
:
dog1.instance_eval{@name}
=> "mic"
Vậy là ta đã có thể biết được tên của con chó, nhưng câu lệnh có vẻ hơi dài và khá là khó hiểu. Mình sẽ khai báo một phương thức ngắn gọn hơn để lấy được tên của con "mic" của mình:
dog1.instance_eval do
def name
.@name
end
end
dog1.name
=> "mic"
dog1.define_singleton_method :name do
.@name
end
dog1.name
=> "mic"
dog2.name
=> NoMethodError: undefined method `name' for...
Và ta đã có phuơng thức name cho con chó của mình. Nhưng, nếu mình có thêm một con chó khác, mình sẽ không thể dùng phương thức name mà mình đã khai báo cho con "mic" để dùng cho con "join":
dog2 = Dog.new "join"
dog2.is_dog?
=> true
dog2.instance_eval{@name}
=> "join"
dog2.name
=> NoMethodError: undefined method `name' for...
Vậy chúng ta không thể dùng instance_eval
để khai báo phương thức dùng chung cho nhiều đối tượng của class Dog được. Phương thức này sẽ rất hữu ích nếu ta cần khai báo phương thức riêng cho một đối tượng cụ thể, nhưng nếu cần khai báo phương thức dùng cho nhiều đối tượng cụ thể, có lẽ ta sẽ cần một công cụ như class_eval
:
Dog.class_eval do
def name
.@name
end
end
dog1.name
=> "mic"
dog2.name
=> "join"
Như vậy, nhờ vào 2 phương thức instance_eval
và class_eval
, ta đã có thể khai báo các phương thức chỉ áp dụng cho một đối tượng duy nhất và các phương thức áp dụng được cho nhiều đối tượng. Nhưng, có một phương thức khác có thể được sử dụng thay thế cho 2 phương thức mình vừa nêu; đó là define_method
.
class Dog
[...]
def define_name
define_method "name" do
.@name
end
end
define_method "kick" do
"My " << .@name
end
end
dog1.name
=> NoMethodError: undefined method `name` for ...
dog1.define_name
dog1.name
=> "join"
dog1.kick
=> "My mic"
dog2.kick
=> "My join"
Dog.send(:define_method, :name) do
.@name
end
Khác với instance_eval
và class_eval
là 2 public method
, define_method
là private method. Do đó, ta chỉ có thể sử dụng nó bên trong class khai báo nó, Trong ví dụ bên trên, mình đã khai báo phương thức kick bằng define_method
, thay vì sử dụng def kick; "My " << @name; end
như bình thường. Và mình cũng đã tạo ra một phương thức define_name
. Chỉ những con chó đã được gọi define_name
mới có phương thức name
. Khi chỉ muốn sử dụng một phương thức cho một số đối tượng nhất định, ta khai báo phương thức đó thông qua một phương thức khác. Nhưng tại sao lại sử dụng define_method
mà không phải cách khai báo hàm truyền thống?
Câu trả lời là ở tính linh hoạt của nó:
Với define_method
, chúng ta có thể khai báo các phương thức với tên gọi tùy ý, tùy vào bối cảnh sử dụng. Đây là điều mà cách khai báo truyền thống không thể làm được. Mặt khác, define_method
còn giúp chúng ta "tiết kiệm" số dòng code. Ví dụ:
# Thay vì phải viết
def a
@a
end
def b
@b
end
def c
@c
end
...
# Ta có thể viết
%w(a b c ...).do |x|
define_method x do
instance_variable_get "@#{x}"
end
end
Tóm lại, define_method
như một cải tiến của eval
, cho phép chúng ta động hóa chương trình mà vẫn đảm bảo một phần nào sự an toàn của nó.
Ngoài việc các phương thức được khai báo bằng define_method
lâu hơn một chút so với khai báo bằng def
thông thường và việc sử dụng define_method
có thể gây khó hiểu cho code của chúng ta thì mọi thứ khác hầu như đều ổn. Chúng ta có thể sử dụng define_method
trong một số trường hợp để tiết tiệm chút thời gian viết code, đơn giản nhất là trong ví dụ bên trên.
All rights reserved