app/models
, even if it isn't a descendent of ActiveRecord::Base
.lib
.lib
or in its own gem.test/unit
.app/models/ability.rb
lib/test/unit/*
test/unit/profile_test.rb
# encoding: utf-8
comment at the top of the file (or immediately after the shebang if it is an executable).if $0 == __FILE__
, if executable__END__
and bundled data, if anyduckhunt, an executable in a gem
#!/usr/bin/env ruby
# encoding: utf-8
require 'ostruct'
require 'gosu'
require_relative 'lib/duckhunt/bloodhound'
# NOTE: ostruct is STDLIB, gosu is a gem, the other is local to this library
if $0 == __FILE__
mechanics = OpenStruct.new(kb: Gosu.has_keyboard?, mouse: Gosu.has_mouse?))
game = Duckhunt.new(mechanics)
game.main_character = Duckhunt::Bloodhound
game.play
end
app/models/television.rb, a business model in an app's library
require 'codec/hdtv'
# Television is responsible for playing a picture and sound stream.
#
# A television can get +TelevisionReception+ from many +Channels+ that
# provide that stream.
#
class Television
# A codec de-scrambles a stream into a playable representation.
class << self
attr_accessor :playback_codec
end
self.playback_codec = Codec::HDTV
# Create a television
def initialize start_channel
initialize_screen
initialize_codec
tune_to start_channel
end
end
{...}
when defining blocks on one line.do...end
for multiline blocks for side-effects, or when DSL authors prefer it.Use {...}
for multiline blocks where the return value is relevant.
multiples = numbers.map {|n| n * 5 }
def select_primes
select {|x|
factors = factorize_in_constant_time(x)
factors.length == 2
}
end
create_task do |task|
task.execute = 'kill -9 init'
end
Blocks end with a space before }
(with exceptions for making it the 79th character and avoiding line wrapping).
# Yes
new_image = pixels.map {|px| px * alpha_boost }
irc_bot.register_callback('PING') { pong }
# No
new_image=pixels.map { | px |px*alpha_boost}
irc_bot.register_callback('PING'){pong}
Blocks that are called by functions that are doing assignment wrap below the start of their capturing/calling method, not under the assigned variable.
# Yes
applicable_results = my_long_named_list_of_things.select {|thing|
ThingSelector.new(thing).is_applicable? }
# Maybe
applicable_results = my_long_named_list_of_things.select {|thing|
ThingSelector.new(thing).is_applicable?
}
# No
applicable_results = my_long_named_list_of_things.select {|thing|
ThingSelector.new(thing).is_applicable? }
# No
applicable_results = my_long_named_list_of_things.select {|thing|
ThingSelector.new(thing).is_applicable?
}
Avoid for
in favor of more ruby-like idioms.
# Yes
presidents.each {|prez| War.new(prez, Country.pick_random) }
# No
for prez in presidents
War.new(prez, Country.pick_random)
end
Never use then
, except in case
statements where all cases are one line.
# Yes
case pig.fly_type
when :none then a_ok
when :in_a_plane then close_one
when :with_wings then hell.feeze_over!
else pig_doctor.check_up_on(pig)
end
# No
case bird.dirtiness_with?(source_bird)
when :only_on_weekends_when_nobody_is_home
pull_the_curtains
when :never then breathe_easy
end
&&
/||
for boolean expressions, and
/or
/not
for control flow.Use !
for negation.
def handle_input string
# Control flow, so use "and" and "not" instead of "&&" and "!":
if (ast=parse) and not something_with_side_effects(ast)
compile ast, target: ForthMachine
end
end
# frozen_over? does not have side effects so not control flow so use "&&":
if pig.flying? && !hell.frozen_over?
prepare_for_apocolypse
else
carry_on
end
self.invisible = !self.visible_to(:robots) && !self.visible_to(:aliens)
Avoid multiline ?:
, use if
.
# Yes
if long_object_name.long_method_name
long_variable = assignment_from_somewhere_else
else
long_variable = nil
end
# No
long_varaible = long_object_name.long_method_name ? \
assignment_from_somewhere_else : nil
Align when
and else
with case
.
# Yes
case foo
when /bar/
raise 'baz'
else
print 'not bar'
end
# No
case foo
when /bar/
raise 'baz'
else
puts 'not bar'
end
return
when the last line is at the outermost level.return
when in a conditional.Use explicit return
when short-circuiting or guarding.
def robot_companion_type bot_attributes
return Chumby if bot_attributes.empty?
if bot_attributes.exists_in_future?
return T1000
end
RoboCop
end
Classes have a leading comment like the following:
# ClassName performs this responsibility.
#
# This is gramaticaly correct english with proper spelling and
# punctuation. The first line was *only* one line. This paragraph
# explains a bit more about what the class does.
#
# This paragraph talks about how to interface with this class. All of
# these lines are wrapping at 80 characters.
#
# = High-level Concept
#
# Knowledge about interoperations, architecutural impact, or specific
# uses of this class can be placed under their own heading.
#
# *Give lots of examples*
#
# # The 'peek' operation has an inverse of 'poke'.
# peek_instance = MyClass.new('peek')
# peek_instance.inverse #=> "poke"
#
def
s in a class or module.private
or public
should be isolated by a single blank line before and after.private
and public
at class scope, not for individual methods.def self.method
to define singleton methods, instead of class << self
.Use RDoc and its conventions for API documentation. Put an empty comment line between the body of the comment and the def
except for one-line documentation.
# Check for the meaning of life.
#
# Return true if:
# * the attempted_answer is greater than 41
# * the attempted_answer is less than 43
#
def check_for_life attempted_answer
return attempted_answer == 42
end
# Life is random (and absurd), give users a number to check with.
def get_attempt
if guess=random(2012)
guess += 1
end
guess
end
private
def one
return 1
end
def two
return 2
end
# No
private :check_for_life
There can be multiple levels of workflow methods.
# A good workflow method
def brew
add_hot_water
add_tea_bag
ding_after(steep_time)
end
# A good task method.
#
# Until the reservoir reaches the target temperature (F)
# continue adding energy (kwh).
#
def heat_water target=180, energy_increment=0.001
while @reservoir.temperature < target
@reservoir.add_energy(energy_increment)
end
end
# A poor task method (does a task, then calls another task).
def add_tea_bag brew='Earl Grey'
box = @cupboard.get_tea_box
bag = box.select {|tea| tea.name == brew}
package = bag.unpackage
dispose_of_waste(package)
add_bag_to_cup(bag)
end
# Rewritten to be more appropriate as workflow.
#
# Interacting with other entities (cupboard, wastebasket, cup) is
# completely encapsulated here. The workflow can be tested by adding
# mocks to return a double object in the case of +bag+ and the other
# methods can be stubbed just to check they're getting the right
# argument.
#
def add_teabag brew='Earl Grey'
bag = select_tea_bag
unpackage_and_discard_waste(bag)
add_bag_to_cup(bag)
end
Avoid long parameter lists. Don't cheat with splats or hashes.
# No
def process processing_object, options={}
if options[:setup_method]
run_setup_method(options[:setup_method])
end
initial_state = capture_state
processing_object.call
final_state = capture_state
if options[:teardown_method]
run_teardown_method(options[:setup_method])
end
end
# Yes
class Processor
attr_accessor :setup_method
attr_accessor :teardown_method
def initialize processing_object
@processing_object = processing_object
end
def process
setup
build_state_comparison do
@processing_object.call
end
teardown
end
# ...
end
+
, -
, *
, etc).,
(
, [
and before ]
, )
.if
/unless
-end
block.Single space between hash key's :
and the value.
# Yes
3 + 2
thing.perform(param, [first, last])
var1 = thing
variable_two = other_thing
{foo: 'bar'}
# No
3+2
thing.perform( param, [ first, last ] )
var1 = thing
variable_two = other_thing
{foo:'bar'}
{foo => 'bar'}
Use parentheses when calling methods with arguments unless it is the only thing on the line, passed blocks do not count as arguments in this context.
# Yes
maybe_turtle = soup.ingredients(:protein)
soup.add_ingredient turtle, :protein
soup.prepare_ingredient(:protien) {|meat| meat.season_with(paprika) }
soup.prepare_ingredient :protein, &meat_prep
# No
mabye_turtle = soup.ingredients :protien
soup.add_ingredient(turtle, :protein)
soup.prepare_ingredient :protein {|meat| meat.season_with(paprika) }
.
on the first line.Closing )
, }
, and ]
should not be on a line by themselves.
# Yes
if score=scoreboard.value(team) and score > 0
return "#{team} scored #{score} goals"
end
time_limit_reached=scoreboard.game_time <= 0
mercy_rule_invoked = scoreboard.score_difference > 10
if time_limit_reached or mercy_rule_invoked
game_over_man!
end
score.method1().method2(team).method3(time_limit_reached).
method4().method5()
variable_name = call_a_really_long_method_with_some_long_argument_names(
this_is_the_first_argument, 2)
# No
if scoreboard.game_time <= 0 and \
scoreboard.score_difference > 10
game_over_man!
end
||=
freely.$0...$9
to named variables
before using them so their intent is clear.Use %r
for regexp containing quotes.
regexp = %r{[Yy]ou're an? "(?<sarcasm>\w+)" person}
regexp =~ "You're an \\"intelligent\\" person"
$~[:sarcasm] #=> "intelligent"
yield
s to
the block (like inject
)._
for unused variables, for example when you
implement a callback to an API that gives you more than you care about.true
/false
/nil
) into functions if
their use is not clear. Instead make a variable
(called skip_processing = true
, for example) and pass the named variable
in instead.other
".map
over collect
, detect
over find
, select
over find_all
.Do not use comments or whitespace as decoration.
# A background worker for routing unicorns to greener pastures.
#
# This worker is created when a `PastureChecker` process notices
# that a pasture is overcrowded or otherwise unhelathy. They
# can also be created via the console for administrative tasks.
#
# Notice the first line of this comment was a clear sentence. The entire
# thing reads like two developers talking to one another. There are
# blank lines (still indented and containing the hash mark, but no
# trailing space or other characters) between paragraphs.
#
class UnicornShuffler < BasicWorker
# Implement the background callback.
#
# This directs a unicorn to fly to a new location if their old location
# was unsuitable. The new location is determined by the Unicorn in
# flight, we have simply disallowed this particular location.
def self.process unicorn, old_pasture
flight = UnicornFlight.create(unicorn)
# TODO: i18n
flight.disallow_pasture old_pasture, reason: 'Not green enough'
# TODO(todd): disallow all pastures in a 3 mile radius.
flight.take_off!
end
end
Kernel
(if you have to) and make them private.alias
when alias_method
will do.freeze
objects assigned to constants.