Using cattr_accessor with caution

Setters and getters provide a great deal of convenience and flexibility with classes. Today, lets take a quick look into cattr_accessor in Rails and possible things to watch out for.

cattr_accessor which is also an alias to mattr_accessor (c = Class, m = Module, you get the idea) is described as:

Defines both class and instance accessors for class attributes.

Which makes it convenient to use the accessor under different situations. Something like this:

class A
cattr_accessor :foo
end
A.foo = 10
A.foo
=> 10
A.new.foo
=> 10

Lets dive in further, and see how it works with subclasses

class A
cattr_accessor: foo
end
# Subclass
B = Class.new(A)
A.foo = 10
A.foo
=> 10
B.foo
=> 10
# Setting value of cattr_accessor from a subclass
B.foo = 20
=> 20
A.foo
=> 20

Yep, we can freely update the attribute from its subclass. This is by design and it feels very interesting. However, in large codebases, where you are DRY-ing up your code or subclassing the parent class to extend functionalities, this behavior of cattr_accessor can start to feel risky (scary?). It has too much influence and power :).

Perhaps, if a class is defining a cattr_accessor, from design principles perspective, it may make the best sense to never subclass it and to avoid scenarios of accidentally updating the attribute. Which can get pretty hard to debug.

Here is a another simplifeid example of cattr_accessor I came across in an open source project (now deprecated), which showed a pretty risky implementation.

module A
def self.const_missing(name)
klass = OtherClass
klass.foo = "A"
self.const_set(name, klass)
klass
end
end
module B
def self.const_missing(name)
klass = OtherClass
klass.foo = "B"
self.const_set(name, klass)
klass
end
end
class OtherClass
cattr_accessor :foo
end

Here, we are only setting the attribute when a new constant is defined. Running this code in Rails application’s runtime and interchangeably using A and B modules introduced very unexpected behavior in application logic.

A::ConstFoo.foo
=> A
B::ConstFoo.foo
=> B
A::ConstFoo.foo
=> B
B::ConstFoo.foo
=> B

The expected value is not returned and the attribute value is from the last time const_missing was called. That being said, I personally try to stay away from cattr_accessor. Unless, there is a strong use case and a reason to use it.

Feel free to share about your experience with cattr_accessor or if there is something you’d have done differently :).

Cheers!


Originally published at ts.shayon.me.