Đây là câu chuyện đầy đủ, giải thích các khái niệm lập trình siêu ứng dụng cần thiết để hiểu tại sao việc bao gồm mô-đun hoạt động theo cách nó hoạt động trong Ruby.
Điều gì xảy ra khi một mô-đun được bao gồm?
Việc bao gồm một mô-đun vào một lớp sẽ thêm mô-đun vào tổ tiên của lớp. Bạn có thể xem tổ tiên của bất kỳ lớp hoặc mô-đun nào bằng cách gọi ancestors
phương thức của nó :
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
# ^ look, it's right here!
Khi bạn gọi một phương thức trên một thể hiện của C
, Ruby sẽ xem xét mọi mục trong danh sách tổ tiên này để tìm một phương thức thể hiện với tên đã cho. Vì chúng ta đã đưa M
vào C
, M
bây giờ là tổ tiên của C
, nên khi chúng ta gọi foo
một phiên bản của C
, Ruby sẽ tìm thấy phương thức đó trong M
:
C.new.foo
#=> "foo"
Lưu ý rằng việc bao gồm không sao chép bất kỳ trường hợp hoặc phương thức lớp nào vào lớp - nó chỉ thêm một "ghi chú" vào lớp mà nó cũng sẽ tìm kiếm các phương thức thể hiện trong mô-đun được bao gồm.
Điều gì về các phương thức "lớp" trong mô-đun của chúng tôi?
Bởi vì việc bao gồm chỉ thay đổi cách các phương thức thể hiện được gửi đi, bao gồm một mô-đun vào một lớp chỉ làm cho các phương thức thể hiện của nó khả dụng trên lớp đó. Các phương thức "lớp" và các khai báo khác trong mô-đun không được tự động sao chép vào lớp:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
#=> "bar"
C.new.instance_method
#=> "foo"
C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class
Ruby thực hiện các phương thức lớp như thế nào?
Trong Ruby, các lớp và mô-đun là các đối tượng thuần túy - chúng là các thể hiện của lớp Class
và Module
. Điều này có nghĩa là bạn có thể tự động tạo các lớp mới, gán chúng cho các biến, v.v.:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Cũng trong Ruby, bạn có khả năng xác định cái gọi là phương thức singleton trên các đối tượng. Các phương thức này được thêm vào dưới dạng các phương thức thể hiện mới vào lớp singleton đặc biệt, ẩn của đối tượng:
obj = Object.new
# define singleton method
def obj.foo
"foo"
end
# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]
Nhưng không phải các lớp và mô-đun cũng chỉ là các đối tượng đơn thuần? Trong thực tế, họ là! Điều đó có nghĩa là họ cũng có thể có các phương thức singleton? Có, nó có! Và đây là cách các phương thức lớp được sinh ra:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Hoặc, cách phổ biến hơn để xác định một phương thức lớp là sử dụng self
trong khối định nghĩa lớp, tham chiếu đến đối tượng lớp đang được tạo:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Làm cách nào để đưa các phương thức lớp vào một mô-đun?
Như chúng ta vừa thiết lập, các phương thức lớp thực sự chỉ là các phương thức thể hiện trên lớp singleton của đối tượng lớp. Điều này có nghĩa là chúng ta chỉ có thể đưa một mô-đun vào lớp singleton để thêm một loạt các phương thức của lớp? Có, nó có!
module M
def new_instance_method; "hi"; end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
self.singleton_class.include M::ClassMethods
end
HostKlass.new_class_method
#=> "hello"
Đây self.singleton_class.include M::ClassMethods
dòng không trông rất thoải mái, vì vậy của Ruby thêm Object#extend
, mà không giống nhau - tức là bao gồm một module vào lớp singleton của đối tượng:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Di chuyển extend
cuộc gọi vào mô-đun
Ví dụ trước này không phải là mã có cấu trúc tốt, vì hai lý do:
- Bây giờ chúng ta phải gọi cả hai
include
và extend
trong HostClass
định nghĩa để đưa mô-đun của chúng ta vào đúng cách. Điều này có thể rất cồng kềnh nếu bạn phải bao gồm nhiều mô-đun giống nhau.
HostClass
tài liệu tham khảo trực tiếp M::ClassMethods
, là chi tiết triển khai của mô-đun M
mà HostClass
không cần biết hoặc quan tâm.
Vậy còn điều này thì sao: khi chúng ta gọi include
ở dòng đầu tiên, bằng cách nào đó chúng ta thông báo cho mô-đun rằng nó đã được đưa vào, đồng thời cung cấp cho nó đối tượng lớp của chúng ta, để nó có thể gọi extend
chính nó. Bằng cách này, nhiệm vụ của mô-đun là thêm các phương thức của lớp nếu nó muốn.
Đây chính xác là những gì phương pháp đặc biệtself.included
dành cho. Ruby tự động gọi phương thức này bất cứ khi nào mô-đun được đưa vào một lớp (hoặc mô-đun) khác và chuyển vào đối tượng lớp chủ như đối số đầu tiên:
module M
def new_instance_method; "hi"; end
def self.included(base) # `base` is `HostClass` in our case
base.extend ClassMethods
end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
def self.existing_class_method; "cool"; end
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ still there!
Tất nhiên, thêm các phương thức lớp không phải là điều duy nhất chúng ta có thể làm self.included
. Chúng ta có đối tượng lớp, vì vậy chúng ta có thể gọi bất kỳ phương thức (lớp) nào khác trên nó:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end