Ruby
Clean Code is:
Is this code readable?
def method1(t, b)
= t + b
c return c
end
This could be:
def sum(a, b)
+ b
a end
Extensibility
Instead of:
def log(message, level)
if level.to_s == 'warning'
puts "WARN: #{message}"
elsif level.to_s == 'info'
puts "INFO: #{message}"
elsif level.to_s == 'error'
puts "ERROR: #{message}"
end
end
"Something happened", :info) log(
We could try:
def log(message, level)
puts "#{level.to_s.upcase}: #{message}"
end
"An error occurred", error) log(
Instead of:
def log_to_console(args)
if args.length > 1
if args[1] == 'warn'
puts 'WARN: ' + args[0]
elsif args[1] == 'error'
puts 'ERROR: ' + args[0]
else
puts args[0]
end
end
end
= ['A message', 'warn'] log_to_console(args) args
def log_to_console(message, level)
puts "#{level.to_s.upcase}: #{message}"
end
'A message', :warn) log_to_console(
Instead of:
def state_tax(total)
* 0.2
total end
def federal_tax(total)
* 0.1
total end
class Tax
def initialize(total)
# Total is now an instance variable
# and can be accessed by all methods
@total = total
end
def state
@total * 0.2
end
def federal
@total * 0.1
end
end
Prefer snake_case for variables and functions, and Camel case for Classes.
# Bad Example
= 'bob'
user
# Good example
= 'bob' first_name
# Bad example
= { players: 4, score_to_win: 5 } 3
start_data # Good example
= { players: 4, score_to_win: 5 } game_config
# Bad example
= 300
purchase_final_sale_total # Good example
= 300 sale_total
Try to avoid generic words like
that don’t convey information to the reader.
Instead of:
# Bad example
class PlayerManager
def spawn(player_id)
@players << Player.new(player_id)
end
end
= PlayerManager.new
player_manager .spawn(1) player_manager
# Good Example
class PlayerSpawner
def spawn(player_id)
@playeds << Player.new(player_id)
end
end
= PlayerManager.new
player_manager .spawn(1) player_manager
Avoid conjunctions, instead use two variables.
Instead of
= { score: 100, player_count: 2 } score_and_player_count
do:
= 100
score = 2 player_count
# Bad example
= 1985
year_1985
# Good Example
= 1985 start_of_grunge
class Account
def initialize(customer)
@customer = customer
end
# Bad method
def money(amount)
@customer.balance -= amount
end
end
class Account
def initialize(customer)
@customer = customer
end
# Good method
def pay_bill(amount)
@customer.balance -= amount
end
end
# bad example
def is_equal(a, b)
== b
a end
# good example
def equal?(a, b)
== b
a end
class User
attr_accessor :friends
def remove_friend!(friend)
@friends.delete(friend)
end
end
Classes are the building blocks of ruby code. They should be a Noun that contains verbs that act on the classs.
The class contains state internally, and the methods operate on said state. Rather than free standing functions like this:
def new_user_add_coins
# code
end
def email_new_user_welcome(email)
# send an email
end
= 'example@example.com'
user_email
new_user_add_coins email_new_user_welcome(user_email)
class UserSetup
def initialize(user)
@user = user
end
def execute
add_coins
send_welcomeend
private
def add_coins
# add coins to their account
end
def send_welcome
= @user.email # send an email
email end
end
= UserSetup.new(user)
user_setup .execute user_setup
Some classes offer a particular role.
A class always needs to be instantiated, whereas a module does not.
# Bad Example
class Math
def add(a, b)
+ b
a end
def subtract(a, b)
- b
a end
end
= Math.new
math = math.add(2, 2) sum
module Math
def add(a, b)
+ b
a end
def subtract(a, b)
- b
a end
end
class CashRegister
include Math
def calculate_change(total_cost, amount_paid)
subtract(amount_paid, total_cost)end
end
Use Fewer Parameters
This function isn’t alterable: we can make this better.
def greeting
"Hello"
end
Now we can alter this:
def greeting(name)
"Hello #{name}"
end
Remember that this can also be nil
, so we should pay
attention to this case as well:
def greeting(name)
"Hello #{name || 'unknown'}"
end
We can have a default argument in the function
def greeting(name = 'unknown')
"Hello #{name}"
end
The passed in value can be an array or a hash or a long string too, so you must be able to catch for those as well.
You’ll want to use instance variables and attr_accessors and the like liberally in classes.
class Config
attr_accessor :num_players, :start_score
def intiialize(num_players, start_score)
@num_players = num_players
@start_score = start_score
end
end
# bad
def login(password, username)
# login
end
# good
def login(username, password)
# login
end
# let them choose with a hash
def login(username:, password:)
# login
end
Try to return the same type always, or throw an exception.
# Bad, because find_by_name ca return a hash or a string
class User
attr_accessor :id, :name
def initialize(id, name)
self.id = id
self.name = name
end
def find_by_name(users, name)
.each do |user|
usersif user.name == name
return user
end
end
return { message: "Unable to find user with name #{name}" }
end
end
class User
attr_accessor :id, :name
def initialize(id, name)
self.id = id
self.name = name
end
def find_by_name(users, name)
.each do |user|
usersif user.name == name
return user
end
end
raise "could not find User"
end
end
def clear(items)
return if items.nil? || !items.is_a?(Array)
.each do |item|
items# clear the item
end
end
You’ll not want a method to grow too long in length; try to make methods < 20 lines long.
Try to create private helper methods in classes that might do validation.
# Bad
def create_user(first_name, last_name)
raise ArgumentError, "First Name is required" unless first_name
raise ArgumentError, "Last Name is required" unless last_name
User.create(first_name: first_name, last_name: last_name)
end
# Good
def create_user(first_name, last_name)
validate_input(first_name, last_name)User.create(first_name: first_name, last_name: last_name)
end
# Bad
# Single Line
def qualified_users
User.where(active: true).select(&:qualified?).sort(&:last_login)
end
# Good
# Multiple Lines
def qualified_users
= User.where(active: true)
active_users = active_users.select(&:qualified?)
qualified_users .sort(&:last_login)
qualified_usersend
class Player
attr_accessor :time_until_spawn, :health
end
# Boolean logic directly in an IF statement
def respawn(player)
if player.time_until_spawn <= 0 && player.health == 0
respawn_at_baseend
end
def respawn_at_base
puts 'Player respawned at base'
end
= Player.new player.time_until_spawn = 0 player.health = 0
player respawn(player)
Instead, capture it in a variable:
class Player
attr_accessor :time_until_spawn, :health
end
# Boolean logic stored in a variable
def respawn(player)
= player.time_until_spawn <= 0 && player.health == 0
ready_to_spawn if ready_to_spawn
respawn_at_base end
def respawn_at_base
puts 'Player respawned at base'
end
= Player.new player.time_until_spawn = 0 player.health = 0
player respawn(player)
def enable_editing
= !user.nil? return if user_exists
user_exists = user.editor? && !user.disabled?
can_edit if user_exists && can_edit
# code to enable editing
end
end
def check_resource(user_id, resource_id)
return false unless User.find(user_id).can_access(resource_id)
# check resource
# Bad
if a > b
= a
result else
= b
result end
# Good
= a > b ? a : b result
# bad
def is_not_found(book)
= self.books.include?(book)
found
!foundend
# good
def is_found(book)
self.books.include?(book)
end
when you call the .new method on a class, it executes the code in
the iniitalize
method.
Leave complicated calculations outside of the
initialize
method.
Try not to throw inside of the initialize method unless there is malformed data provided.
Use instance variables to hoist state in ruby.
class BankAccount
def initialize(starting_balance)
@balance = starting_balance
end
def display_balance
format_for_displayend
private
def format_for_display
"Account Balance: #{@balance}"
end
end
module Math
def add(a, b)
+ b
a end
def multiply(a, b)
* b
a end
end
class BankAccount
include Math
def initialize(balance, interest_rate)
@balance = balance
@interest = interest_rate
end
def add_to_balance(amount)
@balance = add(@balance, amount)
end
def calculate_interest multiply(@balance, @interest_rate)
end
end
class SuperMarket
def initialize(accountant)
@accountant = accountant
end
end
class ToyStore
def initialize(accountant)
@accountant = accountant
end
end
= Accountant.new
accountant = ToyStore.new(accountant) super_market = SuperMarket.new(accountant) toy_store
start with tests to implement your code.
think about the ways your code might interact.
Comments
Quality Comments
Stale Comments
Comments and Refactoring
Instead of:
Try:
Nesting