In the last few weeks I've had the chance to glance into several different codebases, some written years ago by devs in their Rails-infancy (rainfancy?).
Doing the travelling codes-man like that is a golden opportunity to see what people's stumbling blocks are and how a few easy tricks can improve the code substantially. What follows is not your new cutting edge coffe making kitchen sink script-fu, but a rather dull list of everyday Ruby that I learned people need to learn.
One-stop requires
This is a short one: requires go into the environment.rb.
Really, don't use it elsewhere. I've seen people reason along the lines of "I keep the 'require hpricot' in my xe.com currency rates scraping class as to keep things local and closely knit". There is some truth to that argument and if a gem is only used by a rake task, maybe it even saves us a second during app startup and some RAM. Still, load order issues, poor readability and maintainability makes it a real pain to deal with down the road.
Trust me: don't. Put it in environment.rb with a comment describing its purpose in your project.
Whenever possible use the "config.gem". Advantages:- allows you to specify the lib name even if the gem name is different (e.g. for github gems)
- allows you to require sub libs
Not used very often perhaps but put to good use by e.g. the right_aws gem where you can choose to load just the SQS part with:
config.gem 'right_aws', :lib => 'sqs/right_sqs'
or maybe you just wanted the SDB part?
config.gem 'right_aws', :lib => 'sdb/right_sdb_interface'
I personally believe this particular feature is more confusing than helpful, but it's good to know it's there.
- allows you to specify the library version
Towards the end of the development cycle, before rolling out to production, gems should generally be frozen. An alternative is to define exact versions in the config. I generally advise doing both as having the environment.rb as the one-stop source for gem requirements is a boon for everybody involved, capistrano scripts or new devs on your team.
- all in one place, easy to find out requirements
Singleton methods (creation)
Ruby provides three (common) ways to define a singleton method:
Explicitly name the class
class RapidInterventionGroupTeam < ActiveRecord::Base def RapidInterventionGroupTeam.load_group_allocations_and_teams(project_id, curr_user) # Code end end
PROs: hmm, dunno. Makes it easy to know what class you're looking at if the source code file is very long?
CONs: long, ugly
-
Use self
class RapidInterventionGroupTeam < ActiveRecord::Base def self.load_group_allocations_and_teams(project_id, curr_user) # Code end end
PROs: easy to spot here-comes-a-singleton-marker. Some people like to use the 'self' keyword. I call them self-ish people. Sorta short.
CONs: I think it's ugly. For more than two singletons that repeating 'self' annoys me.
-
Use the metaclass
class RapidInterventionGroupTeam < ActiveRecord::Base # ========================== # = Class methods go here! = # ========================== class << self def load_group_allocations_and_teams(project_id, cur_user) # Code end def team_allocations # Code end def group_allocations # Code end end # ============================ # = Instance methods go here = # ============================ def beef_cow_and_fowl # Code end end
PROs: shortest. Encourages devs to gather singletons in one spot, and perhaps they will notice when it gets out of hand and start creating mixins. Maybe. Also allows devs to show off their Ruby metaclass knowledge at bars. Maybe.
CONs: that class << self is funky and if funky doesn't rub well with you (or your boss) then I guess it's no good.
I personally favor c) whenever the number of class methods goes above two or three; b) is fine when there are just a few of them.
Invoking singleton methods
In Ruby there are many ways to invoke a class method. A few common ones:
-
Name the class explicitly:
user = User.find(123) Predators.feed_to_wolves(1, user)
-
Use a helper instance method
class Steak def self.logger @logger ||= Logger.new('log/steak.log') def def logger self.class.logger end def beef logger.debug "MONDAY BEEF!" beef_for!(:monday) end end
Using an instance method this way to invoke a class level method is of course useful only if you invoke it very often and readability becomes very important. In the above example we're also memoizing (caching) the class level Logger object for fast access.
When used from an instance of the class, use the context: self.class
class Predators < ActiveRecord::Base
def self.feed_to_wolves(pack_id, meat)
# code
end
# we know how we were born...
def feed_us!(meat)
self.class.feed_to_wolves(id, meat)
end
end
I personally try 1) to avoid singletons altogether, 2) use method b) whenever possible and 3) start asking myself hard and awkward questions when doing a) more than 3 times a day. Too many singletons might be a codesmell.
Multiple return values
First an example of how people often fake multiple value returns in Ruby:
class Something
def many
[calc_monday_beef, calc_total_beef]
end
end
s = Something.new
retval = s.many
DON'T: when coding a method and you realize you need to return more than one value from a method, you should stop and think. You're probably doing something wrong.
This is actually the reason why Ruby does not provide a way to return multiple values.
If you're sure you know what you're doing and want to return more than value here are some tips:
-
do NOT do what I did in the example.
-
use descriptive variable names. "retval" above sucks. Use Ruby multiple value assignments like so:
>> cows, sheep, fish = [1, 5, 3] => [1, 5, 3] >> cows => 1 >> sheep => 5 >> fish => 3
Use this to assign multiple return values to local variables with helpful names:
s = Something.new monday_beef, total_beef = s.many
-
c) if you're returning many values and you don't know beforehand how many values are coming back, use the splat operator (*):
>> me, *others = [:abe, :bob, :caesar, :donald] => [:abe, :bob, :caesar, :donald] >> me => :abe >> others => [:bob, :caesar, :donald]
-
d) avoid using array indexes and prefer using Array#first, Array#last:
-
BAD:
s = Something.new retval = s.many Beef.find(retval[0], :limit => retval[1])
-
STILL BAD SOMEWHAT BETTER:
s = Something.new retval = s.many Beef.find(retval.first, :limit => retval.last)
Why better? Because you communicate to the reader that that 'retval' variable only have two values. (Yeah, it still sucks I know)
-
ALSO BAD BUT SOMEWHAT-ER BETTER STILL:
s = Something.new monday_beef, beef_count = s.many Beef.find(monday_beef, :limit => beef_count)
-
GOOD:
s = Something.new Beef.find(s.monday_beef, :limit => s.total_beef_count)
-
Drop the return
Use the fact that Ruby methods return the value of the last expression. The only place where I think an explicit return is legit is when we want to return early and that's a performance optimization and as we know, premature optimization is the root of all evil, so unless you have benchmarks at hand proving the need for speed, just don't user 'return'.
-
Instead of:
def course_description retval = course_shortname retval = course_shortname + "(#{project.shortname})" if project return retval end
-
Do:
def course_description course_shortname + (project ? "(#{project.shortname})" : '') end
Enumerable
Learn to use and love ruby Enumerable module. It's really really useful.
-
Instead of:
def Vacation.holidaytype_selectbox types = Array.new for vt in VacationTypes types << vt[:name] end @holidaytype_selectbox_model ||= types end
-
Use:
def self.holidaytype_selectbox @holidaytype_selectbox_model ||= VacationTypes.map(&:name) end
-
Instead of:
for result in @candidate.results do result.destroy end
-
Use:
@candidate.results.each(&:destroy)
-
Instead of:
@approved = 0 unless @do_it.nil? @events = Event.hourreporting_entries(@person.user_id, params[:projectid], @week.first.date, @week.last.date) for e in @events if e.approved == 1 @approved += 1 end end end
-
Do:
if @do_it @events = Event.hourreporting_entries(@person.user_id, params[:projectid], @week.first.date, @week.last.date) @approved = @events.sum{|e| e.approved ? e.approved : 0} end
The Array#sum method is a Rails addition.
In the app where I spotted the above, the default value for the 'approved' field is set to NULL. Had the default value been 0 instead, we could have written the above in an even shorter way:
@events.sum(&:approved)
Truth
In Ruby, everything except false and nil evaluates to true. For realz.
-
Instead of:
if options[:add] == true && check_id.to_i != 0 meanie = MeanGuy.find(check_id) if meanie.reason != '' && !meanie.reason.nil? && meanie.type == MeanGuy.allowed_types[0] # Code end end
-
Do:
if options[:add] && !check_id.zero? meanie = MeanGuy.find(check_id) if !meanie.reason.blank? && meanie.type == MeanGuy.allowed_types.first # Code end end
When chaining conditional checks like above with "&&", remember that Ruby will stop the checks at the first failure, so always put the 'cheapest' checks first (e.g. put any checks that require a database query last).
In the above snippet we see the very common "!something.blank?" idiom. I personally really don't like the prepended "!" and always try to avoid it. Hurts my eyes and makes me stumble while reading. Ideally the above should be:
if meanie.reason && meanie.type == MeanGuy.allowed_type
but if the meanie reason is allowed to be the empty string -- which Ruby will consider True -- then we need the explicit blank?-check.
Unrelated neat trick using &&:
-
Instead of:
def safe_death(dude) if dude if dude.destroy if DudeMailer.deliver_destruction_notification Call.his_mum end end end end safe_death(Dude.first)
-
Do:
def safe_death(dude) dude && dude.destroy && DudeMailer.deliver_destruction_notification && Call.his_mum end safe_death(Dude.first)
-
Instead of:
if @person && @person.active == true && @person.projects.include?(:beef_on_monday) && (@person.type == :human || @person.type == :dog) # Code end
-
Do:
class Person def active_monday_beefer? actvive? && member_of?(:beef_on_monday) && (human? || dog?) end end if @person.active_monday_beefer? # Code end
This works well only if you don't care about handling any error conditions and success depends on *all* method calls being successful.
To improve readability in general create as many predicate methods as possible. They are short, cheap and really helps readability.
Method naming
When naming your methods, avoid prepending "get_" or "set_". The Ruby convention is that getters are just the value name and the setter has the "=" postfix. Avoid the quasi-get_'s too: load_, retrieve_, store_, put_. Sometimes they're ok, but just stop a second and think if:
store_incoming_beef()
is really really better than
more_beef_in_the_holds()
or
beefers_keepers()
or
not_pork_not_sheep_not_fowl_but?()
As always rules are made to be broken, judiciously. ;)
DON'T:
#get_premium, #get_unix_time_stamp, #load_dates, #retrieve_mums etc
Do:
#premium, #unix_timestamp, #dates, #mums, #beefs
DON'T:
Server.set_clock(Time.now)
@doc.store_signer(Dude.new)
@tellus.put_fire(:wild)
Time.set_back(1.year.ago)
Do:
Server.clock = Time.now
@doc.signer = Dude.new
@tellus.fire!(:wild)
Time.now = 1.year.ago
Keep method names short: e.g. #shorten_timespaces_because_of_special_holiday is too long. Long method names are often a sign of complex methods. Complex methods are often a sign we should refactor the code into smaller pieces.
Sometimes the work done by a method is simply too complicated to be expressed properly by a short and self-explanatory method name. If so, don't even try. Leave the method name short and cryptical. It becomes a marker for the reader of "hey, wanna understand this one? Sorry but you really really have to go read the code!".
DON'T:
#shorten_timespaces_because_of_special_holiday
DO:
#holiday_adjustment
Use the "?" for predicates (should *always* return true/false or at least something that evaluates to true false, such as "abc"/nil)
class Session
def finished?
finished_at
end
end
sess = Session.new
sess.finished_at = Time.now
sess.finished?
GOOD:
if sess.finished?
coffee!
end
BAD:
if sess.finished_at
coffee!
end
Use rdoc!
User rdoc for your rails apps. It's actually nice. When your mum asks you what it is you actually do you can show her the rdocs with links and text and colors and lists and it will make her happy too. And very useful for people trying to read your code (ok, they can run the freakin' rake task themselves, but you should do it too!).
rake doc:app
open doc/app/index

thanks David Palm!
This is super awesome.