开发者

How can I choose which version of a module to include dynamically in Ruby?

开发者 https://www.devze.com 2023-01-09 02:34 出处:网络
I\'m writing a small Ruby command-line application that uses fileutils from the standard library for file operations. Depending on how the user invokes the application, I will want to include either F

I'm writing a small Ruby command-line application that uses fileutils from the standard library for file operations. Depending on how the user invokes the application, I will want to include either FileUtils, FileUtils::DryRun or FileUtils::Verbose.

Since include is private, though, I can't put the logic to choose into the object's initialize method. (That was my first thought, since then I could just pass the information about the user's choice as a parameter to new.) I've come up with two options that seem to work, but I'm not happy with either:

  1. Set a global variable in the app's namespace based on the user's choice, and then do a conditional include in the class:

    class Worker
      case App::OPTION
      when "dry-run"
        include FileUtils::DryRun
        etc.
    
  2. Create sub-classes, where the only difference is which version of FileUtils they include. Choose the appropriate one, depending on the user's choice.

    class Worker
      include FileUtils
      # shared Worker methods go here
    end
    class Worker::DryRun < Worker
      include FileUtils::DryRun
    end
    class Worker::Verbose < Worker
      include FileUtils::Verbose
    end
    

The first method seems DRY-er, but I'm hoping that there's s开发者_开发百科omething more straightforward that I haven't thought of.


So what if it's private?

class Worker
  def initialize(verbose=false)
    if verbose
      (class <<self; include FileUtils::Verbose; end)
    else
      (class <<self; include FileUtils; end)
    end
    touch "test"
  end
end

This includes FileUtils::something in particular's Worker's metaclass - not in the main Worker class. Different workers can use different FileUtils this way.


Conditionally including the module through the send methods works for me as in the below tested example:

class Artefact
  include HPALMGenericApi
  # the initializer just sets the server name we will be using ans also the 'transport' method : Rest or OTA (set in the opt parameter)
  def initialize server, opt = {}  
    # conditionally include the Rest or OTA module
    self.class.send(:include, HPALMApiRest) if (opt.empty? || (opt && opt[:using] opt[:using] == :Rest)) 
    self.class.send(:include, HPALMApiOTA) if (opt && opt[:using] opt[:using] == :OTA)    
    # ... rest of initialization code  
  end
end


If you would like to avoid the "switch" and inject the module, the

def initialize(injected_module)
    class << self
        include injected_module
    end
end

syntax won't work (the injected_module variable is out of scope). You could use the self.class.send trick, but per object instance extending seems more reasonable to me, not only because it is shorter to write:

def initialize(injected_module = MyDefaultModule)
    extend injected_module
end

but also it minimizes the side effects - the shared and easily changable state of the class, which can result in an unexpected behavior in a larger project. In Ruby the is no real "privacy" so to say, but some methods are marked private not without a reason.

0

精彩评论

暂无评论...
验证码 换一张
取 消