It is common to have custom data types in an application (whether implicitly coded or not). For example, I have an application which has a ‘sales month’ data type which is a year and month. I store it in the database as a date with the day field set to one, but I would like to hide the implementation and the first day of the month kludge and to be able to modify this type and use it across the application.

Defining a value object lets me do this:


class SalesMonth

  include Comparable

  def initialize(*args)
    if args[0].is_a? Date
      @raw_sales_month = Date.new(args[0].year, args[0].month, 1)
    elsif args.length == 2
      @raw_sales_month = Date.new(args[0], args[1])
    else
      raise ArgumentError, "SalesMonth can be initialized with a Date" +
        " or [year, month] Array"
    end
  end

  def year
    @raw_sales_month.year
  end

  def month
    @raw_sales_month.month
  end

  def yyyymm
    @raw_sales_month.strftime('%Y%m')
  end

  def to_date
    @raw_sales_month
  end   

  def <=>(other)
    raw_sales_month <=> other
  end

  def to_s
    raw_sales_month.to_s
  end

  protected
  attr_reader :raw_sales_month

end

Now to use this with an attribute of a model, I override the accessors for it:


class SomeModel < ActiveRecord::Base

  def start_sales_month=(value)
    # save the Date
    write_attribute(:start_sales_month, SalesMonth.new(value).to_date)
    # or self[:start_sales_month] = SalesMonth.new(value)
  end

  def start_sales_month
    # return the SalesMonth
    SalesMonth.new(read_attribute(:start_sales_month))
  end

end

Note that in this instance my value object maps to a single database column, but this need not be the case. Mapping to multiple fields can be handled in the accessor method overrides.

Now, since I have a bunch of classes that use this data type, I don’t want to rewrite the accessor methods each time, so instead of the above accessors I am going to create a macro-like thingy to do it for me.

require 'active_support/concern'

module ValueObjectMacros

  extend ActiveSupport::Concern
  included do
    # instance methods
  end

  module ClassMethods

    def acts_as_sales_month(*fields)
      fields.each do |field|

        define_method "#{field.to_s}=" do |value|
          # store the date
          write_attribute(":#{field.to_s}", SalesMonth.new(value).to_date)
        end

        define_method "#{field.to_s}" do
          # return the SalesMonth
          SalesMonth.new(read_attribute(":#{field.to_s}"))
        end

      end
    end
  end
end

Now in the model, all I have to do is:

class SomeModel < ActiveRecord::Base

  acts_as_sales_month :start_sales_month, :end_sales_month
...
end

Side note, I don’t want all the value_objects cluttering up lib, so I put them under lib/value_objects and add this to the config/application.rb:

config.autoload_paths += Dir["#{config.root}/lib/value_objects/**/"]

Refs:
https://www.grok-interactive.com/blog/value-objects-in-ruby-part-1/
http://www.informit.com/articles/article.aspx?p=2220311&seqNum=11