Extend for one block call only
-
13-09-2019 - |
Question
I have a class that contains some private attributes. What I would like to do is to dynamically add some setters for these only for the execution of a specific block.
Example of what I would like to be able to:
class Content
attr_reader :a, :b
def initialize
@a = 1
@b = "plop"
end
def set(&block)
extend(Setter)
instance_eval(&block)
unextend(Setter) ????
end
module Setter
def a(value)
@a = value
end
def b(value)
@b = value
end
end
end
content = Content.new
content.set do
a 2
b "yeah!"
end
content.a # should return 2
EDIT: Thanks for the great answers so far. I clarified the question because I actually need to define attribute readers in the class itself that may conflict with the setters defined in the module. I forgot about this part when posting the question. (It was late ^^)
CLARIFICATION: This class is intended for a DSL to write a configuration file. It is targeted at non-developer so the less operators, the better.
I currently implement this using a proxy class that instance_eval
the block but I have to mess with instance_variable_set
in order to set the values and I don't like it. I am just trying another way to see if I can make my code more readable.
Solution
def set(&block)
extend(Setter)
instance_eval(&block)
Setter.instance_methods.each do |m|
instance_eval "undef #{m}"
end
end
I don't know of any method that would do that for you although there might be something.. This should do the job though, by finding all the instance methods of Setter and undefining them in Content.
OTHER TIPS
There's no native way to "unextend" modules in Ruby. The mixology gem implements this pattern as a C (and Java, for JRuby) extension, creating mixin
and unmix
methods. It appears you may need to apply a patch if you need Ruby 1.9 support, however.
If you'd prefer to avoid using third-party libraries, another approach might simply be to make the setters private:
class Content
def initialize
@a = 1
@b = "plop"
end
def set(&block)
instance_eval(&block)
end
private
def a(val)
@a = val
end
def b(val)
@b = val
end
end
content = Content.new
#This will succeed
content.set do
a 2
b "yeah!"
end
# This will raise a NoMethodError, as it attempts to call a private method
content.a 3
You could use _why's mixico library (available on github)
It would let you do this:
require 'mixology'
#...
def set(&block)
Setter.mix_eval(Setter, &block)
end
The mixology gem does much the same thing, just slightly differently.
if you're feeling in an experimental mood also check out: dup_eval
It's similar in some ways to mixico but with some interesting extras (object2module)