Methods trong ruby
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à ...
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.