clean-ruby

Table of Contents

Ruby

Clean Ruby

Chapter 1 The qualities of clean code

Clean Code is:

Is this code readable?

def method1(t, b)
  c = t + b
  return c
end

This could be:

def sum(a, b)
  a + b
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

log("Something happened", )

We could try:

def log(message, level)
  puts "#{level.to_s.upcase}: #{message}"
end

log("An error occurred", error)

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

args = ['A message', 'warn'] log_to_console(args)
def log_to_console(message, level)
  puts "#{level.to_s.upcase}: #{message}"
end

log_to_console('A message', )

Chapter 2 Naming Things

Instead of:

def state_tax(total)
  total * 0.2
end

def federal_tax(total)
  total * 0.1
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.

Keep data contained.

# Bad Example
user = 'bob'

# Good example
first_name = 'bob'
# Bad example
start_data = { 4, 5 } 3
# Good example
game_config = { 4, 5 }

Length

  • Keep variable names short.
# Bad example
purchase_final_sale_total = 300
 # Good example
sale_total = 300

Try to avoid generic words like

  • Manager
  • Data
  • Info
  • List

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
player_manager = PlayerManager.new
player_manager.spawn(1)
# Good Example
class PlayerSpawner
  def spawn(player_id)
    @playeds << Player.new(player_id)
  end
end

player_manager = PlayerManager.new
player_manager.spawn(1)

Avoid conjunctions, instead use two variables.

Instead of

score_and_player_count = { 100, 2 }

do:

score = 100
player_count = 2

Only use Alpha Characters for variable namess

# Bad example
year_1985 = 1985

# Good Example
start_of_grunge = 1985

Methods

Use Verbs

  • Methods are doers, so have verbs.
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

Return Value

  • rubyists prefer using a question mark when a function or method returns a boolean.
# bad example
def is_equal(a, b)
  a == b
end

# good example
def equal?(a, b)
  a == b
end
  • Rubyists use an exclamation mark for mutating methods, that change the data they work on.
class User
attr_accessor 
  def remove_friend!(friend)
    @friends.delete(friend)
  end
end

Classes

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

user_email = 'example@example.com'
new_user_add_coins
email_new_user_welcome(user_email)
class UserSetup
  def initialize(user)
    @user = user
  end
  def execute
    add_coins
    send_welcome
  end

  private
  def add_coins
    # add coins to their account
  end
  def send_welcome
    email = @user.email # send an email
  end
end

user_setup = UserSetup.new(user)
user_setup.execute

Role

Some classes offer a particular role.

Modules

  • A module is a class that you can’t instantiate; instead, you include it.

A class always needs to be instantiated, whereas a module does not.

# Bad Example
class Math
  def add(a, b)
    a + b
  end

  def subtract(a, b)
    a - b
  end
end
math = Math.new
sum = math.add(2, 2)
module Math
  def add(a, b)
    a + b
  end
  def subtract(a, b)
    a - b
  end
end
  class CashRegister
  include Math
  def calculate_change(total_cost, amount_paid)
    subtract(amount_paid, total_cost)
  end
end

Chapter 3: Creating Quality Methods

Parameters

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 , 

  def intiialize(num_players, start_score)
    @num_players = num_players
    @start_score = start_score
  end
end

Parameter Order

  • You should pass in parameters in an order that the caller would expect.
# 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 , 

  def initialize(id, name)
    self.id = id
    self.name = name
  end

  def find_by_name(users, name)
    users.each do |user|
      if user.name == name
        return user
      end
    end
    return { "Unable to find user with name #{name}" }
  end
end

class User
  attr_accessor , 

  def initialize(id, name)
    self.id = id
    self.name = name
  end

  def find_by_name(users, name)
    users.each do |user|
      if user.name == name
        return user
      end
    end
    raise "could not find User"
  end
end

Guard Clause

  • If the data provided to a method is not valid, you will want to guard against that.
def clear(items)
  return if items.nil? || !items.is_a?(Array)

  items.each do |item|
    # clear the item
  end
end

Method Length

  • 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, last_name)
end

# Good
def create_user(first_name, last_name)
  validate_input(first_name, last_name)
  User.create(first_name, last_name)
end
  • Make method names short, but not too short.
# Bad
# Single Line
 def qualified_users
 User.where(true).select(&).sort(&)
end

# Good
# Multiple Lines
def qualified_users
  active_users = User.where(true)
  qualified_users = active_users.select(&)
  qualified_users.sort(&)
end

Comments

  • Make sure to comment why you’re doing things, or notes for later.
def change_role(user_id, new_role)
  # find a user by id
  user = User.find(user_id)
   # check that current role does not equal the new role
  if user.role != new_role
  # assign new role to the user user.role = new_role
  end
 # end of method
end

Quality Comments

def change_role(user_id, new_role)
  user = User.find(user_id)
  role_service = RoleService.new(user)

  # Guard against cases where the role service can't assign.
  return unless role_service.can_assign(new_role)

  if user.role != new_role
    user.role = new_role
  end
end

Stale Comments

  • Comments can cause problems if they are incorrect.
  • Be sure to guard against these cases.
def fullname(first_name, last_name)
  # Confirm last_name is not blank or nil
  "#{first_name} #{last_name}".strip
end

Comments and Refactoring

  • If you use comments to group related methods, you should break out the logic into smaller methods.

Instead of:

def accounts_from_file(file_path)
  # read lines from file
  file = File.new(file_path)
  lines = file.readlines

  # Create an account for each line
  accounts = lines.collect do |line|
  # Parse name and email
  account_info = line.parse(',')
  name = account_info[0]
  email = account_info[1]

  # Create an account using the parsed data
  Account.create(name, email)
  end
end

Try:

def accounts_from_file(file_path)
lines = read_file(file_path)
create_accounts(lines)
end

def read_file(file_path)
  file = File.new(file_path)
  file.readlines
end

def create_accounts(lines)
  accounts = lines.collect do |line|
    account_params = account_params_from_line(line)
    Account.create(account_params)
  end
end

def account_params_from_line(line)
  account_info = line.parse(',')
  { account_info[0], account_info[1] }
end

Nesting

  • Avoid deep nesting:
MAX_PROMO_RATE = 5
def send_promo_email(user)
  if user.email.present?
    if user.promos_sent < MAX_PROMO_RATE
      UserMailer.promo_email(user).deliver
    end
  end
end
  • Reduce nesting by creating helper methods
MAX_PROMO_RATE = 5
def send_promo_email(user)
  if can_send?(user)
    UserMailer.promo_email(user).deliver
  end
end

def can_send?(user)
  user.email.present? && user.promos_sent < MAX_PROMO_RATE
end

Chapter 4 Using Boolean Logic

class Player
  attr_accessor , 
end
# Boolean logic directly in an IF statement
def respawn(player)
  if player.time_until_spawn <= 0 && player.health == 0
    respawn_at_base
  end
end

def respawn_at_base
  puts 'Player respawned at base'
end
player = Player.new player.time_until_spawn = 0 player.health = 0
respawn(player)

Instead, capture it in a variable:

class Player
  attr_accessor , 
end

# Boolean logic stored in a variable
def respawn(player)
  ready_to_spawn = player.time_until_spawn <= 0 && player.health == 0
  respawn_at_base if ready_to_spawn
end
def respawn_at_base
  puts 'Player respawned at base'
end
player = Player.new player.time_until_spawn = 0 player.health = 0
respawn(player)
def enable_editing
  user_exists = !user.nil? return if user_exists
  can_edit = user.editor? && !user.disabled?
  if user_exists && can_edit
    # code to enable editing
  end
end

Unless statements

  • unless statements are the opposite of if statements.
  • use them in guard clauses.
def check_resource(user_id, resource_id)
  return false unless User.find(user_id).can_access(resource_id)
  # check resource

Ternary Operators

  • Use this once per line to simplify if/else expressions.
# Bad
if a > b
  result = a
else
  result = b
end

# Good
result = a > b ? a : b

Avoid Double Negatives

  • Don’t use a double negative
# bad
def is_not_found(book)
  found = self.books.include?(book)
  !found
end

# good
def is_found(book)
  self.books.include?(book)
end

Truthy and Falsy

  • Everything that isn’t nil or false is falsy.
  • These evaluate to false.
  • Everything else is truthy, and evaluates to true.

Single Ampersand

  • Single ampersand evaluates both sides of its arguments.
  • The doubly ampersand does not.

Chapter 5 Classes

Initialize Method

  • 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.

Private Methods

  • Use private methods to do calculations that aren’t necessary for callers to understand.
class BankAccount
  def initialize(starting_balance)
    @balance = starting_balance
  end

  def display_balance
    format_for_display
  end

  private
  def format_for_display
    "Account Balance: #{@balance}"
  end
end

Modules

  • Use modules for methods that shouldn’t have to be instantiated.
module Math
  def add(a, b)
    a + b
  end
  def multiply(a, b)
    a * b
  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
  • Try to use composition when possible, but inheritance has its place.
  • Create a common class and embed that into each class.
class SuperMarket
  def initialize(accountant)
    @accountant = accountant
  end
end

class ToyStore
  def initialize(accountant)
    @accountant = accountant
  end
end

accountant = Accountant.new
toy_store = ToyStore.new(accountant) super_market = SuperMarket.new(accountant)

Chapter 6: Refactoring

No change is too small.

Single Responsibility Principle

  • make it so a method only does one thing.

Chapter 7: Test Driven Development