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 endNow 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 endNote 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 endNow in the model, all I have to do is:
class SomeModel < ActiveRecord::Base acts_as_sales_month :start_sales_month, :end_sales_month ... endSide 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
Recent Comments