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.