12/08/2018, 13:01

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.

0