Metaprogramming in Ruby lib: beauty vs usability > stdout.in Ievgen Kuzminov IT blog

Metaprogramming in Ruby lib: beauty vs usability

Jun 30, 2016, 6:36:20 AM

Recently I have evaluated different HTTP request wrapper libraries for Ruby project. Took 3 most popular: Faraday, RestClient, HTTParty. And found an interesting fact that illustrates very common issue in the world of Ruby libs.

Metaprogramming is used for the sake of Metaprogramming - "because it is Ruby and I can do it like this...". Not to make end-user developer life simpler.

Let's see the main purpose of HTTP request lib - is to send HTTP request (GET, POST, etc.). And the main purpose should dictate public interface of the lib classes. It should have a generic method to do any kind of HTTP request + shortcut methods to do the most common requests, like GET and POST.

That is what all these libs have

# kind-a this
connection.get(url)
# or this
Client.post(url, body_params_hash)
# it doesn't really matter is it static or object method

The issue is - these methods get/post are not real. For some reasons lib author try to spare 10 lines of code, but to use "cool Metaprogramming approach". The consequences of this - I am as an end-user of this lib, can't Ctrl+Click on post method in my IDE to see how it is implemented and what's happen inside. Because this method does not really exist in class, it appears in runtime after class_eval or define_method. I can't even find the place in Gem where it defined without getting deep in internal implementation. That is a bit frustrating for me. These 3-5 methods are the most important parts of such a lib, I don't care about all other parts, but exactly these parts are hidden from me. And for what good reason?

Proof Faraday

 %w[get head delete].each do |method|
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{method}(url = nil, params = nil, headers = nil)
          run_request(:#{method}, url, nil, headers) { |request|
            request.params.update(params) if params
            yield(request) if block_given?
          }
        end
      RUBY
    end

Proof RestClient

POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']
...
POSSIBLE_VERBS.each do |m|
  define_method(m.to_sym) do |path, *args, &b|
    r[path].public_send(m.to_sym, *args, &b)
  end
end

Only HTTParty has explicit declaration of these methods . Thank you guys, you rock! :)

...
    def get(path, options = {}, &block)
      perform_request Net::HTTP::Get, path, options, &block
    end
...

As we can see it is 3-5 line methods. There is no any problem to write them explicitly and hardcode method name inside them + extract all repeating lines inside a separate method. That is absolutely normal code design approach. Instead of sacrificing code simplicity and readability of favor of code size.

If you are a visualist, please compare these 2 approaches

 %w[get head delete].each do |method|
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{method}(url = nil, params = nil, headers = nil)
          run_request(:#{method}, url, nil, headers) { |request|
            request.params.update(params) if params
            yield(request) if block_given?
          }
        end
      RUBY
    end

vs

 def perform_request(method, url = nil, params = nil, headers = nil)
    run_request(method, url, nil, headers) { |request|
      request.params.update(params) if params
      yield(request) if block_given?
 }
 end

def get(url = nil, params = nil, headers = nil)
    perform_request(:get, url, params, headers) {yield}
end

def head(url = nil, params = nil, headers = nil)
    perform_request(:head, url, params, headers) {yield} 
end

def delete(url = nil, params = nil, headers = nil)
    perform_request(:delete, url, params, headers) {yield} 
end

I am making my bet on the second one.

comments powered by Disqus
Ievgen
Kuzminov "iJackUA"
Web Team Lead
at MobiDev (Kharkiv, Ukraine)
Code in Ruby and Elixir, but still love PHP. Explore ES6 and Vue.js. Explore databases, use Ubuntu and MacOS, think about IT people and management

Notes


At last I have just got the difference between Dependency Injection and Service Locator (I really hope so :) ). Hallelujah ! And now it is clear what is Inversion of Control. That worth all these years in programming...



First "real" visitors on blog from search and soc nets today (not only my friends and colleagues) ! Hello Italy and Norway ! :)



Dev stack abbreviations became more and more popular (MEAN vs LAMP etc.), StrongLoop introduces BACN - stack for mobile applications.