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

Tránh Các Cạm Bẫy Phổ Biến Khi Lồng Module Ruby

Module (và class) được thiết kế để lồng nhau. Các đoạn mã như ActiveRecord::RecordNotFound phổ biến đến mức chúng ta không nghĩ hai lần về chúng. Nhưng chôn sâu trong việc triển khai lồng của Ruby - và hệ thống autoload của Rails - là một vài cạm bẫy có thể khiến mã của bạn thất bại theo những cách kỳ lạ và tuyệt vời. Trong bài này, chúng ta sẽ thảo luận nguồn gốc của các cạm bẫy này và cách bạn có thể tránh chúng.

Hằng là gì?

Bài này nói về module, nhưng để hiểu chúng chúng ta cần hiểu hằng. Trong hầu hết các ngôn ngữ, hằng chỉ dùng để lưu trữ các mẩu dữ liệu nhỏ, như trong ví dụ dưới đây:

# Đây là các hằng đơn giản
MAX_RETRIES = 5
DEFAULT_LANGUAGE = "en"

Nhưng trong Ruby, class và module cũng là hằng. Tôi đã viết một ví dụ nhỏ để chứng minh điều này. Có ba hằng trong module dưới đây: một số, một class và một module lồng. Khi bạn truy cập một class hoặc module lồng, Ruby tìm nó bằng cách sử dụng cùng quy tắc nó dùng với hằng số đơn giản.

module MyModule
  MY_FAVORITE_NUMBER = 7
  # Class là hằng
  class MyClass
  end
  # Module cũng vậy
  module MyEmbeddedModule
  end
end
puts MyModule.constants.inspect
# => [:MY_FAVORITE_NUMBER, :MyClass, :MyEmbeddedModule]

Thường thì module có quyền truy cập vào các hằng được định nghĩa trong parent của chúng. Đó là lý do bạn có thể viết mã như sau:

module X
  MARCO = "polo"
  module Y
    def self.n
      puts MARCO
    end
  end
end
X::Y.n() # => "polo"

Nhưng bạn sẽ sai nếu nghĩ rằng mối quan hệ parent/child này là thứ cho phép Y truy cập hằng MARCO của X.

Một vấn đề phổ biến

Nếu chúng ta viết lại mã trên theo cách hơi khác, một điều bất ngờ xảy ra. Y không còn truy cập được X::MARCO. Chuyện gì đang xảy ra ở đây?

module A
  MARCO = "polo"
end
module A::B
  def self.n
    puts MARCO # => uninitialized constant A::B::MARCO (NameError)
  end
end
A::B.n()

Rất thú vị, "kế thừa" hằng của parent bởi child không phải do mối quan hệ parent/child. Nó là lexical. Nghĩa là nó dựa trên cấu trúc của mã bạn, không phải trên cấu trúc của các đối tượng mã của bạn đang xây dựng.

Kiểm tra nesting

Nếu bạn muốn hiểu sâu hơn về cách Ruby tìm kiếm các hằng lồng, đáng giá để xem hàm Module.nesting.

Hàm này trả về một mảng các đối tượng tạo nên "đường dẫn tìm kiếm" cho hằng trong một scope cho trước. Hãy xem xét nesting cho các ví dụ trước của chúng ta.

Trong ví dụ đầu tiên, chúng ta thấy nesting là [A::B, A]. Nghĩa là nếu chúng ta dùng hằng MARCO, Ruby sẽ tìm nó trước trong A::B, sau đó trong A.

module A
  MARCO = "polo"
  module B
    def self.n
      puts Module.nesting.inspect # => [A::B, A]
      puts MARCO # => "polo"
    end
  end
end

Trong ví dụ thứ hai, chúng ta thấy nesting chỉ bao gồm A::B, không phải A. Mặc dù B là "con" của A, cách tôi viết mã không cho thấy chúng lồng nhau, vì vậy cho mục đích này chúng cũng không tồn tại.

module A
  MARCO = "polo"
end
module A::B
  def self.n
    puts Module.nesting.inspect # => [A::B]
    puts MARCO # => uninitialized constant A::B::MARCO (NameError)
  end
end

Phức tạp hóa từ autoload của Rails

Bạn có bao giờ nhận thấy bạn không phải include file khi dùng Rails? Nếu bạn muốn dùng một model, bạn chỉ dùng nó.

Điều này có thể vì Rails triển khai một hệ thống autoload. Nó dùng Module.const_missing để phát hiện khi bạn thử tham chiếu một hằng chưa được load. Sau đó nó load các file nó tin rằng nên chứa hằng đó. Điều này hoạt động hầu hết thời gian, nhưng có một cái bẫy.

Rails giả định một module luôn có nesting lớn nhất có thể. Nó giả định module A::B::C sẽ có nesting là [A::B::C, A::B, A]. Nếu không, bạn sẽ nhận được hành vi bất ngờ.

Trong mã dưới đây, module B không nên truy cập được A::MARCO. Trong Ruby bình thường, nó không thể vì nesting của nó là [A::B]. Vì vậy bạn nên nhận được exception. Nhưng autoload của Rails không ném exception. Thay vào đó, nó trả về A::MARCO.

# a.rb
module A
  MARCO = "polo"
end
# a/b.rb
module A::B
  def self.n
    puts MARCO # => "polo"
  end
end
# some_controller.rb
A::B.n()

Kết luận?

Tất cả điều này nhiều để suy nghĩ. Tôi thích tránh suy nghĩ nơi có thể, vì vậy tôi cố gắng tránh cú pháp module A::B. Tôi không nghĩ ra trường hợp nào tôi muốn thao tác module nesting một cách ý định. Nếu bạn biết, tôi rất muốn nghe!