Easy Metaprogramming For Making Your Code Habitable

by Jim

In my last message, I wrote about simplifying the code you write by using the Forwardable library. It allows you to represent ideas with succinct code:

delegate [:sanitize, :link_to, :paginate] => :view

In one short line we’re able to show that certain messages and any arguments will be forwarded to another object.

The Presenter class we’ve been working with also has convenient methods created based upon the name of any inherited classes. A UserPresenter will have user and user= methods mapped to __getobj__ and __setobj__ by an easy to follow convention of choosing a name that makes sense.

These are ways that I’ve found in my own code to make it more habitable for me and my team. With as little effort as possible, we should feel at home understanding and changing behavior of the system.

Make your own shortcuts

I have had projects where data would be displayed to a user but may have come from multiple different sources. A user record might be imported from a third-party system and have data that needs to be updated. Or a person in a system might have one or more public profiles from which we need to choose to display information.

If an updated profile has good information we want to show that, otherwise, we’ll show some other imperfect information.

Here’s an example of an approach I took recently. I needed a way to specify that my data should first come from a profile and then fallback to the user record if no data was available. This is what it looked like:

class UserPresenter < ::Presenter
  delegate [:profile] => :user
  maybe :full_name, :address, :from => :profile
end

I went with the name maybe but you might find that something else is clearer for you.

Here’s how I made it work:

class Presenter < SimpleDelegator
  def self.maybe(*names)
    options = names.last.is_a?(Hash) ? names.pop : {}
    from = options.fetch(:from)
    class_eval names.map{|name|
      %{
        def #{name}
          #{from}.public_send(:#{name}) || __getobj__.public_send(:#{name})
        end
      }
    }.join
  end
end

This creates a maybe class method which looks for a collection of method names and an hash in which it expects to find :from. Next it opens up the class (which would be UserPresenter in this case) with class_eval and defines methods based upon the provided string.

The methods are simple and they are equivalent to something like this:

class UserPresenter < ::Presenter
  def full_name
    profile.full_name || user.full_name
  end
end 

Although the full methods are short, using this technique allowed me to simplify the code necessary to handle the concept. Additionally, as I needed, I was able to make changes where a blank, but non-nil value wasn’t considered a valid.

Changing the behavior to avoid nil and blank strings was easy:

def #{name}
   value = #{from}.public_send(:#{name})
   (!value.nil? && value != '' && value) || __getobj__.public_send(:#{name})
end

Or if you have ActiveSupport:

def #{name}
   #{from}.public_send(:#{name}).presence  || __getobj__.public_send(:#{name})
end

Beware of indirection

This code made sense in my project, but for yours it might not. If I were to only use this for the full_name and address methods then I’d probably be making my project less habitable. A layer of indirection, especially when woven through metaprogramming, can be a painful stumbling block to understanding the code later. Be careful to think about how or if you need to apply code for a pattern like this.

Get more tips like this by signing up for my newsletter below.

Clean Up Your Code

If you liked this post and want more like it, get periodic tips in your inbox by leaving your email address below.

To get more information about cleaning up your objects, classes, and code, then check out Clean Ruby, an ebook which will describe ways to keep your code clean, maintainable, and focused on business value. Make your OOP more obvious, easier to understand, easier to test, and easier to maintain.



Find more in the archives

1999 - 2014 © Saturn Flyer LLC 2321 S. Buchanan St. Arlington, VA 22206

Call Jim Gay at 571 403 0338