- Read Tutorial
- Watch Guide Video
In this guide let's learn about another metaprogramming mechanism called define _method
that will allow you to dynamically create methods at runtime in a Ruby program.
We'll continue with our Author
class, and use a method called define_method
here.
class Author define_method("some_method") do puts "Some details" end end
Next, we'll call this method.
author = Author.new author.some_method
When you run this code, it'll print out the value "Some details".
Now, you may wonder how this is different from a regular method such as this one:
def some_method puts "Some details" end
The answer is: they are the same!
So, this is not the kind of implementation you'd use define_method
for in the real world. However I wanted to start with this as a base case so you can see the core functionality.
Now, let's look at a more practical example. Going back to our Author
class, let's imagine that we have different genres and we have to print out the details for each of the different genres. To do that, let's create a few methods inside our Author
class, like this.
class Author def fiction_details(arg) puts "fiction" puts arg puts "something else..." end def coding_details(arg) puts "coding" puts arg puts "something else..." end def history_details(arg) puts "history" puts arg puts "something else..." end end
Each of these methods simply print out the values based on the arguments that are passed to it. Let's call one of the methods now with the code,
author = Author.new author.coding_details("Cal Newport")
When you run this code, it will print out:
coding Cal Newport something else...
Though this implementation works, it's showcasing some poor programming practices because of the amount of identical code we are using. This is where define_method
is useful. A better implementation would be to utilize define_method
, which would look something like this:
class Author genres = %w(fiction coding history) genres.each do |genre| define_method("#{genre}_details") do |arg| puts "Genre: #{genre}" puts arg puts genre.object_id end end end author = Author.new author.coding_details "Cal Newport"
In this code, we are creating a variable called genres
. If you're not familiar with %w(...)
syntax, it is a way that Ruby allows us to create an array of strings without commas and double quotes. In the next line, we are iterating over the genres
array, and inside of the each
block we are calling define_method
. Inside of this block we are printing the name of the genre and the value of arg
. Lastly, we are generating a unique ID with the code genre.object_id
to show that the object values will be unique.
Essentially what this code is doing is it iterates over our genres
array, and based on the genre, it will dynamically create a method that will have the name of the genre followed by the word _details
. If you notice, the functionality is the same as before, but this time we have to define it only once, and obviously, the amount of code is greatly reduced.
Now if you run the code, the output will be:
Genre: coding Cal Newport 70338847194560
Let's add another line:
author.fiction_details("Ayn Rand")
And now the result is:
Genre: coding Cal Newport 70107179007240 Genre: fiction Ayn Rand 70107179007260
So, that's all working fine. See how easy and compact your code is when you use define_method
?
There's also another cool thing about this method. If you remember our method_missing
guide and how we had to integrate our 'respond_to?` method to get it working properly? You can call that method here like this.
p author.respond_to?("coding_details")
When you execute it, the program will output true
.
In the method_missing
guide we had to build another method simply to ensure that the respond_to?
method will work properly (which is key to us following best practices for metaprogramming). However, when we use define_method
, it's actually creating methods for us at runtime, and the respond_to
method comes by default.
At this point, I'd like to point out a subtle difference that exists between the methods define_method
and method_missing
in Ruby. Since define_method
creates the methods for us at runtime, we get the functionality of respond_to
method for free. On the other hand, the method_missing
method does not trigger a call until the method call has gone through the method call cycle with Ruby. For example, let's say we had a call for a method named summary
like this:
author.summary
Ruby will go to Author
class and look for a method called summary
. When it can't find it there, it'll go up the chain and look for this method in BasicObject
and all the other built-in classes that Ruby has to see if there is a summary
method somewhere that it can call. When Ruby can't find the method anywhere, it comes back to the Author
class and checks to see if there is a method_missing
definition, and if there is there a summary
method in it. This is why we had to add the respond_to
method earlier in order to get it working. With define_method
, we don't have to write any code for respond_to
as it works right out of the box for us.
However, the downside with define_method
is its limited flexibility. With method_missing
, you can do pretty much anything when there is a method call for a non-existent method, but with define_method
, you can't do anything other than defining methods from a pre-defined list of values. Also, we have to provide all the necessary details at the time of writing the code to ensure that define_method
works properly. However with missing_method
, you can decide what you want to do when the call comes in at runtime.
In short, you can use define_method
when you know what needs to be done. In fact, it's key use is to create methods for you at runtime based on the details you provide in the code - just like how we did. In this sense, it's a great way to DRY up your code and to have a nice code structure.