Trong Ruby Magic trước, chúng tôi đã tìm ra cách đưa các mô-đun vào các lớp một cách đáng tin cậy bằng cách ghi đè lên .new
của nó , cho phép chúng tôi kết hợp các phương thức với hành vi bổ sung.
Lần này, chúng tôi đang tiến thêm một bước nữa bằng cách trích xuất hành vi đó vào một mô-đun của riêng nó để chúng tôi có thể sử dụng lại nó. Chúng tôi sẽ tạo một Wrappable
mô-đun xử lý phần mở rộng lớp cho chúng ta và chúng ta sẽ tìm hiểu tất cả về các biến phiên bản cấp lớp trong quá trình thực hiện. Hãy đi sâu vào ngay!
Giới thiệu Wrappable
Mô-đun
Để bọc các đối tượng bằng mô-đun khi chúng được khởi tạo, chúng ta phải cho lớp biết những mô hình gói nào sẽ sử dụng. Hãy bắt đầu bằng cách tạo một Wrappable
đơn giản mô-đun cung cấp một wrap
phương thức đẩy mô-đun đã cho vào một mảng được xác định là thuộc tính lớp. Ngoài ra, chúng tôi xác định lại new
như đã thảo luận trong bài trước.
module Wrappable
@@wrappers = []
def wrap(mod)
@@wrappers << mod
end
def new(*arguments, &block)
instance = allocate
@@wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
Để thêm hành vi mới vào một lớp, chúng tôi sử dụng extend
. extend
phương thức thêm mô-đun đã cho vào lớp. Các phương thức sau đó trở thành các phương thức lớp. Để thêm một mô-đun để bọc các thể hiện của lớp này, bây giờ chúng ta có thể gọi wrap
phương pháp.
module Logging
def make_noise
puts "Started making noise"
super
puts "Finished making noise"
end
end
class Bird
extend Wrappable
wrap Logging
def make_noise
puts "Chirp, chirp!"
end
end
Hãy thử điều này bằng cách tạo một phiên bản mới của Bird
và gọi make_noise
phương pháp.
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
Tuyệt quá! Nó hoạt động như mong đợi. Tuy nhiên, mọi thứ bắt đầu hoạt động hơi kỳ lạ khi chúng tôi mở rộng lớp thứ hai với Wrappable
mô-đun.
module Powered
def make_noise
puts "Powering up"
super
puts "Shutting down"
end
end
class Machine
extend Wrappable
wrap Powered
def make_noise
puts "Buzzzzzz"
end
end
machine = Machine.new
machine.make_noise
# Powering up
# Started making noise
# Buzzzzzz
# Finished making noise
# Shutting down
bird = Bird.new
bird.make_noise
# Powering up
# Started making noise
# Chirp, chirp!
# Finished making noise
# Shutting down
Mặc dù Machine
chưa được bọc bằng Logging
mô-đun, nó vẫn xuất ra thông tin ghi nhật ký. Điều tồi tệ hơn - ngay cả con chim hiện đang bật nguồn và tắt nguồn. Điều đó không thể đúng, phải không?
Gốc của vấn đề này nằm ở cách chúng ta đang lưu trữ các mô-đun. Biến lớp @@wrappables
được xác định trên Wrappable
mô-đun và được sử dụng bất cứ khi nào chúng tôi thêm một mô-đun mới, bất kể lớp wrap
là gì được sử dụng trong.
Điều này rõ ràng hơn khi xem xét các biến lớp được xác định trên Wrappable
mô-đun và Bird
và Machine
các lớp học. Trong khi Wrappable
có một phương thức lớp được xác định, hai lớp thì không.
Wrappable.class_variables # => [:@@wrappers]
Bird.class_variables # => []
Machine.class_variables # => []
Để khắc phục điều này, chúng ta phải sửa đổi việc triển khai để nó sử dụng các biến cá thể. Tuy nhiên, đây không phải là các biến trên các bản sao của Bird
hoặc Machine
, nhưng các biến phiên bản trên chính các lớp.
Trong Ruby, các lớp chỉ là các đối tượng
Đây chắc chắn là một chút bối rối lúc đầu, nhưng vẫn là một khái niệm rất quan trọng cần hiểu. Các lớp là các bản sao của Class
và viết class Bird; end
tương đương với cách viết Bird = Class.new
. Để làm cho mọi thứ trở nên khó hiểu hơn Class
kế thừa từ Module
kế thừa từ Object
. Kết quả là, các lớp và mô-đun có các phương thức giống như bất kỳ đối tượng nào khác. Hầu hết các phương thức chúng tôi sử dụng trên các lớp (như attr_accessor
macro) thực sự là các phương thức phiên bản của Module
.
Sử dụng các biến phiên bản trên các lớp
Hãy thay đổi Wrappable
triển khai để sử dụng các biến cá thể. Để giúp mọi thứ gọn gàng hơn một chút, chúng tôi giới thiệu wrappers
phương thức thiết lập mảng hoặc trả về mảng hiện có khi biến cá thể đã tồn tại. Chúng tôi cũng sửa đổi wrap
và new
để họ sử dụng phương pháp mới đó.
module Wrappable
def wrap(mod)
wrappers << mod
end
def wrappers
@wrappers ||= []
end
def new(*arguments, &block)
instance = allocate
wrappers.each { |mod| instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
Khi chúng tôi kiểm tra các biến cá thể trên mô-đun và trên hai lớp, chúng tôi có thể thấy rằng cả Bird
và Machine
hiện đang duy trì bộ sưu tập các mô-đun gói của riêng họ.
Wrappable.instance_variables #=> []
Bird.instance_variables #=> [:@wrappers]
Machine.instance_variables #=> [:@wrappers]
Không có gì đáng ngạc nhiên, điều này cũng khắc phục sự cố mà chúng tôi đã quan sát trước đó - bây giờ, cả hai lớp đều được bao bọc bằng các mô-đun riêng lẻ của riêng chúng.
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzzz
# Shutting down
Hỗ trợ Kế thừa
Tất cả điều này đều hoạt động tốt cho đến khi kế thừa được giới thiệu. Chúng tôi mong đợi rằng các lớp sẽ kế thừa các mô-đun gói từ lớp cha. Hãy kiểm tra xem có đúng như vậy không.
module Flying
def make_noise
super
puts "Is flying away"
end
end
class Pigeon < Bird
wrap Flying
def make_noise
puts "Coo!"
end
end
pigeon = Pigeon.new
pigeon.make_noise
# Coo!
# Is flying away
Như bạn có thể thấy, nó không hoạt động như mong đợi, vì Pigeon
cũng đang duy trì bộ sưu tập các mô-đun gói của riêng mình. Mặc dù có ý nghĩa khi gói các mô-đun được xác định cho Pigeon
không được xác định trên Bird
, đó không phải là chính xác những gì chúng tôi muốn. Hãy tìm ra cách để lấy tất cả các trình bao bọc từ toàn bộ chuỗi kế thừa.
Thật may mắn cho chúng tôi, Ruby cung cấp Module#ancestors
để liệt kê tất cả các lớp và mô-đun mà một lớp (hoặc mô-đun) kế thừa.
Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]
Bằng cách thêm grep
gọi, chúng tôi có thể chọn những cái thực sự được mở rộng với Wrappable
. Trước tiên, vì chúng tôi muốn bao bọc các phiên bản bằng trình bao bọc từ chuỗi cao hơn lên, chúng tôi gọi .reverse
để lật đơn đặt hàng.
Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]
#===
của Ruby phương pháp
Một số phép thuật của Ruby đến với #===
(hoặc bình đẳng trường hợp ) phương pháp. Theo mặc định, nó hoạt động giống như #==
(hoặc bình đẳng ) phương pháp. Tuy nhiên, một số lớp ghi đè #===
để cung cấp các hành vi khác nhau trong case
các câu lệnh. Đây là cách bạn có thể sử dụng biểu thức chính quy (#===
tương đương với #match?
) hoặc các lớp (#===
tương đương với #kind_of?
) trong các câu lệnh đó. Các phương thức như Enumerable#grep
, Enumerable#all?
hoặc Enumerable#any?
cũng dựa trên phương pháp bình đẳng trường hợp.
Bây giờ chúng ta có thể gọi flat_map(&:wrappers)
để nhận danh sách tất cả các trình bao bọc được xác định trong chuỗi kế thừa dưới dạng một mảng.
Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]
Tất cả những gì còn lại là đóng gói nó vào một inherited_wrappers
và sửa đổi một chút phương thức mới để nó sử dụng phương thức đó thay vì wrappers
phương pháp.
module Wrappable
def inherited_wrappers
ancestors
.grep(Wrappable)
.reverse
.flat_map(&:wrappers)
end
def new(*arguments, &block)
instance = allocate
inherited_wrappers.each { |mod|instance.singleton_class.include(mod) }
instance.send(:initialize, *arguments, &block)
instance
end
end
Một lần chạy thử nghiệm cuối cùng xác nhận rằng mọi thứ hiện đang hoạt động như mong đợi. Các mô-đun gói chỉ được áp dụng cho lớp (và các lớp con của nó) mà chúng được áp dụng.
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
machine = Machine.new
machine.make_noise
# Powering up
# Buzzzzz
# Shutting down
pigeon = Pigeon.new
pigeon.make_noise
# Started making noise
# Coo!
# Finished making noise
# Is flying away
Đó là một sự kết thúc!
Phải thừa nhận rằng những con chim ồn ào này là một ví dụ lý thuyết (tweet, tweet). Nhưng các biến cá thể của lớp có thể kế thừa không chỉ đơn giản là hiểu cách thức hoạt động của các lớp. Chúng là một ví dụ tuyệt vời cho thấy các lớp chỉ là các đối tượng trong Ruby.
Và chúng tôi sẽ thừa nhận rằng các biến cá thể lớp có thể kế thừa thậm chí có thể khá hữu ích trong cuộc sống thực. Ví dụ, hãy nghĩ về việc xác định các thuộc tính và mối quan hệ trên một mô hình với khả năng xem xét chúng sau này. Đối với chúng tôi, điều kỳ diệu là thử nghiệm điều này và hiểu rõ hơn về cách mọi thứ hoạt động. Và mở rộng tâm trí của bạn cho một cấp độ giải pháp tiếp theo. 🧙🏼♀️
Như mọi khi, chúng tôi mong muốn được nghe những gì bạn xây dựng bằng cách sử dụng mẫu này hoặc các mẫu tương tự. Chỉ cần gọi @AppSignal trên Twitter.