Little metaprogramming for neater API

programming ruby

Recently I noticed repeated pattern I use for some parts of code. It looks like this:

class RiceCooker < Appliance
  def cook_rice(rice_type)
    # some code
  end

  def self.cook_rice(rice_type)
    new.cook_rice(rice_type)
  end
end

Wait, what?

Ok, so my goal is to have clean API.

With first code, I need to call RiceCooker.new.cook_rice(:sushi). But as there are no parameters for the actual class, this seems unnecessary. Therefore I create “wrapper class method” that just instantiates RiceCooker and calls the method itself.

“Wouldn’t it be better to use Module instead of Class”, I hear you ask?

In some cases, definitely. But here (well, in my actual code), I’m not sure. Maybe it’s just my obsession with OOP, and I’m unable to write more functional code inside module, but I use instance variables and methods in my parent class, and I’m not sure how would I do that in non-OOP way.

So, until I decide to learn to use Modules properly and give it a shot, I will stick with this.

But this brings different issue - I repeat this “wrapper class method” often enough to think “this is boring boilerplate, I don’t wanna do this” and start thinking about “smart” ways to simplify this.

Well, thanks to great metaprogramming capabilities of Ruby, I can!

My idea for the API (inspired by other libraries, mostly Rails) is this:

class RiceCooker < Appliance
  def cook_rice(rice_type)
    # some code
  end

  create_class_method :cook_rice
end

I’m not actually decided on the method class, but something like this, you get the idea.

So, how to make this work?

With some searching, I came to define_singleton_method method. This allows me to write this code that generates methods dynamically.

class Appliance
  def self.create_class_method(method_name)
    define_singleton_method method_name do |*args, **kwargs, &block|
      # I'm adding args, kwargs and block for most general approach, for my usecase I could get away with |arg|, as my methods look like that.
      new.send(method_name, *args, **kwargs, &block)
    end
  end
end

Ok, I can do this, but should I? Metaprogramming can be tricky, especially if I don’t understand deeper workings here. I should write some tests for this, to at least be sure it works like intended for all kinds of methods. Some day, that is.