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.
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.