Artur Roszczyk

March 12, 2024

Today, I would avoid metaprogramming at all costs

Ruby is a powerful, dynamic language. It allows the developer to accomplish things that would be unbelievable in other languages. However, with great power comes great responsibility. I know, it's cliche. But hear me out. Today, I read a post by Ahmed Nadar on refactoring helpers in his Rails project. Please go and read it now to get a taste of the power of metaprogramming in Ruby. 

This article reminded me how my thinking changed and how I structure my code these days. First, I would like to emphasise that this post is not to undermine Ahmed's experience and approach. I appreciate various opinions as ways to learn from each other.

Ahmed, in his post, proposed a series of refactorings to a Ruby on Rails helper class. The improvements mainly relied on metaprogramming methods such as method_missing and constantize. This approach helped to achieve the goal defined as:

I no longer had to manually update the helper for each new component, and I could easily handle exceptions to naming conventions 

However, I would set priorities to achieve code maintainability differently. First, I care much about code discoverability. It should be easy for a developer to trace method calls across the stacktrace. Next, I would work on the least surprising code for the potential reader, not prioritizing DRYness too much. Metaprogramming adds a layer of indirection to the codebase, requiring one additional cognitive leap to get what the code does. It also reduces the grepabiility.

Following my priorities, I would refactor the mentioned ViewHelper in the following way:

module RapidRailsUI
  module ViewHelper
    def railsui_button(*)
      railsui_component ButtonComponent, *
    end

    def railsui_icon(*)
      railsui_component IconComponent, *
    end

    # more methods

    def railsui_component(component_class, *args, **kwargs, &block)
      render component_class.new(*args, **kwargs), &block
    end
  end
end

With this approach, I keep the grepability, and make life easier for LSP. Each method name to class mapping is apparent, and the code is reusable and extendable thanks to the logic being extracted to the railsui_component method. Perhaps this is a bit less DRY and is longer, but in my opinion, code should be like a fractal - a mosaic of repeating patterns. 

About Artur Roszczyk

Engineering manager by trade, Ruby on Rails enthusiast at heart.
Find me on Twitter – @sevos