Trong bài Ruby Magic trước, chúng ta đã tìm hiểu cách chèn module vào class một cách tin cậy bằng cách ghi đè phương thức .new, cho phép bao bọc các phương thức với hành vi bổ sung.
Lần này, chúng ta sẽ tiến xa hơn bằng cách trích xuất hành vi đó thành một module riêng để có thể tái sử dụng. Chúng ta sẽ xây dựng module Wrappable xử lý việc mở rộng class cho chúng ta, và trong quá trình đó sẽ tìm hiểu kỹ về biến instance cấp lớp (class-level instance variables). Hãy bắt đầu ngay!
Giới thiệu Module Wrappable
Để bao bọc đối tượng với module khi chúng được khởi tạo, chúng ta phải cho class biết module bao bọc nào sẽ được sử dụng. Hãy bắt đầu bằng cách tạo một module Wrappable đơn giản cung cấp phương thức wrap để đẩy module cho trước vào một mảng được định nghĩa như một thuộc tính class. Ngoài ra, chúng ta định nghĩa lại phương thức 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 class, chúng ta sử dụng extend. Phương thức extend thêm module cho trước vào class. Các phương thức sau đó trở thành phương thức class. Để thêm module bao bọc các instance của class này, giờ chúng ta có thể gọi phương thức wrap.
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ử tạo instance mới của Bird và gọi phương thức make_noise.
bird = Bird.new
bird.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise
Tuyệt vời! Nó hoạt động như mong đợi. Tuy nhiên, mọi thứ bắt đầu trở nên lạ khi chúng ta extend một class thứ hai với module Wrappable.
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
Dù Machine chưa được bao bọc với module Logging, nó vẫn xuất ra thông tin log. Càng tệ hơn - con chim giờ cũng đang "powering up" và "shutting down". Điều này không thể đúng được!
Nguyên nhân gốc rễ nằm ở cách chúng ta lưu trữ các module. Biến class @@wrappers được định nghĩa trên module Wrappable và được sử dụng bất cứ khi nào thêm module mới, bất kể class nào sử dụng wrap.
Điều này rõ ràng hơn khi xem các biến class được định nghĩa trên module Wrappable và các class Bird, Machine. Trong khi Wrappable có biến class được định nghĩa, hai class kia thì không.
Wrappable.class_variables # => [:@@wrappers]
Bird.class_variables # => []
Machine.class_variables # => []
Để sửa lỗi này, chúng ta phải sửa đổi implementation để sử dụng biến instance. Tuy nhiên, đây không phải biến trên các instance của Bird hay Machine, mà là biến instance trên chính các class đó.
Trong Ruby, Class chính là Object
Khái niệm này có thể hơi gây choáng ngợp lúc đầu, nhưng rất quan trọng để hiểu. Class là instance của Class và viết class Bird; end tương đương với Bird = Class.new. Để mọi thứ càng thêm "đỉnh" Class kế thừa từ Module và Module kế thừa từ Object. Kết quả là class và module có cùng các phương thức như bất kỳ object nào khác. Hầu hết các phương thức dùng trên class (như macro attr_accessor) thực chất là phương thức instance của Module.
Sử dụng Biến Instance trên Class
Hãy thay đổi implementation của Wrappable để dùng biến instance. Để code gọn gàng hơn, ta đưa ra phương thức wrappers sẽ khởi tạo mảng hoặc trả về mảng hiện có khi biến instance đã tồn tại. Ta cũng sửa phương thức wrap và new để sử dụng phương thức mới này.
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 kiểm tra biến instance trên module và hai class, ta thấy cả Bird và Machine giờ đều tự quản lý bộ sưu tập module bao bọc của mình.
Wrappable.instance_variables #=> []
Bird.instance_variables #=> [:@wrappers]
Machine.instance_variables #=> [:@wrappers]
Không ngạc nhiên, cách này cũng khắc phục vấn đề trước - giờ mỗi class được bao bọc với module riêng của nó.
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 (Inheritance)
Tất cả hoạt động tốt cho đến khi kế thừa xuất hiện. Chúng ta mong đợi class con sẽ kế thừa module bao bọc từ class cha. Hãy kiểm tra xem có đúng 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 thấy, nó không hoạt động như mong đợi, vì Pigeon cũng tự quản lý bộ sưu tập module bao bọc riêng. Mặc dù hợp lý khi module bao bọc của Pigeon không được định nghĩa trên Bird, nhưng không phải điều ta muốn. Hãy tìm cách lấy tất cả wrapper từ toàn bộ chuỗi kế thừa.
May mắn thay, Ruby cung cấp phương thức Module#ancestors liệt kê tất cả class và module mà một class (hoặc module) kế thừa từ.
Pigeon.ancestors # => [Pigeon, Bird, Object, Kernel, BasicObject]
Bằng cách thêm grep, ta lọc ra những class/module thực sự được extend với Wrappable. Vì ta muốn bao bọc instance với wrapper từ cấp cao hơn trước, ta gọi .reverse để đảo thứ tự.
Pigeon.ancestors.grep(Wrappable).reverse # => [Bird, Pigeon]
Phương thức #=== của Ruby
Một phần phép màu của Ruby đến từ phương thức #=== (hay case equality). Mặc định nó hoạt động như #== (equality). Tuy nhiên, một số class ghi đè #=== để cung cấp hành vi khác trong câu lệnh case. Đây là cách bạn dùng regex (#=== tương đương #match?), hoặc class (#=== tương đương #kind_of?) trong các câu lệnh đó. Các phương thức như Enumerable#grep, Enumerable#all?, hay Enumerable#any? cũng dựa trên case equality.
Giờ ta có thể gọi flat_map(&:wrappers) để lấy danh sách tất cả wrapper trong chuỗi kế thừa dưới dạng mảng đơn.
Pigeon.ancestors.grep(Wrappable).reverse.flat_map(&:wrappers) # => [Logging]
Còn lại là gói gọn vào module inherited_wrappers và sửa nhẹ phương thức new để dùng nó thay vì wrappers.
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
Chạy thử lần cuối xác nhận mọi thứ hoạt động như mong đợi. Module bao bọc chỉ được áp dụng cho class (và subclass) mà nó đượ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
Kết thúc!
Thừa nhận những con chim ồn ào này chỉ là ví dụ lý thuyết (tweet, tweet). Nhưng biến instance cấp lớp có thể kế thừa không chỉ thú vị để hiểu cách class hoạt động. Chúng là ví dụ tuyệt vời cho thấy class trong Ruby chỉ là object.
Và chúng tôi thừa nhận biến instance cấp lớp có thể kế thừa thậm chí còn hữu ích trong thực tế. Ví dụ: định nghĩa thuộc tính và mối quan hệ trên model với khả năng introspect sau này. Đối với chúng tôi, phép màu nằm ở việc thử nghiệm và hiểu sâu hơn cách mọi thứ hoạt động. Và mở rộng tư duy cho giải pháp cấp độ tiếp theo. 🧙🏼♀️
Như thường lệ, chúng tôi mong chờ nghe những gì bạn xây dựng dùng pattern này hoặc tương tự. Chỉ cần "chirp" (tweet) cho @AppSignal trên Twitter.