Nate Bird
Nate Bird

Reputation: 5335

DRY way to assign function values to an object

I have an empty object that accepts calculated values for each factor. The method is readable but long and ugly. What is a DRY way of doing this?

class ReadingScore

  def initialize(reading, score)
    @reading = reading
    @score = score
  end

  def assign_scoring_factors
    @score.heart_rate_factor              = heart_rate_factor
    @score.systolic_pressure_factor       = systolic_pressure_factor
    @score.diastolic_pressure_factor      = diastolic_pressure_factor
    @score.mean_pressure_factor           = mean_pressure_factor
    @score.signal_minimum_factor          = signal_minimum_factor
    @score.signal_average_factor          = signal_average_factor
    …
  end

  def heart_rate_factor
    @reading.heart_rate && (1..10).include?(@reading.heart_rate) ? 0 : 10
  end

  …
end

Update The overall purpose of this class is to calculate a score of a reading. I can’t provide all the code because it is a proprietary algorithm for a medical device.

But basically there are n factors of a @reading that are calculated and then saved to a @score object associated with the @reading. The sum of the factors is also calculated as a total on the @score object. The @score object looks like the following:

  @score=
    #<Score:0x007faa0b33ec50
     @attributes=
      {"id"=>42,
       "reading_id"=>42,
       "sum_of_factors"=>10,
       "heart_rate_factor"=>10,
       "another_factor"=>0,
       "another_factor"=>0}

Upvotes: 0

Views: 82

Answers (4)

engineersmnky
engineersmnky

Reputation: 29478

You could use delegate

class ReadingScore
  extend Forwardable
  delegate [:heart_rate_factor=, :systolic_pressure_factor=,:diastolic_pressure_factor=,
      :mean_pressure_factor=,:signal_minimum_factor=,:signal_average_factor=] => :@score
  def initialize
    @score = Score.new
  end

  def assign_scoring_factors
    %w(heart_rate_factor systolic_pressure_factor diastolic_pressure_factor mean_pressure_factor signal_minimum_factor signal_average_factor).each do |meth|
      self.send("#{meth}=",self.send(meth))
    end
    self
  end
end

but I agree with others that rethinking the whole design might be better here.

You could also use tap but the code will look fairly similar to what you have now.

Also I have no idea what a Score looks like internally because it seems like it would be better to place this logic inside the Score or Reading and pass all of this to a method or initializer of Score. e.g.

class ReadingScore
   def intialize(reading)
     @reading = Reading.new(reading)
     @score = Score.new(@reading)
   end

end
class Reading
  #...
  def heart_rate_score
    heart_rate && (1..10).include?(@reading.heart_rate) ? 0 : 10
  end
  def systolic_pressure_score
    #logic
  end
  def diastolic_pressure_score
    #logic
  end
  def mean_pressure_score
    #logic
  end
  def signal_minimum_score
    #logic
  end
  def signal_average_score
    #logic
  end
end
class Score
  attr_accessor :heart_rate_factor,:systolic_pressure_factor,:diastolic_pressure_factor,
      :mean_pressure_factor,:signal_minimum_factor,:signal_average_factor
   def initialize(reading)
     factorialize(reading)
     self
   end


  private
    def factorialize(reading)
      %w(heart_rate systolic_pressure diastolic_pressure mean_pressure signal_minimum signal_average) do |meth|
        self.send("#{meth}_factor=",reading.send("#{meth}_score")
      end
    end
 end

This way your logic is centralized in Score and Reading and can be avoided in ReadingScore. This will make the code easier to trace and will clean up the original class.

Upvotes: 1

Max
Max

Reputation: 22365

You can automatically populate an array of factors using method_added. This combines nicely with dynamic assignment of factors as in your answer.

class ReadingScore
  @factors = []

  def self.method_added meth
    @factors << meth if meth =~ /_factor\Z/
  end

  def self.factors
    @factors
  end
end

Note that these are class methods, so you would need to use self.class.factors when using this in an instance method.

Here is a full implementation in case you do not see how to integrate this code.

Upvotes: 2

Nate Bird
Nate Bird

Reputation: 5335

This seems to be the best option so far. The first answer to the question started me on this route, but the poster seems to have removed it…

  def assign_factors_to_score
    factors.each do |factor|
      @score.public_send("#{factor}=", self.public_send(factor))
    end
  end

  def factors
    %i{factor_a factor_b factor_c factor_d}
  end

Upvotes: 2

Jordan Samuels
Jordan Samuels

Reputation: 997

you can do it like this, if you insist:

def assign_scoring_factors
  %w(heart_rate systolic_pressure diastolic_pressure mean_pressure signal_minimum signal_average).each |f| do
    eval("@score.#{f}.factor = #{f}.factor")
  end
end

but this isn't what I'd do. I'd either leave it moist, or just use a map.

Upvotes: -1

Related Questions