Translunar
Translunar

Reputation: 3816

Is it possible to include a module during initialize?

Suppose one has the following pattern:

A good example is an orbital elements calculator, which might take radius and velocity vectors, and compute orbital elements --- or alternatively, might take orbital elements and compute radius and velocity vectors.

Why not do this in a more straightforward way? Well, I want to make use of instance variables to allow just-in-time computations, e.g.,

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

I tried doing this roughly as follows:

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

Is there a better pattern / is this an advisable usage? Is it possible to do this?

Upvotes: 0

Views: 403

Answers (3)

BroiSatse
BroiSatse

Reputation: 44715

To me it sounds like a job for simple inheritance:

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

Upvotes: 1

Ahmad Sherif
Ahmad Sherif

Reputation: 6223

You can by including the module of choice in the singleton class (sometimes called eigenclass), not the object itself. So the initializer can look like this

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

If you're going to use this approach, then you need to adjust your modules accordingly:

  1. No need for ClassMethod modules, all the method included are (supposedly) called from an object, not from a class
  2. Remove the self. from the method names (except self.included of course)

The rest of the code can look like this

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

That said, this solution is just for demonstration and I suggest you follow the example @Ollie mentioned in his answer as it is much cleaner.

Upvotes: 1

Ollie
Ollie

Reputation: 344

I'm not really sure why you want to mask an instance of two different entities under one but if you want simple decision logic based on arguments, this works:

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

If you want to do some kind of same-interface-thingy, may want to turn Orbit into a class and subclass it in the other classes but that isn't clear from your code.

Upvotes: 2

Related Questions