Ruby Metaprogramming - Creating Methods
Creating Methods
In this post I'll be discussing another aspect of metaprogramming in Ruby. The ability to create methods dynamically, during runtime. There are many reasons to do this, but one of them is to allow you to write generator methods to help you avoid writing repetitive code.
The Long Way
It's generally a good practice in Ruby to not access instance variables directly but instead through getter and setter methods. Unless something fancy is happening, these methods don't really do much other than get a value and set a value to the instance variable.
class Alpacadef initialize@nameenddef name@nameenddef name=(value)@name = valueendendbuddy = Alpaca.new("Buddy")buddy.name # Buddy
This is really annoying to write... and thankfully Ruby comes with an easy way to get this same functionality with only having to write a single line of code.
Enter attr_accessor!
Ruby comes with 3 methods to help you create instance variable getters and setters. attr_reader
creates a getter, attr_writer
creates a setter, and attr_accessor
creates both. To use this we just have to declare it at the top of our class, and Ruby will essentially create our getters and setters for us!
class Alpacaattr_accessor :namedef initialize(name)self.name = nameendendbuddy = Alpaca.new("Buddy")puts buddy.name # "Buddy"
Let's do this ourselves
Because Ruby it is a dynamic language that allows us to create (and remove) methods during runtime, let's try to essentially re-create what Ruby has done for us with the attr_accessor
code. We'll call it getset
, getter
, and setter
.
We can use the define_method
method to dynamically create a new method. It accepts the name of the method, and we pass it a block of code, which will become the body of our new method.
To complete this code, we'll also use the instance_variable_get
and instance_variable_set
methods to dynamically get and set our instance variables directly.
# Let's create a base class to extend from.# This class contains the code generator methods that we'll be using.class Basedef self.getset(*args)args.each do |field|getter(field)setter(field)endenddef self.getter(*args)args.each do |field|define_method(field) doinstance_variable_get("@#{field}")endendenddef self.setter(*args)args.each do |field|define_method("#{field}=") do |value|instance_variable_set("@#{field}", value)endendendend# Now let's create a class and utilize our getset generatorclass Alpaca < Base# We'll create accessors for :name and :agegetset :name, :agedef initialize(name, age)self.name = nameself.age = ageendendbuddy = Alpaca.new("Buddy", 24)# Let's call our methods and make sure they return what we expectputs buddy.name # Buddyputs buddy.age # 24# Let's see if our object responds to the new methods we createdputs buddy.respond_to?(:name) # trueputs buddy.respond_to?(:name=) # true
Concluding thoughts
If you're a Rails developer, you've probably called methods which were dynamically generated before whether you knew it or not. Your Rails models have had methods created dynamically for each attribute on that model. This is the reason why you can call @user.name
without having to create a getter method for the name
attribute.
Another example of a way I've used this technique before is when working with money fields in my Rails models. The values are stored in cents in the database, but often you just want to display the dollar equivalent to your user. To do this we have a code generator to define money_fields
which creates methods for each of the fields and provides methods to easily get/set the values in either cents or in dollars.
I recommend Metaprogramming Ruby 2 to go deeper on this subject.