Search code examples
ruby-on-railsrubydefault

Creating attribute accessors with optional defaults


I have a Rails application with a recurring need for setting default attributes. Sometimes the user will supply values for the attributes which will be respected, but in other circumstances either the model or the user might desire that these attributes are overridden with default values disregarding original values.

I guessed that this problem called for a banged (!) and non-banged method for setting default values allowing the user and program to switch to the appropriate state. The non-banged setter will only set default values when they are not nil whilst the banged version will always overwrite the attributes with defaults. The difference is minor:

class BangDiBang
  attr_accessor :value

  def set_default
    self.value ||= do_some_suff_to_determine_default_value
  end

  def set_default!
    self.value = do_some_suff_to_determine_default_value
  end

  ...
end

The issue with this code is that if I had a bunch of variables to set, I would end up repeating the same code twice for each variable.

My question is how to partial out this code? Saving the logic in one method and having two methods set_value and set_value! calling the central logic with the different assignment operators.

I have conjured one solution: write the central logic as text, replace the assignment operation from the setter methods and evaluate (but this does not feel right). How do I not repeat myself?


Solution

  • The way you're going about this isn't going to work for multiple params since you're calling set_default but not specifying which variable. You'll want to define behavior for each variable, ideally. There are libraries that handle this sort of thing, but you can roll your own pretty easily:

    class Example
      def self.default_param(name, default_value)
        define_method("default_#{name}") { default_value }
    
        define_method(name) do 
          instance_variable_get("@#{name}") || default_value
        end
    
        attr_writer name
      end
    
      default_param :foo, 'foo default'
    end
    
    ex = Example.new
    ex.foo #=> "foo default" 
    ex.foo = 'bar'
    ex.foo #=> "bar"
    ex.default_foo #=> "foo default" 
    

    I've renamed set_default and set_default! to be more clear: for each variable with a default value, three methods are created (example using foo as the variable name):

    1. foo — returns the value of @foo if it is truthy, and default_foo otherwise
    2. foo= — sets the value of @foo
    3. default_foo — returns the specified default

    You could compartmentalize and dry up some of the code above further, creating a default_params (plural) method to take a hash, extracting the class macro to a concern:

    module DefaultParams
      def default_param(name, default_value)
        define_method("default_#{name}") { default_value }
    
        define_method(name) do 
          instance_variable_get("@#{name}") || default_value
        end
    
        attr_writer name
      end
    
      def default_params(params)
        params.each { |default|  default_param(*default) }
      end
    end
    
    class Example
      extend DefaultParams
      default_params foo: 'default foo', bar: 'my favorite bar'
    end