Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Xây dựng Module Ruby Có thể Cấu hình: Pattern Module Builder

Trong bài này, chúng ta sẽ khám phá cách tạo Ruby module có thể cấu hình bởi user của code chúng ta - một pattern cho phép gem author thêm sự linh hoạt cho thư viện.

Hầu hết Ruby developer quen dùng module để share behavior. Theo docs, đây là một trong hai mục đích chính:

Modules serve two purposes in Ruby, namespacing and mixin functionality.

Rails thêm syntactic sugar dạng ActiveSupport::Concern, nhưng nguyên tắc chung vẫn vậy.

Vấn đề

Dùng module cung cấp mixin functionality thường đơn giản. Chỉ cần gom method và include module ailleurs:

module HelloWorld
  def hello
    "Hello, world!"
  end
end

class Test
  include HelloWorld
end

Test.new.hello #=> "Hello, world!"

Cơ chế này khá static, tuy hook method inherited và extended của Ruby cho phép behavior thay đổi dựa trên class including:

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!"

Đây dynamic hơn nhưng vẫn không cho user code rename method hello tại thời điểm include module.

Giải pháp: Configurable Ruby Modules

Vài năm qua, pattern mới nổi lên giải quyết vấn đề này, đôi khi gọi là "module builder pattern". Kỹ thuật này dựa trên hai đặc tính chính của Ruby:

  • Module giống như object bất kỳ - có thể tạo on the fly, gán biến, dynamic modify, pass to/return from method.

    def make_module
      mod = Module.new
      mod.module_eval do
        def hello
          "Hello, AppSignal world!"
        end
      end
      mod
    end
  • Argument của include hoặc extend không nhất thiết phải là module, có thể là expression trả về module, ví dụ method call.

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

Module Builder Hành động

Chúng ta sẽ dùng kiến thức xây dựng module đơn giản Wrapper implement behavior sau:

  1. Class include Wrapper chỉ wrap object của type cụ thể. Constructor verify argument type, raise error nếu type không match expected.
  2. Wrapped object доступ qua instance method tên original_<class>, ví dụ original_integer hoặc original_string.
  3. Cho phép consumer chỉ định tên alternative cho accessor method, ví dụ the_string.

Xem cách code hoạt động mong muốn:

# 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!"

Bước 1: define class IntWrapper.

Bước 2: class không просто include module by name mà mixin kết quả gọi Wrapper.for(Integer).

Bước 3: instantiate object, gán cho i. Object có method original_integer, thỏa mãn yêu cầu.

Bước 4: nếu pass argument sai type (string), raise TypeError hữu ích. Cuối cùng verify user có thể chỉ định custom accessor name.

Bước 5: define class StringWrapper, pass the_string làm keyword argument accessor_name, thấy hoạt động bước 6.

Mặc dù ví dụ hơi constructed, nhưng đủ varying behavior để demo module builder pattern và cách dùng.

Lần thử đầu tiên

Dựa trên yêu cầu và usage example, bắt đầu implement. Chúng ta biết cần module Wrapper với module-level method for, nhận class và optional keyword argument:

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

Vì return value của method này trở thành argument của include, nó phải là module. Tạo anonymous module với Module.new.

Module.new do
end

Theo yêu cầu, cần define constructor verify type của object passed, và appropriately named accessor method. Bắt đầu với constructor:

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

Dùng define_method dynamic add instance method cho receiver. Block đóng vai closure, dùng object klass từ outer scope để type check.

Thêm accessor method tên phù hợp không khó hơn:

# 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 }

Bước 1: check caller có pass accessor_name không. Nếu có, gán cho method_name xong. Nếu không, lấy class convert to underscored string, ví dụ Integer → integer, OpenStruct → open_struct. Biến klass_name prefix original_ tạo final accessor name. Bước 2: dùng define_method thêm vào module.

Code hoàn chỉnh đến đây. Dưới 20 dòng cho module Ruby flexible, configurable; không 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

Độc giả observant có thể nhớ Wrapper.for trả về anonymous module. Không phải vấn đề, nhưng hơi confusing khi examine inheritance chain của object:

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

Ở đây # (tên khác nếu bạn follow along) refer đến anonymous module của chúng ta.

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

Làm cho user dễ dàng hơn bằng cách trả về named module thay vì anonymous. Code tương tự trước, với minor changes:

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

Bước 1: tạo nested module tên "#{klass}InstanceMethods" (ví dụ IntegerInstanceMethods), là module "empty".

Bước 2: dùng module_eval trong method for, evaluate block code trong context của module nó được gọi. Cách này thêm behavior vào module trước khi return bước 3.

Bây giờ examine ancestors của class include Wrapper, output sẽ include properly named module, meaningful và dễ debug hơn anonymous module trước.

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

Module Builder Pattern Trong Thực Tế

Ngoài bài này, pattern module builder hoặc kỹ thuật tương tự còn tìm thấy đâu?

Ví dụ: family gem dry-rb, nơi dry-effects dùng module builder pass configuration options cho các effect handler:

# 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)

Tương tự trong gem Shrine tuyệt vời, cung cấp file upload toolkit cho Ruby app:

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

Pattern này còn tương đối mới, nhưng tôi kỳ vọng thấy nhiều hơn trong tương lai, đặc biệt trong gem focus pure Ruby app hơn Rails.

Tóm tắt

Bài này khám phá implement configurable module trong Ruby, kỹ thuật đôi khi gọi module builder pattern. Giống metaprogramming techniques khác, đi kèm cost tăng complexity, không nên dùng nếu không có lý do tốt. Tuy nhiên, trong rare case cần flexibility, object model Ruby lại cho phép giải pháp elegant, concise. Module builder pattern không phải thứ hầu hết Ruby developer cần thường xuyên, nhưng tool tuyệt vời trong toolkit, đặc biệt cho library author.

P.S. Nếu muốn đọc Ruby Magic posts ngay khi xuất bản, đăng ký newsletter Ruby Magic để không bỏ lỡ bài nào!