Computer >> Máy Tính >  >> Lập trình >> Ruby

Sự kỳ diệu của các biến phiên bản cấp lớp

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à BirdMachine 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 wrapnew để 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ả BirdMachine 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.