Ruby Metaprogramming - define_method

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_evalclass_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_evalclass_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