How can I choose which version of a module to include dynamically in Ruby?
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:
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.
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.
精彩评论