Methods trong ruby
Bài đăng này đã không được cập nhật trong 9 năm
Trong phần trước chúng ta đã tìm hiều qua về object trong ruby cũng như các khái niệm cơ bản về instance variable, method, module... Trong phần này chúng ta sẽ tập trung vào vấn đề trùng lặp code và cách xử lý với việc dùng phương thức động và method_missing.
1, Trùng lặp code
Trùng lặp code là vấn đề chúng ta hay gặp phải khi lập trình. Để hiểu rõ hơn vấn đề này chúng ta xét một class có tên là DS(data source) với những phương thức của nó
class DS
def initialize # connect to data source...
def get_mouse_info(workstation_id) # ...
def get_mouse_price(workstation_id) # ...
def get_keyboard_info(workstation_id) # ...
def get_keyboard_price(workstation_id) # ...
def get_cpu_info(workstation_id) # ...
...
end
Trong đó, phương thức DS#initialize( ) được dùng để kết nối với hệ thống dư liệu khi chúng ta khởi tạo một đối tương của class DS. Các phương thức khác giúp chúng ta có thể lấy thông tin của máy tính đang sử dụng. chúng ta có thể thử trên mành hình lệnh irb
ds = DS.new
ds.get_cpu_info(42) # => 2.16 Ghz
ds.get_cpu_price(42) # => 150
ds.get_mouse_info(42) # => Dual
như vậy mỗi máy tính của chúng ta sẽ được coi là một đối tượng, mỗi đối tượng sẽ có các phương thức đơn để lấy thông tin cho mỗi thành phần của máy tính và ở đây chúng ta cần là lây thông tin và giá của mỗi thành phần trong máy tính.
Chúng ta có một class tên là Computer với 3 phương thức
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def keyboard
info = @data_source.get_keyboard_info(@id)
price = @data_source.get_keyboard_price(@id)
result = "Keyboard: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
Nếu như chúng ta tiếp tục phát triển class Computer theo hướng như trên thì chúng ta sẽ có một danh sách dài các phương thức, việc này sẽ dẫn đến chúng ta phải viết test cho từng phương thức, và sẽ dẫn đến sự trùng lặp code và buồn tẻ.
Để xử lý vấn để trên chúng ta có 2 phương pháp giải quyết sử dụng Dynamic Methods(phương thức động) và phương thức đặc biệt method_missing().
- Dynamic Methods
Khi gọi một phương thức của một đối tượng, chúng ta thường sử dụng dấu .
sau đối tượng để gọi các phương thức.
class MyClass
def my_method(my_arg)
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3) # => 6
Nhưng chúng ta cũng thể dùng Object#send() để gọi một phương thức của đối tượng.
obj.send(:my_method, 3) #> 6
thành phần đầu tiên của send() là tên của phương thức chúng ta muốn gọi, có thế là chuỗi hay symbol, sau đó là giá trị của các thành phần tương ứng trong phương thức.
Định nghĩa phương thưc động
Chúng ta có thể định nghĩa một phương thức động với việc sử dụng Module#define_method()
cùng với một block.
class MyClass
define_method :my_method do |my_arg|
my_arg * 3
end
end
obj = MyClass.new
obj.my_method(2) # => 6
define_method()
được khai báo bên trong MyClass vì vậy my_method() sẽ được coi như là một instance method của class MyClass. Như vậy chúng ta đã học được cách dùng Module#define_method() thay thế từ khoá def khi định nghĩa một phương thức mới và cách gọi phương thức bằng sent() thay thế cho dấu .
. Vậy bây giờ chúng ta bắt đầu refactor class Computer ở trên.
Bước 1: Thêm đường dẫn động khi gọi các phương thức
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def component(name)
info = @data_source.send "get_#{name}_info" , @id
price = @data_source.send "get_#{name}_price" , @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
Bước 2: Sử dụng define_method() để khai báo các phương thức
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) {
info = @data_source.send "get_#{name}_info" , @id
price = @data_source.send "get_#{name}_price" , @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
}
end
define_component :mouse
define_component :cpu
define_component :keyboard
end
Bước 3: Refactor các trùng lặp còn lại
Ở code trên, chúng ta vẫn thấy có sự trùng lặp code về việc khai báo các define_component, do vậy chúng ta sẽ di chuyển việc khai báo này báo này vào trong hàm initialize
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
end
def self.define_component(name)
define_method(name) {
info = @data_source.send "get_#{name}_info" , @id
price = @data_source.send "get_#{name}_price" , @id
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
}
end
end
Với việc sử dụng data_source.methods
chúng ta sẽ dựa vào các phương thức đã có trong data_source để khai báo các phương thức tương tự trong class Computer. ví dụ như trong data_source có phương thức là get_cpu_info()
như vậy chúng ta sẽ có phương thức cpu trong class Computer.
- Method_missing
Như chúng ta đã biết, trong ruby chúng ta có thể gọi một phương thức không tồn tại, và sẽ dẫn đến phương thức method_missing để thông báo lỗi không tồn tại phương thức đã gọi.
class Lawyer; end
nick = Lawyer.new
nick.talk_simple
NoMethodError: undefined method ‘talk_simple' for #<Lawyer:0x3c848> [...]
mà nếu cần thiết chúng ta có thể viết đè phương thức này để ra kết quả mà chúng ta mong muốn
class Lawyer
def method_missing(method, *args)
puts "You called: #{method}(#{args.join(', ')})"
puts "(You also passed it a block)" if block_given?
end
end
bob = Lawyer.new
bob.talk_simple('a' , 'b' ) do
# a block
end
)
You called: talk_simple(a, b)
(You also passed it a block)
Với đặc điểm của phương thức method_missing()
sự trùng lặp code trong class Computer sẽ được sửa như sau
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name, *args)
super if !@data_source.respond_to?("get_#{name}_info" )
info = @data_source.send("get_#{name}_info" , args[0])
price = @data_source.send("get_#{name}_price" , args[0])
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
Vậy cơ chế làm việc của đoạn code trên sẽ là thế nào? Khi chúng ta gọi một phương thức Computer#mouse()
, do phương thức này không tồn tại lên nó sẽ đi vào phương thức method_missing() mà chúng ta viết ở trên. Nếu phương thức get_mouse_info()
cũng không tồn tại trong Class DS thì chúng ta sẽ đi vào Kernel#method_missing()
và xuất hiện thông báo lỗi NoMethodError
. Nếu phương thức đã được chuyển đổi từ name
chúng ta truyền vào tồn tại, thì chúng ta sẽ lấy được thông tin mà chúng ta muốn.
All rights reserved