Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Đường Tra Cứu Phương Thức Của Ruby: Cách Giải Quyết Các Phương Thức

Bạn nghĩ gì xảy ra khi bạn gọi một phương thức? Ruby quyết định gọi phương thức nào khi có một phương thức khác cùng tên như thế nào? Bạn từng tự hỏi phương thức được lưu trữ hoặc nguồn gốc từ đâu chưa?

Ruby sử dụng một "cách" hoặc "mẫu" được định nghĩa để xác định phương thức đúng để gọi và thời điểm thích hợp để trả về lỗi "no method error", và chúng ta có thể gọi "cách" đó là Đường Tra Cứu Phương Thức Của Ruby. Trong hướng dẫn này, chúng ta sẽ đi sâu vào việc tra cứu phương thức của Ruby. Cuối cùng, bạn sẽ hiểu rõ cách Ruby duyệt qua hệ thống phân cấp của một đối tượng để xác định phương thức bạn đang tham chiếu.

Để nắm vững những gì chúng ta sẽ học, bạn cần có hiểu biết cơ bản về Ruby. Mặc dù chúng ta sẽ nhắc đến module và lớp, nhưng đây không phải là một bài đi sâu vào chúng. Chúng ta chỉ bao phủ chiều sâu cần thiết để đạt được mục tiêu của hướng dẫn này: chỉ cho bạn thấy Ruby xác định thông điệp (phương thức) bạn truyền cho một đối tượng như thế nào.

Tổng Quan

Khi bạn gọi một phương thức, chẳng hạn như first_person.valid?, Ruby phải xác định một vài điều:

  1. Nơi phương thức .valid? được định nghĩa.
  2. Có nhiều nơi định nghĩa phương thức .valid? không? Nếu có, đâu là cái đúng để sử dụng trong ngữ cảnh này.

Quá trình (hoặc đường đi) Ruby tuân theo để tìm ra điều này chính là tra cứu phương thức. Ruby phải tìm nơi phương thức được tạo ra để có thể gọi nó. Nó phải tìm kiếm ở các nơi sau để đảm bảo nó gọi đúng phương thức:

  1. Phương thức singleton: Ruby cung cấp cách để một đối tượng định nghĩa các phương thức riêng; các phương thức này chỉ khả dụng cho đối tượng đó và không thể truy cập bởi một instance của đối tượng.
  2. Phương thức trong các module được mixin: Module có thể được mixin vào một lớp sử dụng prepend, include, hoặc extend. Khi điều này xảy ra, lớp có quyền truy cập vào các phương thức được định nghĩa trong các module, và Ruby đi vào các module để tìm kiếm phương thức đã được gọi. Cũng quan trọng là biết rằng các module khác có thể được mixin vào các module ban đầu, và tìm kiếm cũng tiến triển vào những module đó.
  3. Phương thức instance: Đây là các phương thức được định nghĩa trong lớp và có thể truy cập bởi các instance của lớp đó.
  4. Phương thức lớp cha hoặc module: Nếu lớp碰巧 là con của một lớp khác, Ruby tìm kiếm trong lớp cha. Tìm kiếm đi vào phương thức singleton của lớp cha, các module được mixin, và lớp cha của nó.
  5. Object, Kernel và BasicObject: Đây là những nơi cuối cùng Ruby tìm kiếm. Điều này là vì mọi đối tượng trong Ruby đều có chúng như một phần của tổ tiên (ancestors).

Lớp Và Module

Phương thức thường được gọi trên các đối tượng. Các đối tượng này được tạo bởi các lớp nhất định, có thể là lớp tích hợp của Ruby hoặc lớp do nhà phát triển tạo ra.

class Human
  attr_reader :name
  def initialize(name)
    @name = name
  end

  def hello
    puts "Hello! #{name}"
  end
end

Chúng ta có thể gọi phương thức hello mà chúng ta đã tạo ở trên trên các instance của lớp Human; ví dụ:

john = Human.new("John")
john.hello # Output -> Hello John

Phương thức hello là phương thức instance; đó là lý do chúng ta có thể gọi nó trên các instance của lớp Human. Có thể có trường hợp chúng ta không muốn phương thức được gọi trên các instance. Trong những trường hợp này, chúng ta muốn gọi phương thức trên chính lớp. Để đạt được điều này, chúng ta phải tạo một phương thức lớp (class method). Định nghĩa phương thức lớp cho lớp trên sẽ trông như sau:

def self.me
  puts "I am a class method"
end

Chúng ta có thể gọi nó bằng cách làm Human.me. Khi độ phức tạp của ứng dụng tăng lên (hãy tưởng tượng chúng ta đang xây dựng một startup mới), có thể đến lúc hai hoặc nhiều lớp của chúng ta có nhiều phương thức làm cùng một việc. Nếu điều này xảy ra, nghĩa là chúng ta cần giữ cho code khô ráo (DRY) và đảm bảo không lặp lại chính mình. Vấn đề liên quan đến cách chúng ta chia sẻ chức năng trên các lớp này.

Nếu bạn chưa từng dùng module, bạn có thể muốn tạo một lớp mới nghiêm ngặt cho các phương thức "chia sẻ" này. Tuy nhiên, làm như vậy có thể dẫn đến hậu quả tiêu cực, đặc biệt khi bạn cần sử dụng kế thừa đa thừa (multiple inheritance), thứ mà Ruby không hỗ trợ. Module là phương tiện tốt nhất để xử lý trường hợp này. Module tương tự như lớp, nhưng chúng có một vài khác biệt. Trước tiên, đây là ví dụ về module trông như thế nào:

module Movement
  def walk
    puts "I can walk!"
  end
end
  1. Định nghĩa bắt đầu bằng từ khóa module thay vì class.
  2. Module không thể có instance, vì vậy bạn không thể dùng Movement.new.

Phương Thức

Phương thức có thể được xem như các hành động được thực hiện bởi một đối tượng cụ thể. Nếu tôi có một mảng như [2, 3, 4] được gán cho biến numberList, phương thức .push là một hành động có thể được thực hiện bởi mảng để đưa giá trị nó nhận được vào mảng. Đoạn mã sau là ví dụ:

john.walk

Có thể điển hình của bạn là nói điều gì đó như, "Tôi đang gọi phương thức của đối tượng", trong đó john tham chiếu đến một đối tượng là instance của Human, và walk là phương thức. Tuy nhiên, điều này không hoàn toàn đúng vì phương thức được suy luận có xu hướng đến từ lớp của đối tượng, superclass, hoặc module được mixin.

Quan trọng để thêm rằng có thể định nghĩa một phương thức trên một đối tượng, ngay cả đối tượng như john, vì mọi thứ trong Ruby đều là đối tượng, ngay cả lớp được dùng để tạo đối tượng.

def john.drip
  puts "My drip is eternal"
end

Phương thức drip chỉ có thể truy cập được bởi đối tượng được gán cho john. drip là phương thức singleton sẽ khả dụng cho đối tượng john. Quan trọng là biết rằng không có sự khác biệt giữa phương thức singleton và phương thức lớp, như bạn có thể thấy từ câu trả lời Stack Overflow này. Trừ khi bạn tham chiếu đến một phương thức được định nghĩa trên một đối tượng như ví dụ trên, sẽ không chính xác khi nói phương thức thuộc về một đối tượng cụ thể. Trong ví dụ của chúng ta, phương thức walk thuộc về module Movement, trong khi phương thức hello thuộc về lớp Human. Với sự hiểu này, sẽ dễ dàng hơn để tiến thêm một bước, đó là để xác định chính xác phương thức nào được gọi trên một đối tượng, Ruby phải kiểm tra lớp của đối tượng, superclass, hoặc các module đã được mixin trong hệ thống phân cấp của đối tượng.

Mixing Module (Trộn Module)

Ruby chỉ hỗ trợ đơn kế thừa (single inheritance); một lớp chỉ có thể kế thừa từ một lớp. Điều này làm cho lớp con có thể kế thừa hành vi (phương thức) của một lớp khác. Điều gì xảy ra khi bạn có hành vi cần chia sẻ trên các lớp khác nhau? Ví dụ, để làm cho phương thức walk khả dụng cho các instance của lớp Human, chúng ta có thể mix in module Movement vào lớp Human. Vì vậy, việc viết lại lớp Human sử dụng include sẽ trông như sau:

require "movement" # Giả sử chúng ta có module trong tệp movement.rb
class Human
  include Movement
  attr_reader :name
  def initialize(name)
    @name = name
  end

  def hello
    puts "Hello! #{name}"
  end
end

Bây giờ, chúng ta có thể gọi phương thức walk trên instance:

john = Human.new("John")
john.walk

Include

Khi bạn sử dụng từ khóa include, như chúng ta đã làm ở trên, các phương thức của module được include sẽ được thêm vào lớp như phương thức instance. Điều này là vì module included được thêm vào giữa các ancestors của lớp Human, sao cho module Movement có thể được nhìn thấy như một cha của lớp Human. Như bạn thấy trong ví dụ trên, chúng ta đã gọi phương thức walk trên instance của lớp Human.

Extend

Ngoài include, Ruby cung cấp cho chúng ta từ khóa extend. Điều này làm cho phương thức của module khả dụng cho lớp như phương thức lớp (class method), còn được gọi là phương thức singleton, như chúng ta đã học trước đó. Vì vậy, nếu chúng ta có một module tên Feeding trông như:

module Feeding
  def food
    "I make my food :)"
  end
end

Chúng ta có thể chia sẻ hành vi này trong lớp Human bằng cách require nó và thêm extend Feeding. Tuy nhiên, để sử dụng nó, thay vì gọi phương thức food trên instance của lớp, chúng ta sẽ gọi nó trên chính lớp, giống như cách gọi phương thức lớp.

Human.food

Prepend

Điều này tương tự như include nhưng có một số khác biệt, như được nêu trong bài viết này;

Nó thực sự hoạt động như include, trừ việc thay vì chèn module giữa lớp và superclass của nó trong chuỗi, nó sẽ chèn nó ở dưới cùng của chuỗi, ngay cả trước chính lớp đó.

Điều đó có nghĩa là khi gọi một phương thức trên instance của lớp, Ruby sẽ nhìn vào phương thức của module trước khi nhìn vào lớp.

Nếu chúng ta có một module định nghĩa phương thức hello mà chúng ta sau đó mixin vào lớp Human bằng cách sử dụng prepend, Ruby sẽ gọi phương thức chúng ta có trong module thay vì cái trong lớp.

Để hiểu đúng cách prepend của Ruby hoạt động, tôi đề nghị xem bài viết này.

Đường Tra Cứu Phương Thức (Method Lookup Path)

Nơi đầu tiên trình thông dịch Ruby nhìn khi cố gắng gọi một phương thức là các phương thức singleton. Tôi đã tạo repl này, mà bạn có thể chơi để thấy các kết quả có thể.

Giả sử chúng ta có một đống module và lớp trông như sau:

module One
  def another
    puts "From one module"
  end
end

module Two
  def another
    puts "From two module"
  end
end

module Three
  def another
    puts "From three module"
  end
end

class Creature
  def another
    puts "From creature class"
  end
end

Hãy tiến hành mix chúng vào lớp Human.

class Human < Creature
  prepend Three
  extend Two
  include One
  def another
    puts "Instance method"
  end
  def self.another
    puts "From Human class singleton"
  end
end

Ngoài việc mixin các module, chúng ta có phương thức instance và phương thức lớp. Bạn cũng có thể thấy lớp Human là lớp con của lớp Creature.

Tra Cứu Thứ Nhất - Phương Thức Singleton

Khi chúng ta chạy Human.another, những gì được in ra là From Human class singleton, chính là cái chúng ta có trong phương thức lớp. Nếu chúng ta comment phương thức lớp và chạy lại, nó sẽ in From two module ra console. Điều này đến từ module chúng ta mixin sử dụng extend. Nó cho thấy tra cứu bắt đầu giữa các phương thức singleton. Nếu chúng ta xóa (hoặc comment) extend Two và chạy lệnh lại, điều này sẽ ném ra lỗi method missing error. Chúng ta nhận được lỗi này vì Ruby không tìm thấy phương thức another giữa các phương thức singleton.

Chúng ta sẽ tiến hành sử dụng instance của lớp bằng cách tạo một instance:

n = Human.new

Chúng ta cũng sẽ tạo một phương thức singleton cho instance:

def n.another
  puts "From n object"
end

Bây giờ, khi chúng ta chạy n.another, phiên bản được gọi là phương thức singleton được định nghĩa trên đối tượng n. Lý do Ruby không gọi module được mixin sử dụng extend trong trường hợp này là vì chúng ta đang gọi phương thức trên instance của lớp. Quan trọng là biết rằng phương thức singleton có độ liên quan cao hơn các phương thức liên quan đến module được mixin sử dụng extend.

Tra Cứu Thứ Hai - Module Được Mixin Sử Dụng prepend

Nếu chúng ta comment phương thức singleton trên đối tượng n và chạy lệnh, phiên bản phương thức được gọi là module chúng ta mixin sử dụng prepend. Điều này là vì việc sử dụng prepend chèn module trước chính lớp đó.

Tra Cứu Thứ Ba - Lớp

Nếu chúng ta comment module Three, phiên bản phương thức another được gọi là phương thức instance được định nghĩa trên lớp.

Tra Cứu Thứ Tư - Module Được Mixin Sử Dụng include

Nơi tiếp theo Ruby tìm kiếm phương thức là trong các module đã được mixin sử dụng include. Vì vậy, khi chúng ta comment phương thức instance, phiên bản chúng ta nhận được là cái trong module One.

Tra Cứu Thứ Năm - Lớp Cha

Nếu lớp có lớp cha, Ruby tìm kiếm trong lớp đó. Tìm kiếm bao gồm đi vào các module được mixin vào lớp cha; nếu chúng ta có phương thức được định nghĩa trong một module được mixin vào lớp Creature, phương thức sẽ được gọi.

Chúng ta có thể biết nơi tìm kiếm phương thức kết thúc bằng cách kiểm tra tổ tiên của nó: gọi .ancestors trên lớp. Làm điều này cho lớp Human sẽ trả về [Three, Human, One, Creature, Object, Kernel, BasicObject]. Tìm kiếm phương thức kết thúc tại lớp BasicObject, đây là lớp gốc của Ruby. Mọi đối tượng là instance của một số lớp đều xuất phát từ lớp BasicObject.

Sau khi tìm kiếm phương thức đi qua lớp cha do nhà phát triển định nghĩa, nó đến với:

  • lớp Object
  • module Kernel
  • lớp BasicObject

Phương Thức method_missing

Nếu bạn đã sử dụng Ruby một thời gian, bạn có thể đã gặp NoMethodError, xảy ra khi bạn cố gắng gọi một phương thức không xác định trên một đối tượng. Điều này xảy ra sau khi Ruby đã đi qua tổ tiên của đối tượng và không tìm thấy phương thức được gọi. Thông báo lỗi bạn nhận được được xử lý bởi phương thức method_missing, được định nghĩa trong lớp BasicObject. Có thể ghi đè phương thức này cho đối tượng bạn đang gọi phương thức, bạn có thể tìm hiểu bằng cách kiểm tra điều này.

Kết Luận

Bây giờ bạn biết đường đi Ruby thực hiện để tìm ra phương thức được gọi trên một đối tượng. Với sự hiểu này, bạn nên có thể dễ dàng sửa các lỗi phát sinh do gọi phương thức không xác định trên một đối tượng.