The task was allow user to view, create, edit records in different units, i.e. altitude in meters and feets, speed in m/s, knots, km/h, mi/h.
I've read a lot about Value Objects, composed_of
and why we should not use it and use serialize
instead and came with solution below.
It seems like complex solution for me. Could you please point me what can I refactor and direction for it?
app/models/weather.rb
class Weather < ActiveRecord::Base
attr_accessor :altitude_unit, :wind_speed_unit
attr_reader :altitude_in_units, :wind_speed_in_units
belongs_to :weatherable, polymorphic: true
before_save :set_altitude, :set_wind_speed
validates_presence_of :actual_on, :wind_direction
validate :altitude_present?
validate :wind_speed_present?
validates_numericality_of :altitude, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :altitude_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_direction, greater_than_or_equal_to: 0, less_than: 360, allow_nil: true
serialize :altitude, Distance
serialize :wind_speed, Velocity
def wind_speed_in_units=(value)
@wind_speed_in_units = value_from_param(value)
end
def altitude_in_units=(value)
@altitude_in_units = value_from_param(value)
end
private
def value_from_param(value)
return nil if value.is_a?(String) && value.empty?
value
end
def altitude_present?
return if altitude.present? || altitude_in_units.present?
errors.add :altitude, :blank
end
def wind_speed_present?
return if wind_speed.present? || wind_speed_in_units.present?
errors.add :wind_speed, :blank
end
def set_altitude
return if altitude_in_units.blank? || altitude_unit.blank?
self.altitude = Distance.new(altitude_in_units, altitude_unit)
end
def set_wind_speed
return if wind_speed_in_units.blank? || wind_speed_unit.blank?
self.wind_speed = Velocity.new(wind_speed_in_units, wind_speed_unit)
end
end
app/model/distance.rb
class Distance < DelegateClass(BigDecimal)
FT_IN_M = 3.280839895
def self.load(distance)
new(distance) unless distance.nil?
end
def self.dump(obj)
obj.dump
end
def initialize(distance, unit = 'm')
value = convert_from(BigDecimal.new(distance), unit)
super(value)
end
def dump
@delegate_dc_obj
end
def convert_to(unit)
method = "to_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method
end
def convert_from(val, unit)
method = "from_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method, val
end
def to_m
@delegate_dc_obj
end
def to_ft
@delegate_dc_obj * FT_IN_M
end
def from_m(val)
val
end
def from_ft(val)
val / FT_IN_M
end
end
app/models/velocity.rb
is almost the same as distance.