Home > other >  Is it possible to make a class_attribute private or protected in Rails?
Is it possible to make a class_attribute private or protected in Rails?

Time:08-24

I have a situation where I need a variable (e.g. foo) which is used internally within a class (e.g. Parent) and sub-classes (e.g. Child). In my case, I'm using Single Table Inheritance. Child should inherit the variable foo from Parent if Child does not set foo. If Child does set foo, that value should only change foo for Child but not in Parent. This seems like a clear cut case for class_attribute.

The problem is that class_attribute is not respecting a private or protected keyword, so foo can be accessed from outside of Parent and Child, e.g. from the console:

# Now
Parent.foo
=> "Foo"
Child.foo
=> "Foo"/"Bar"

# What I want
Parent.foo
=> 'method_missing': protected method 'foo' called for Parent:Class (NoMethodError)
Child.foo
=> 'method_missing': protected method 'foo' called for Child:Class (NoMethodError)

I'm wondering how to achieve the setup I've described above, including solutions other that class_attribute. Note, setting instance_accessor, instance_writer, and instance_reader had no effect. Also note that a class instance variable will not work since Child will not inherit a value for foo from Parent.

class Parent < ApplicationRecord
  private
  class_attribute :foo, default: "Foo" 
end

class Child < Parent
  # Child may or may not set :foo. If not, the value should be inherited from Parent.
  # Setting :foo in Child should NOT change the value of Parent, hence using class_attribute.
  # class_attribute :foo, default: "Bar"
end

CodePudding user response:

I think this meets your needs, if I have understood them correctly! Take a look:

class Parent
  @@foo = "parent foo"
  class << self
    private
    def foo
      @@foo
    end
  end
end

class Child < Parent
  @@foo = "child foo"
end

class AnotherChild < Parent
  @@foo = "another child foo"
  class << self
    def foo
      @@foo
    end
  end
end

puts Parent.foo # private
puts Parent.send(:foo) # private, but accessible via .send
puts Child.send(:foo) # inherits from Parent, => "parent foo"
puts Child.foo # private method
puts AnotherChild.foo # defined in subclass => "child foo"

The class attributes (@@...) are accessed via getters def foo..., and the getters are constrained by the private keyword to control the access as you expressed.

CodePudding user response:

The following should work. It uses class instance variables instead of class variables, which can be a bit tricky to implement.

class Parent < ApplicationRecord  
  @foo = "Parent"

  class << self
    private

    # This will try to get the class instance variable @foo on the current class,
    # trying each parent until base_class is reached.
    def foo
      if self == base_class
        @foo
      else
        @foo || superclass.send(:foo)
      end
    end
  end
end

class Child < Parent
  @foo = "Child"
end

class DeepChild < Child
end

puts Parent.foo # private method error
puts Parent.send(:foo) # private, but accessible via .send
puts Child.foo # private methode error
puts Child.send(:foo) # private, but accessible via .send. Child's @foo || Parent's @foo
puts DeepChild.foo # private method error
puts DeepChild.send(:foo) # private, but accessible via .send. DeepChild's @foo || Child's @foo || Parent's @foo
  • Related