Best Pattern for 'repeatable' Model Concerns

I have a concern that allows me to mark a model in my Rails app as “Durationable”. Something like this:

module Durationable
  extend ActiveSupport::Concern

  included do
    attr_accessor :duration_years, :duration_months, ... etc

    after_initialize(unless: :new_record?) do |record|
      duration = record.read_attribute(durationable_field_name)

      @duration_years = record.duration&.parts&.dig(:years) || 0
      @duration_months = record.duration&.parts&.dig(:months) || 0
        ...etc
    end

    before_save do |record|
      record.duration = (@duration_years.to_i.years + @duration_months.to_i.months ... etc)
    end

  end
  
end

This worked fine on my model:

class Task < ApplicationRecord
   include Durationable
end

It turns out that I now have a model that has two “duration” fields - say booked_duration and actual_duration. My concern doesn’t seem particularly well suited as it only copes with a single duration field.

I was thinking of being able to define my model with something like:

class Task < ApplicationRecord
   include Durationable
   acts_as_duration :booked_duration
   acts_as_duration :actual_duration
end

I would then define a class method in the concern called acts_as_duration that manags an array of duration fields and add similar behaviour to the original concern, but operating over these multiple fields.

Does this seem like a good/suitable use of concerns? I can certainly get something functional working, but I wonder if there is a better suited pattern? It seems strange to include Durationable but that doesn’t actually do anything until a call to acts_as_duration?

Would really appreciate any thoughts on the best way to architect this.

That sounds like a good way to start. Another thing you might try is to look at how another gem “decorates” values. The money-rails gem has its monetize method money-rails/monetizable.rb at main · RubyMoney/money-rails · GitHub which loops over the arguments passed to it and uses define_method internally to create the predictably named getters and setters for the attribute.

That’s a long method, and it does a lot more than what you’re trying to do, but if you squint and read through it a few times, the pattern should become clear.

As far as including Durationable, that’s probably just fine for this stage of your module. If you were to turn it into a Gem, then you’d want to allow it to register on the ActiveModel::Base so any of your classes could just use it without that extra line. Especially if you write it in such a way that it doesn’t do anything until you call your modifier class method to set it loose on a set of attributes…

Walter

1 Like

Since in Ruby a module is just an instance of class Module you can also just use normal object-oriented mechanics to have a constructor for your module which takes an argument allowing multiple different instances of the same class of module.

I wrote an article a while back about this but the end result might end up looking like:

class Task < ApplicationRecord
  include Durationable.new :booked
  include Durationable.new :actual
end