hw-ruby-advance/ruby_metaprogramming.md

58 lines
1.7 KiB
Markdown

[Back to main README](README.md)
## Ruby metaprogramming
Specs: `spec/attr_accessor_with_history_spec.rb`
Skeleton: `lib/attr_accessor_with_history.rb`
In lecture we saw how `attr_accessor` uses metaprogramming to create
getters and setters for object attributes on the fly.
Define a method `attr_accessor_with_history` that provides the same
functionality as attr accessor but also tracks every value the attribute
has ever had:
```ruby
class Foo
attr_accessor_with_history :bar
end
f = Foo.new # => #<Foo:0x127e678>
f.bar = 3 # => 3
f.bar = :wowzo # => :wowzo
f.bar = 'boo!' # => 'boo!'
f.bar_history # => [nil, 3, :wowzo]
```
(Calling `bar_history` before `bar`'s setter is ever called should
return `nil`.)
History of instance variables should be maintained separately for each
object instance. that is:
```ruby
f = Foo.new
f.bar = 1 ; f.bar = 2
g = Foo.new
g.bar = 3 ; g.bar = 4
g.bar_history
```
then the last line should just return `[nil,3]`, rather than
`[nil,1,2,3]`.
If you're interested in how the template works,
the first thing to notice is that if we define
`attr_accessor_with_history` in class `Class`, we can use it as in the
snippet
above. This is because a Ruby class like `Foo` or `String` is actually just an
object of class `Class`. (If that makes your brain hurt, just don't worry
about it for now. It'll come.)
The second thing to notice is that Ruby
provides a method `class_eval` that takes a string and evaluates it in the
context of the current class, that is, the class from which you're
calling `attr_accessor_with_history`. This string will need to contain a
method definition that implements a setter-with-history for the desired
attribute `attr_name`.