Book review – Ruby Best Practices

Ruby is a nice and expressive language, and full of hidden tricks. It takes time for new programmers to learn them, and Ruby Best Practices speeds this process. Expert programmers can also benefit from the book by learning new tricks and remembering forgotten ones. As a seasoned Ruby newbie, I’ve found the book to be mostly useful.

The book is basically a collection of strategies to solve common problems in Ruby, and it’s full of real world examples. The author, Gregory T. Brown, is an experienced Rubyist who authored libraries like Prawn, a PDF documents generator (it’s very good!). Given his practical experience, the book is filled with real and relevant examples and intelligent and elegant solutions to them.

Besides the quality content, the book is well written, straightforward and funny. Gregory is enthusiastic and he keeps you hooked wanting to read more and more, which is something very interesting in a technical book. And even though the book was released in 2009, the problems and code are still valid.

This book is a great read for the ones who aspire becoming great (or at least good) Rubyists. ;)

Book on Amazon: Ruby Best Practices

My personal notes

Chapter 1: driving code through tests

  • Automate test tasks with Rake.
  • Complex data testing in viewsby using small text chunks with tools like nokogiri.
  • Stubs are used when we want to replace some functionality with canned results to make testing other code easier.
  • Mocks are used to create objects that can act in place of an external resource for the purpose of testing. Mock objects are set up with expected responses, which are then verified when the tests are run.
  • If your solution seems difficult to test, it may be a sign that your design is not flexible enough to easily be refactored or interacted with.

Chapter 2: designing beautiful APIs

  • Execute a block in the scope of an instantiated object
class Server
  # other methods same as before
  def self.run(port=3333,&block)
    server = Server.new(port)
    server.instance_eval(&block)
    server.run
  end
end

Server.run do
  handle(/hello/i) { "Hello from server at #{Time.now}" }
  handle(/goodbye/i) { "Goodbye from server at #{Time.now}" }
  handle(/name is (\w+)/) { |m| "Nice to meet you #{m[1]}!" }
end
  • If you create a collection class that you need to traverse, build on top of Enumerable rather than reinventing the wheel.
  • If you have shared code that differs only in the middle, create a helper method that yields a block in between the pre/postprocessing code to avoid duplication of effort.
  • If you use the &block syntax, you can capture the code block provided to a method inside a variable. You can then store this and use it later, which is very useful for creating dynamic callbacks.
  • Using a combination of &block and instance_eval, you can execute blocks within the context of arbitrary objects, which opens up a lot of doors for highly customized interfaces.
  • The return value of yield (and block.call) is the same as the return value of the provided block.
  • Hack for converting Ruby objects to their boolean values:
!!(obj)
!!(:foo)
!!(123)

Chapter 4: text and file processing

  • When processing a file:
    • Identify beggining and end markers of sections with a pattern.
    • If sections are nested, maintain a stack that you update before further processing of each line.
    • Break up your extraction code into different cases and select the right one based on the section you are in.
    • When a line cannot be processed, skip to the next one ASAP, using the ‘next’ keyword.
    • Maintain state as you normally would, processing whatever data you need.
  • Tempfile: deals with temporary files without hassle.
  • Enum.inject => Combines all elements of enum by applying a binary operation, specified by a block or a symbol that names a method or operator.
(5..10).inject(:+)
(5..10).inject('+')
(5..10).inject { |sum, n| sum + n }

Chapter 5: functional programming

  • Lazy code => lambda, Proc => is essentially a chunk of code that gets executed on demand, rather than in place
  • Memoizable: caches function results in well defined functions (function that return the same result on the same input). Can be done using hashes.

Appendix

  • Ruby worst practices
    • Reserve class variables (@@foo) to shared data only
    • Beware the use of eval
    • Don’t mess with method_missing

Code examples

# logger.rb
require 'logger'

class StandardError
  def report
    %{#{self.class}: #{message}\n#{backtrace.join("\n")}}
  end
end

logger = Logger.new('newb.log')
logger.error 'Foo barrrrrrr!'
logger.info 'Fus rooooh!'
logger.fatal 'Dead!'
logger.warn 'Danger!'
logger.debug 'Here comes the error'
# meta.rb
class MetaProg
  def self.go(&block)
    m = MetaProg.new
    m.instance_eval &block
    m.say
  end
  
  def yell(sentence)
    puts sentence
  end
  
  def say
    puts 'say foo!'
  end
end

MetaProg.go do |m|
  m.yell 'Hayoooo!'
  m.yell 'Silver!'
end
# module.rb
module Foo
  extend self
  
  def bar
    puts 'baz'
  end
  
  module Baz
    extend self
    
    def biz
      puts 'bir'
    end
  end
end

Foo.bar
Foo::Baz.biz
# Rakefile

task default: [:bang]

desc 'Faz algo inútil'
task :bang do
  sh 'pwd'
  puts `ruby -v`
end

desc 'Imprime o texto shoop da whoop no terminal'
task :shoop do
  puts "sh00p da wh00p"
end

namespace :bar do
  desc 'Does bar:foo without name clash.'
  task :foo do
    puts 'task bar:foo'
  end
end
# stubs.rb
module Stubber
  extend self
  
  def stubs(method, options = {})
    singleton(options[:for]).send(:define_method, method) do |*a|
      options[:returns]
    end
  end
  
  def singleton(obj)
    class << obj
      self
    end
  end
end

class User
end

user = User.new
puts user.inspect
Stubber.stubs(:logged_in?, for: user, returns: true)

# True
puts user.logged_in?

# NoMethodError
puts User.new.logged_in?

I used to have Disqus enabled on my website, but I have disabled it because of privacy concerns.

If you feel like commenting or asking something, you can still contact me via other means.