Ruby 将现有代码封装/打包到命名空间中

Ruby encapsulate/package existing code into a namespace

提问人:Milmandre 提问时间:1/13/2020 最后编辑:Milmandre 更新时间:1/13/2020 访问量:43

问:

我一直在四处寻找,但没有找到任何解决我在 Ruby 中面临的问题的答案。我正在编写一个应用程序,它使用不同版本中可用的核心模块集。如果我在另一个核心版本之后采购一个核心集版本,则两个代码版本将同时获取,并且会相互冲突。这很正常,我对此没意见。

一种方法可能是卸载以前的版本以加载新版本,但我想将所有版本都加载到特定的命名空间中(以避免一直卸载/重新加载代码所耗时)。我(或其他)的 2 种可能解决方案

  1. 要么获取代码,然后将其移动到版本命名空间中(请参阅下面的一些线索,但尚不起作用)
  2. 或者将代码直接源代码源到版本命名空间中(不知道如何确切地做到这一点,也许使用module_eval但需要使用依赖项重新编码 require 进程)。是否有任何解决方案似乎可行?

这是我试图实现的一个非常简单的 poc

文件:coreset_1.0.0.rb

module CoreA
  def self.who_am_i?; self.to_s; end
  def self.get_coreb; CoreB end
end

module CoreB
  def self.who_am_i?; self.to_s; end
end

文件:coreset_2.0.0.rb(有一些变化)

module CoreA
  def self.my_name; self.to_s; end
  def self.get_coreb; CoreB end
end

module CoreB
  def self.my_name; self.to_s; end
end

文件:coreManager.rb

module CoreManager
  def self.load_version(arg_version)
     #Create a module set for the selected version
     core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
     core_set = eval("Module #{core_set_name}; end; #{core_set_name}"

     #Load the requested code
     require "coreset_#{arg_version}.rb"

     #Move loaded code into it core set module
     core_set.const_set(:CoreA, Object.send(:remove_const, :CoreA))
     core_set.const_set(:CoreB, Object.send(:remove_const,:CoreB))

     #Return the created core set
     core_set
  end
end

如果运行代码:

require 'coreManager.rb'
core_set = CoreManager.load_version("1.0.0")
puts core_set::CoreA.who_am_i?
puts core_set::CoreA.get_coreB

它返回:

CoreA #not CoreSet_1_0_0::CoreA
uninitialized constant CoreA::CoreB (NameError)

如果运行静态定义的东西,它可以工作

module CoreSet
  module CoreA
    def self.who_am_i?; self.to_s; end
    def self.get_coreb; CoreB end
  end

  module CoreB
    def self.who_am_i?; self.to_s; end
  end
end

CoreSet::CoreA.get_coreb

它按预期返回:

CoreSet::CoreB

尽管通常说:“模块是一个常量”,但它似乎不止于此。有什么区别以及如何使动态版本工作?

还有其他想法吗?

感谢您的帮助,:)

Ruby 模块 嵌套 命名空间

评论


答:

0赞 Konstantin Strukov 1/13/2020 #1

您的代码中有几件事被破坏了(我想这对 POC 来说没问题),但最主要的是将常量和全局变量加载到全局命名空间中。require

因此,您的模块并不像您所期望的那样以这种方式命名。有一种方法允许“包装”执行加载的文件,但它被包装在一个匿名模块中,因此您可以防止全局命名空间被污染,但您不能以这种方式“定位”要定义到特定命名空间中的常量。Core<X>Kernel#load

您可以尝试将代码加载为文本,然后在动态创建的模块中对其进行评估,以匹配您的版本。例如,看看这个快速且非常肮脏的草图(文件应该位于目录中):coreset_...rbcoresets

module CoreSet; end

class CoreManager
  class << self
    def load_version(ver)
      raise "Vesion #{ver} unknown" unless exists?(ver)

      file = filename(ver)
      code = File.read(file)

      versioned_wrapper = Module.new do
        class_eval code
      end

      CoreSet.const_set("V_#{ver.gsub('.', '_')}", versioned_wrapper)
    end

    private

    def exists?(ver)
      File.exists? filename(ver)
    end

    def filename(ver)
      "coresets/coreset_#{ver.gsub('.', '_')}.rb"
    end
  end
end

CoreManager.load_version("1.0.0")
CoreManager.load_version("2.0.0")

p CoreSet::V_1_0_0::CoreA.who_am_i? # => "CoreSet::V_1_0_0::CoreA"
p CoreSet::V_1_0_0::CoreA.get_coreb # => CoreSet::V_1_0_0::CoreB
p CoreSet::V_2_0_0::CoreA.my_name # => "CoreSet::V_2_0_0::CoreA"
p CoreSet::V_2_0_0::CoreA.get_coreb # => CoreSet::V_2_0_0::CoreB

但是,请不要在家里这样做,请:)至少,我会三思而后行。

如果你只需要一次加载所有版本(你需要命名空间,对吧?)是什么阻止你静态地定义它们,比如等等,并使用惯用和安全的方式(自动)加载它们?:)使用动态嵌套常量定义(尤其是删除常量)是射出自己的脚的最简单方法之一......CoreSet::V1::Core<X>

评论

0赞 Milmandre 1/13/2020
感谢康斯坦丁的回答。你说得对,这个 POC 在真实代码方面真的很简约。我一直在考虑将源文件转换为字符串 eval,但问题是每个核心模块都是十几个类文件,由于类继承,这些类文件彼此之间存在依赖关系。因此,通过评估源代码来手动采购它们也不是一种选择,它会导致我编写带有 depencies 的采购代码,这正是 require 命令的作用......此外,在测试此解决方案时,我也遇到了一些评估错误,如果没有其他选择,我绝对同意您的看法
0赞 Milmandre 1/13/2020 #2

好的,我终于想出了一个可以帮助他人或可以讨论的解决方案。

获取错误未初始化常量 CoreA::CoreB (NameError) 导致我从一个新的角度看待问题。如果我无法从 CoreA 访问 CoreB 模块(因为在将模块常量重新定义到 CoreSet 模块中时模块嵌套已被破坏),那么为什么不在每个核心模块中引用集合中的其他模块?最后,它可以在没有任何肮脏的黑客攻击的情况下工作,我只是在创建指针,而 Ruby Core 发现它本身;)

module CoreManager
  def self.load_version(arg_version)
     #Create a module set for the selected version
     core_set_name = CoreSet + '_' + arg_version.gsub('.', '_')
     core_set = eval("Module #{core_set_name}; end; #{core_set_name}"

     #Load the requested code
     toplevel_consts = Object.constants
     require "coreset_#{arg_version}.rb"
     core_modules = Object.constants - toplevel_consts

     #Move the core modules to the set namespace
     core_modules.collect! do |core_module|
       core_module_sym = core_module.to_s.to_sym
       core_set.const_set(core_module_sym, Object.send(:remove_const, core_module_sym))
       eval("#{core_set}::#{core_module}")
     end

     #Create connexion between set cores to skirt broken module nesting
     core_modules.each do |current_core|
       core_modules.each do |other_core|
         current_core.const_set(other_core.to_s.to_sym, other_core) unless current_core == other_core
       end
     end

     #Return the created core set
     core_set
  end
end