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

Mô-đun Ruby có thể cấu hình:Mẫu trình tạo mô-đun

Trong bài đăng này, chúng ta sẽ khám phá cách tạo các mô-đun Ruby mà người dùng mã của chúng tôi có thể định cấu hình - một mẫu cho phép các tác giả đá quý thêm tính linh hoạt hơn cho thư viện của họ.

Hầu hết các nhà phát triển Ruby đều quen thuộc với việc sử dụng các mô-đun để chia sẻ hành vi. Rốt cuộc, đây là một trong những trường hợp sử dụng chính của họ, theo tài liệu:

Các mô-đun phục vụ hai mục đích trong Ruby, không gian tên và chức năng mixin.

Rails đã thêm một số đường cú pháp dưới dạng ActiveSupport::Concern , nhưng nguyên tắc chung vẫn như cũ.

Vấn đề

Việc sử dụng các mô-đun để cung cấp chức năng mixin thường đơn giản. Tất cả những gì chúng tôi phải làm là đóng gói một số phương pháp và đưa mô-đun của chúng tôi vào nơi khác:

module HelloWorld
  def hello
    "Hello, world!"
  end
end
class Test
  include HelloWorld
end
Test.new.hello
#=> "Hello, world!"

Đây là một cơ chế khá tĩnh, mặc dù Ruby's inheritedextended các phương thức hook cho phép một số hành vi khác nhau dựa trên lớp bao gồm:

module HelloWorld
  def self.included(base)
    define_method :hello do
      "Hello, world from #{base}!"
    end
  end
end
class Test
  include HelloWorld
end
Test.new.hello
#=> "Hello, world from Test!"

Điều này có phần năng động hơn nhưng vẫn không cho phép người dùng mã của chúng tôi, ví dụ:đổi tên hello tại thời điểm đưa vào mô-đun.

Giải pháp:Mô-đun Ruby có thể định cấu hình

Trong vài năm qua, một mô hình mới đã xuất hiện để giải quyết vấn đề này, mà đôi khi mọi người gọi là "mô hình xây dựng mô-đun". Kỹ thuật này dựa trên hai tính năng chính của Ruby:

  • Mô-đun cũng giống như bất kỳ đối tượng nào khác — chúng có thể được tạo nhanh chóng, được gán cho các biến, được sửa đổi động, cũng như được chuyển đến hoặc trả về từ các phương thức.

    def make_module
      # create a module on the fly and assign it to variable
      mod = Module.new
     
      # modify module
      mod.module_eval do
        def hello
          "Hello, AppSignal world!"
        end
      end
     
      # explicitly return it
      mod
    end
  • Đối số cho include hoặc extended các cuộc gọi không nhất thiết phải là một mô-đun, nó cũng có thể là một biểu thức trả về một mô-đun, ví dụ:một cuộc gọi phương thức.

    class Test
      # include the module returned by make_module
      include make_module
    end
     
    Test.new.hello
    #=> "Hello, AppSignal world!"

Trình tạo mô-đun đang hoạt động

Bây giờ chúng ta sẽ sử dụng kiến ​​thức này để xây dựng một mô-đun đơn giản có tên là Wrapper , thực hiện hành vi sau:

  1. Một lớp bao gồm Wrapper chỉ có thể bọc các đối tượng của một loại cụ thể. Hàm tạo sẽ xác minh loại đối số và đưa ra lỗi nếu loại không khớp với những gì mong đợi.
  2. Đối tượng được bọc sẽ khả dụng thông qua một phương thức phiên bản có tên là original_<class> , ví dụ. original_integer hoặc original_string .
  3. Nó sẽ cho phép người tiêu dùng mã của chúng tôi chỉ định một tên thay thế cho phương thức truy cập này, ví dụ:the_string .

Hãy xem cách chúng tôi muốn mã của mình hoạt động:

# 1
class IntWrapper
 # 2
 include Wrapper.for(Integer)
end
 
# 3
i = IntWrapper.new(42)
i.original_integer
#=> 42
 
# 4
i = IntWrapper.new("42")
#=> TypeError (not a Integer)
 
# 5
class StringWrapper
 include Wrapper.for(String, accessor_name: :the_string)
end
 
s = StringWrapper.new("Hello, World!")
# 6
s.the_string
#=> "Hello, World!"

Trong bước 1, chúng tôi xác định một lớp mới được gọi là IntWrapper .

Trong bước 2, chúng tôi đảm bảo rằng lớp này không chỉ bao gồm một mô-đun theo tên mà thay vào đó, kết hợp trong kết quả của một lệnh gọi đến Wrapper.for(Integer) .

Trong bước 3, chúng tôi khởi tạo một đối tượng của lớp mới và gán nó cho i . Như đã chỉ định, đối tượng này có một phương thức được gọi là original_integer , đáp ứng một trong các yêu cầu của chúng tôi.

Trong bước 4, nếu chúng tôi cố gắng chuyển đối số không đúng loại, chẳng hạn như một chuỗi, thì TypeError hữu ích sẽ được nâng lên. Cuối cùng, hãy xác minh rằng người dùng có thể chỉ định tên người truy cập tùy chỉnh.

Đối với điều này, chúng tôi xác định một lớp mới được gọi là StringWrapper ở bước 5 và chuyển the_string làm đối số từ khóa accessor_name , mà chúng ta sẽ thấy trong hành động ở bước 6.

Mặc dù đây được thừa nhận là một ví dụ có phần phức tạp, nhưng nó có đủ hành vi khác nhau để thể hiện mẫu trình tạo mô-đun và cách nó được sử dụng.

Lần thử đầu tiên

Dựa trên các yêu cầu và ví dụ sử dụng, bây giờ chúng ta có thể bắt đầu triển khai. Chúng tôi đã biết rằng chúng tôi cần một mô-đun có tên Wrapper với một phương thức cấp mô-đun được gọi là for , lấy một lớp làm đối số từ khóa tùy chọn:

module Wrapper
 def self.for(klass, accessor_name: nil)
 end
end

Vì giá trị trả về của phương thức này trở thành đối số của include , nó cần phải là một mô-đun. Do đó, chúng ta có thể tạo một cái ẩn danh mới với Module.new .

Module.new do
end

Theo yêu cầu của chúng tôi, điều này cần xác định một phương thức khởi tạo xác minh kiểu của đối tượng được truyền vào, cũng như một phương thức truy cập được đặt tên thích hợp. Hãy bắt đầu với hàm tạo:

define_method :initialize do |object|
 raise TypeError, "not a #{klass}" unless object.is_a?(klass)
 @object = object
end

Đoạn mã này sử dụng define_method để thêm động một phương thức phiên bản vào bộ thu. Vì khối hoạt động như một bao đóng, nó có thể sử dụng klass đối tượng từ phạm vi bên ngoài để thực hiện kiểm tra loại bắt buộc.

Việc thêm một phương thức trình truy cập được đặt tên thích hợp không khó hơn nhiều:

# 1
method_name = accessor_name || begin
 klass_name = klass.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase
 "original_#{klass_name}"
end
 
# 2
define_method(method_name) { @object }

Trước tiên, chúng ta cần xem liệu người gọi mã của chúng ta có được chuyển vào accessor_name hay không . Nếu vậy, chúng tôi chỉ cần gán nó cho method_name và sau đó được thực hiện. Nếu không, chúng tôi lấy lớp và chuyển đổi nó thành một chuỗi được gạch dưới, ví dụ:Integer biến thành integer hoặc OpenStruct vào open_struct . klass_name này sau đó biến được đặt tiền tố là original_ để tạo tên người truy cập cuối cùng. Khi chúng tôi biết tên của phương pháp, chúng tôi lại sử dụng define_method để thêm nó vào mô-đun của chúng tôi, như được hiển thị trong bước 2.

Đây là mã hoàn chỉnh cho đến thời điểm này. Ít hơn 20 dòng cho một mô-đun Ruby linh hoạt và có thể cấu hình; không quá tệ.

module Wrapper
  def self.for(klass, accessor_name: nil)
    Module.new do
      define_method :initialize do |object|
        raise TypeError, "not a #{klass}" unless object.is_a?(klass)
        @object = object
      end
 
      method_name = accessor_name || begin
        klass_name = klass.to_s.gsub(/(.)([A-Z])/,'\1_\2').downcase
        "original_#{klass_name}"
      end
 
      define_method(method_name) { @object }
    end
  end
end

Những độc giả tinh ý có thể nhớ rằng Wrapper.for trả về một mô-đun ẩn danh. Đây không phải là vấn đề, nhưng có thể hơi khó hiểu khi kiểm tra chuỗi kế thừa của một đối tượng:

StringWrapper.ancestors
#=> [StringWrapper, #<Module:0x0000000107283680>, Object, Kernel, BasicObject]

Đây #<Module:0x0000000107283680> (tên sẽ thay đổi nếu bạn đang theo dõi) đề cập đến mô-đun ẩn danh của chúng tôi.

Phiên bản cải tiến

Hãy làm cho cuộc sống của người dùng trở nên dễ dàng hơn bằng cách trả về một mô-đun được đặt tên thay vì một mô-đun ẩn danh. Mã cho điều này rất giống với những gì chúng tôi đã có trước đây, với một số thay đổi nhỏ:

module Wrapper
  def self.for(klass, accessor_name: nil)
    # 1
    mod = const_set("#{klass}InstanceMethods", Module.new)
 
    # 2
    mod.module_eval do
      define_method :initialize do |object|
        raise TypeError, "not a #{klass}" unless object.is_a?(klass)
        @object = object
      end
 
      method_name = accessor_name || begin
        klass_name = klass.to_s.gsub(/(.)([A-Z])/, '\1_\2').downcase
        "original_#{klass_name}"
      end
 
      define_method(method_name) { @object }
    end
 
    # 3
    mod
  end
end

Trong bước đầu tiên, chúng tôi tạo một mô-đun lồng nhau có tên "# {klass} InstanceMethods" (ví dụ:IntegerInstanceMethods ), đó chỉ là một mô-đun "trống".

Như được hiển thị trong bước 2, chúng tôi sử dụng module_eval trong for , đánh giá một khối mã trong ngữ cảnh của mô-đun mà nó được gọi. Bằng cách này, chúng ta có thể thêm hành vi vào mô-đun trước khi trả lại nó ở bước 3.

Nếu bây giờ chúng ta kiểm tra tổ tiên của một lớp bao gồm Wrapper , đầu ra sẽ bao gồm một mô-đun được đặt tên đúng, có ý nghĩa hơn và dễ gỡ lỗi hơn nhiều so với mô-đun ẩn danh trước đó.

StringWrapper.ancestors
#=> [StringWrapper, Wrapper::StringInstanceMethods, Object, Kernel, BasicObject]

Mẫu trình tạo mô-đun trong tự nhiên

Ngoài bài đăng này, chúng ta có thể tìm thấy mẫu trình tạo mô-đun hoặc các kỹ thuật tương tự ở đâu?

Một ví dụ là dry-rb họ đá quý, ví dụ, trong đó, dry-effects sử dụng trình tạo mô-đun để chuyển các tùy chọn cấu hình đến các trình xử lý hiệu ứng khác nhau:

# This adds a `counter` effect provider. It will handle (eliminate) effects
include Dry::Effects::Handler.State(:counter)
 
# Providing scope is required
# All cache values will be scoped with this key
include Dry::Effects::Handler.Cache(:blog)

Chúng ta có thể tìm thấy cách sử dụng tương tự trong đá quý Shrine tuyệt vời, cung cấp bộ công cụ tải tệp lên cho các ứng dụng Ruby:

class Photo < Sequel::Model
  include Shrine::Attachment(:image)
end

Mẫu này vẫn còn tương đối mới, nhưng tôi hy vọng chúng ta sẽ thấy nhiều hơn trong tương lai, đặc biệt là trong các gem tập trung nhiều hơn vào các ứng dụng Ruby thuần túy hơn là các ứng dụng Rails.

Tóm tắt

Trong bài đăng này, chúng tôi đã khám phá cách triển khai các mô-đun có thể định cấu hình trong Ruby, một kỹ thuật đôi khi được gọi là mô hình trình tạo mô-đun. Giống như các kỹ thuật lập trình siêu ứng dụng khác, điều này phải trả giá là tăng độ phức tạp và do đó không nên sử dụng mà không có lý do chính đáng. Tuy nhiên, trong những trường hợp hiếm hoi cần sự linh hoạt như vậy, mô hình đối tượng của Ruby một lần nữa cho phép đưa ra một giải pháp ngắn gọn và trang nhã. Mẫu trình tạo mô-đun không phải là thứ mà hầu hết các nhà phát triển Ruby sẽ cần thường xuyên, nhưng nó là một công cụ tuyệt vời để có trong bộ công cụ của một người, đặc biệt là đối với các tác giả thư viện.

Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!