Class attributes and ActiveSupport

It’s not uncommon case to provide a class-level accessors for some kind of configuration. For instance, ActiveRecord has multiple class-level settings:

ActiveRecord::Base.inheritance_column = "type"
ActiveRecord::Base.schema_migrations_table_name = "schema_migrations"

Some of them are model-level:

class Post < ActiveRecord::Base;end

Post.ignored_columns = ["legacy_column"]

How do you implement them? You can start with the vanilla Ruby implementation:

module Configurable
  def setting
    @setting
  end

  def setting=(value)
    @setting = value
  end
end

class Post
  extend Configurable
  self.setting = "default"
end

Post.setting # => "default"

However, the default value won’t be available in the subclass:

class Article < Post;end

Article.setting # => nil

You can fix it by changing the setting accessor:

module Configurable
  def setting
    if defined?(@setting)
      @setting
    else
      superclass.setting
    end
  end
end

Now the setting value of parent class will also be accessible in the child class. Unfortunatelly, this snippet doesn’t scale if you are going to have a dozen of class accessors.

There comes ActiveSupport with mattr_accessor and class_attribute.

mattr_accessor defines both class and instance accessors for class attributes (docs)

require 'active_support/core_ext/module/attribute_accessors'

class Person
  mattr_accessor :hair_colors
end

HairColors.hair_colors = [:brown, :black, :blonde, :red]
HairColors.hair_colors # => [:brown, :black, :blonde, :red]
Person.new.hair_colors # => [:brown, :black, :blonde, :red]

Keep in mind that if a subclass changes the value then that would also change the value for parent class. Similarly if parent class changes the value then that would change the value of subclasses too.

class Male < Person;end

Male.hair_colors = [:blue]
Person.hair_colors # => [:blue]

Usually this is not the desired behaviour and you’d want subclasses not to change the parent class values. There comes class_attribute (docs).

It declares a class-level attribute whose value is inheritable by subclasses. Subclasses can change their own value and it will not impact parent class.

require 'active_support/core_ext/class/attribute'

class Base
  class_attribute :setting
end

class Subclass < Base;end

Base.setting = true
Subclass.setting            # => true
Subclass.setting = false
Subclass.setting            # => false
Base.setting                # => true

When I’ve been reading class_attribute implementation it surprised me how elegant the writer method works. You’d probably expect that it stores the value in the instance variable, like we did in the vanilla Ruby solution above.

The code from active_support/core_ext/class/attribute.rb:

def class_attribute(*attrs)
  # ...
  attrs.each do |name|
    # ...
    define_singleton_method("#{name}=") do |val|
      singleton_class.class_eval do
        define_method(name) { val }
      end
    end
  end
end

It took me a moment to understand why writer method does class_eval and define_method. Then I realized that it simply declares a reader method that returns the new value.

Normally this would hurt the performance because adding a method resets the method cache, but in case of a class-level attributes you only change it once or twice on the application start. In this case it makes more sense to declare a method dynamically rather than use instance variables.


I really liked this trick when I found it, and it’s the main reason why I wrote this post. Even if you avoid using ActiveSupport in smaller non-Rails projects, now you know multiple options of implementing class-level attributes in Ruby.

Comments

comments powered by Disqus