初始化期间是否可以包含模块?

时间:2014-10-07 19:15:44

标签: ruby metaprogramming

假设一个人有以下模式:

  • Foobar类,它使用Foo或Bar中的逻辑,但不是两者,取决于构造函数给出的参数
  • Foo模块,包含一些逻辑
  • 条形模块,包含一些备用逻辑

一个很好的例子是轨道元素计算器,它可能采用半径和速度矢量,计算轨道元素---或者可能采用轨道元素并计算半径和速度矢量。

为什么不以更直接的方式做到这一点?好吧,我想利用实例变量来实现即时计算,例如,

def derived_value
  @derived_value ||= begin
    # compute only when/if necessary
  end
end

我尝试大致如下:

class Orbit
  def initialize options = {}
    if [:radius,:velocity].all? { |key| options.has_key?(key) }
      # Provide instance methods which derive orbital elements from radius, velocity
      self.include Orbit::FromRadiusAndVelocity
    else
      # Provide instance methods which derive radius and velocity from orbital elements
      self.include Orbit::FromOrbitalElements
    end
    initialize_helper options
  end
end

module Orbit::FromOrbitalElements
  module ClassMethods
    class << self
      def initialize_helper options
        @argument_of_perigee = options[:argument_of_perigee]
        puts "From orbital elements"
      end

      attr_reader :argument_of_perigee
    end
  end

  def self.included(base)
    base.extend Orbit::FromOrbitalElements::ClassMethods
  end

  def self.radius
    # Instance method to calculate the radius from the orbital elements
    @radius ||= begin
      # If radius is not already defined, calculate it from some intermediate
      # values, which themselves depend upon orbital elements.
    end
  end
end

module Orbit::FromRadiusAndVelocity
  module ClassMethods
    class << self
      def initialize_helper options
        @radius = options[:radius]
        @velocity = options[:velocity]
        puts "From radius and velocity"
      end

      attr_reader :radius, :velocity
    end
  end

  def self.included(base)
    base.extend Orbit::FromRadiusAndVelocity::ClassMethods
  end

  def self.argument_of_perigee
    # Instance method to calculate an orbital element
    # (There would be more instance methods like this)
    @argument_of_perigee ||= begin
      # If argument_of_perigee is not already defined, calculate it from some intermediate
      # values, which themselves depend upon radius and velocity.
    end
  end
end

是否有更好的模式/这是一个明智的用法?有可能这样做吗?

3 个答案:

答案 0 :(得分:2)

我不确定你为什么要在一个实体下屏蔽两个不同实体的实例,但是如果你想要基于参数的简单决策逻辑,这可行:

# orbit.rb
module Orbit
  module_function

  def from_options(options = {})
    klass = if options[:radius] && options[:velocity]
              FromRadiusAndVelocity
            else
              FromOrbitalElements
            end

    klass.new(options)
  end
end

# orbit/from_orbital_elements.rb
module Orbit
  class FromOrbitalElements
    attr_reader :argument_of_perigee

    def initialize(options)
      @argument_of_perigee = options[:argument_of_perigee]

      puts 'From orbital elements'
    end

    def radius
      @radius ||= begin
                    # ...
                  end
    end
  end
end

# orbit/from_radius_and_velocity.rb
module Orbit
  class FromRadiusAndVelocity
    attr_reader :radius, :velocity

    def initialize(options)
      @radius   = options[:radius]
      @velocity = options[:velocity]

      puts 'From radius and velocity'
    end

    def argument_of_perigee
      @argument_of_perigee ||= begin
                                 # ...
                               end
    end
  end
end

# Usage
orbit = Orbit.from_options(radius: 5, velocity: 6)
orbit = Orbit.from_options(argument_of_perigee: 5)

如果你想做某种同样的接口,可能想把Orbit变成一个类,并把它子类化在其他类中,但是你的代码并不清楚。

答案 1 :(得分:1)

对我而言,这听起来像是简单继承的工作:

class FooBar
  def self.new(options)
    if [:radius,:velocity].all? { |key| options.has_key?(key) }
      Foo.new options
    else
      Bar.new options
    end
  end

  # common Foo and Barlogic here
end

class Foo < FooBar
end

class Bar < FooBar
end

答案 2 :(得分:1)

您可以通过在单例类(有时称为eigenclass)中包含所选模块,而不是对象本身。所以初始化器看起来像这样

class Orbit
  def initialize options = {}
    if [:radius,:velocity].all? { |key| options.has_key?(key) }
      self.singleton_class.include Orbit::FromRadiusAndVelocity
    else
      self.singleton_class.include Orbit::FromOrbitalElements
    end

    initialize_helper options
  end
end

如果您打算使用这种方法,那么您需要相应地调整模块:

  1. 不需要ClassMethod个模块,所有包含的方法都是(据说)从一个对象而不是一个类中调用
  2. 从方法名称中删除self.(当然,self.included除外)
  3. 其余代码可能如下所示

    module Orbit::FromOrbitalElements
      def initialize_helper options
        @argument_of_perigee = options[:argument_of_perigee]
        puts "From orbital elements"
      end
    
      def self.included(base)
        base.instance_eval do
          attr_reader :argument_of_perigee
        end
      end
    
      def radius
        @radius ||= begin
        end
      end
    end
    
    module Orbit::FromRadiusAndVelocity
      def self.included(base)
        base.instance_eval do
          attr_reader :radius, :velocity
        end
      end
    
      def initialize_helper options
        @radius = options[:radius]
        @velocity = options[:velocity]
        puts "From radius and velocity"
      end
    
      def argument_of_perigee
        @argument_of_perigee ||= begin
        end
      end
    end
    

    那就是说,这个解决方案只是为了演示,我建议你按照他的回答中提到的@Ollie的例子,因为它更清晰。