Wednesday 5 September 2012

Ruby - Spying on instance variables

This week I've been working on some code to be executed on cucumber's hooks, due to the lack of useful (or easy to read) documentation I found myself using a debugger to find out what info I could get from the object passed to the "After" block.
Deconstructing the object allowed me to see it had some instance variables with info I needed, but I could not retrieve it in any way, what a bummer, "it's right there!" I thought, if the debugger can get it so can I.
Poking the object a little I found "instance_variable_get" method (which I hadn't used before nor ever paid attention to it), since there's no real private variables in Ruby, you can get any instance variable despite if there's a method for it or not.
Translating this new acquired knowledge into simple examples:


Simple instance variable spying


irb(main):001:0> class A irb(main):002:1> def initialize var irb(main):003:2> @var = var irb(main):004:2> end irb(main):005:1> end => nil irb(main):006:0> a = A.new 20 => #<A:0x56ee20fe @var=20> irb(main):007:0> a.var NoMethodError: undefined method `var' for #<A:0x56ee20fe @var=20> [...] irb(main):008:0> irb(main):009:0* irb(main):010:0* irb(main):011:0* var.instance_variable_get(:@var)
=> 20

This could have easily been avoided using "attr :var" on class declaration, and it's the result of bad encapsulation.
If class A is yours, then go change it so you can easily access vars you need, if it's not (comes from a gem, maybe) you can still change it, because ruby classes are always open (which is awesome), so just add "class A; attr :var;end" before instancing it and you're good to go.


Now, when you don't have control over the class nor when it's instantiated (like on cucumber's hooks) you can still use what's described above, but have the option of using instance eval to define a new method to access the variable you want, although I hear it's slower I didn't bother with benchmarks for this case.



Spying with instance_eval

irb(main):001:0> class A irb(main):002:1> def initialize var irb(main):003:2> @var = var irb(main):004:2> end irb(main):005:1> end => nil irb(main):006:0> a = A.new 20 => #<A:0x63fb050c @var=20> irb(main):007:0> a.instance_eval do @var; end => 20 irb(main):008:0> #or, if you need to do it often.... irb(main):009:0* a.instance_eval do def var; @var;end; end => nil irb(main):010:0> a.var => 20

Instance eval evaluates your block in the context of the instance, so you can run code on it like if it were on the inside, you even have access to "self".

No comments:

Post a Comment