Reputation: 3816
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
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
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:
ClassMethod
modules, all the method included are (supposedly) called from an object, not from a classself.
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
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