Confident Code

Avdi Grimm

Creative Commons License
Confident Code by Avdi Grimm is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.

What this talk is about    Incremental

  • A style of Code Construction
    code-complete.jpg

What this talk is not about    Incremental

  • New Ruby tricks
  • New libraries
  • Just a collection of techniques

What is confident code?    Incremental

  • The opposite of timid code

Timid Code

  • Examples, then themes

Source Code

Cowsay: ASCII art animals

$ echo "Mooo" | cowsay
 ______
< Mooo >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |

Cowsay Options

$ echo "Ontogeny Recapitulates Phylogeny" | cowsay -e "00"
 __________________________________
< Ontogeny Recapitulates Phylogeny >
 ----------------------------------
        \   ^__^
         \  (00)\_______
            (__)\       )\/\
                ||----w |

Installing Cowsay

  • Debian/Ubuntu
    sudo apt-get install cowsay
    
  • Mac OS X
    sudo port install cowsay
    

Cowsay.rb

require 'lib/cowsay'
cow = Cowsay::Cow.new
puts cow.say "Baaa"                  # => nil
# >>  ______
# >> < Baaa >
# >>  ------
# >>         \   ^__^
# >>          \  (oo)\_______
# >>             (__)\       )\/\
# >>                 ||----w |
# >>                 ||     ||

Cowsay.rb

  • Contrived for the sake of example
  • Examples drawn from real production code

Checking for nil

if options[:out]
  options[:out] << output
end

&&

command = "cowsay"
if options[:strings] && options[:strings][:eyes]
  command << " -e '#{options[:strings][:eyes]}'"
end

try() and friends    Incremental

@post.try(:comments).first.try(:author).try(:email)
  • AKA #andand(), #ergo()
  • Cleaner, but doesn't get to the root of the problem
  • Still interrupting flow to do a nil check

begin...rescue...end

@io_class.popen(command, "w+") do |process|
  results << begin
               process.write(message)
               process.close_write
               result = process.read
             rescue Errno::EPIPE
               message
             end
end

case blocks

destination = case options[:out]
              when nil  then "return value"
              when File then options[:out].path
              else options[:out].inspect
              end
@logger.info "Wrote to #{destination}"

raise in the middle of a method

if $? && ![0,172].include?($?.exitstatus)
  raise ArgumentError, 
        "Command exited with status #{$?.exitstatus.to_s}"
end

Timid Code

zoe.jpg

Timid Code    Incremental

  • Lives in fear
  • Constantly second-guessing itself
  • Randomly mixes input gathering, error handling, and business logic
  • Imposes cognitive load on the reader

Confident Code    Incremental

sandy.jpg

  • Says exactly what it intends to do
    • Without provisos or digressions
  • Has a consistent narrative structure

Narrative structure: Four Parts

  • Gather input
  • Perform work
  • Deliver results
  • Handle errors
  • In that order!

Haphazard structure

timid-code-plain.png

Haphazard structure (Annotated)

timid-code-annotated.png

Step 1: Gather Input

On Duck Typing    Incremental

  • True duck typing is a confident style
  • Duck typing doesn't ask "are you a duck?"
  • Or even "can you quack?"
  • Treat the object like a duck. If it isn't it will complain
  • Sometimes an object just needs help discovering its duck nature

Dealing with Unnacceptable Input

  • Coerce
  • Reject
  • Ignore

Coerce Values into What You Want    Incremental

  • #to_s, #to_i, #to_sym, Array()
  • Decorators

When in doubt, coerce    Incremental

  • Use #to_s, #to_i, Array() liberally
  • Ruby standard libs do this
    require 'pathname'
    path = Pathname("/home/avdi/.emacs")
    open(path) # => #<File:/home/avdi/.emacs>
    
  • Prefer Array() to to_a
    Object.new.to_a                 
    # => [#<Object:0xb78a665c>] 
    # !> default `to_a' will be obsolete
    

Array()

  • The "arrayification operator"
    Array([1,2,3])                # => [1, 2, 3]
    Array("foo")                  # => ["foo"]
    Array(nil)                    # => []
    
    # Gotcha:
    Array("foo\nbar")             # => ["foo\n", "bar"]
    

Using Array()    Incremental

  • Before
    messages = case message
               when Array then message
               when nil then []
               else [message]
               end
    
  • After
    messages = Array(message)
    

Gluing feathers to a pig    Incremental

  • When there is no consistent interface
  • Decorator Pattern
  • Wraps an object and adds a little extra
  • The Candidate
    destination = case options[:out]
                  when nil  then "return value"
                  when File then options[:out].path
                  else options[:out].inspect
                  end
    @logger.info "Wrote to #{destination}"
    

The WithPath Decorator

require 'delegate'
# ...
class WithPath < SimpleDelegator
  def path
    case __getobj__
    when File then super
    when nil then "return value"
    else inspect
    end
  end
end

Using the Decorator

destination = WithPath.new(options[:out]).path
# ...
@logger.info "Wrote to #{destination}"

Other Ways to Massage Objects    Incremental

  • Dynamically #extend objects
    obj.extend(MyMethods)
    
  • Dynamically add methods
    def obj.foo
       # ...
    end
    

Reject Unexpected Values    Incremental

  • Confident Code is Assertive
  • Preconditions: state your demands up-front
  • Part of Design by Contract
  • A complement to TDD/BDD
  • No DbC framework needed
  • Assertions don't have to be spelled "assert()"

Basic Precondition

def say(message, options={})
  if options[:cowfile] && options[:cowfile] =~ /^\s*$/
    raise ArgumentError, "Cowfile cannot be blank"
  end
  # ...
  if options[:cowfile]
    command << " -f #{options[:cowfile]}"
  end
  # ...
end

Assertion Method

def assert(value, message="Assertion failed")
  raise Exception, message, caller unless value
end

# ...

options[:cowfile] and 
  assert(options[:cowfile].to_s !~ /^\s*$/)

More on Assertions

Ignore Unnacceptable Values

  • Guard Clause
  • Null Object

Guard Clause    Incremental

  • Short-circuit on nil message
    def say(message, options={})
      return "" if message.nil?
      # ...
    
  • Avoids special cases later in method
  • Return from beginning or end of method

Null Object    Incremental

"A Null Object is an object with defined neutral ("null") behavior." (Wikipedia)

  • Responds to any message with nil (or itself)
  • Should be in the standard library

A Basic Null Object

class NullObject
  def method_missing(*args, &block)
    self
  end

  def nil?; true; end
end

def Maybe(value)
  value.nil? ? NullObject.new : value
end

Using the Null Object    Incremental

  • if statement
    if options[:out]
      options[:out] << output
    end
    
  • Null Object
    Maybe(options[:out]) << output
    

More on Null Objects

  • Returning self lets us nullify chains of calls
    Example coming up later.

Zero Tolerance for nil    Incremental

  • nil is overused in Ruby code
  • It means too many things
    • Error
    • Missing Data
    • Flag for "default behavior"
    • Uninitialized variable
  • Nils propagate
  • Nil checks are the most common form of timid code

Where'd that nil come from?!

post_id = params[:post_id]
post    = Post.find(post_id.to_i)
post.date     # =>
# ~> -:4: undefined method `date' for nil:NilClass (NoMethodError)

Eliminating nil

Use Hash#fetch when appropriate    Incremental

{}[:width]                      # => nil
{}[:width] || 40                # => 40
{:width => nil}[:width] || 40   # => 40
{:width => false}[:width] || 40 # => 40
{:width => 29}[:width] || 40    # => 29

{}.fetch(:width)                     # => 
# ~> -:7:in `fetch': key not found (IndexError)
{}.fetch(:width) { 40 }         # => 40
{:width => nil}.fetch(:width) {40} # => nil
{:width => false}.fetch(:width) {40} # => false
{:width => 29}.fetch(:width) {40}    # => 29

#fetch as an assertion

  • Default error
    opt = {}.fetch(:required_opt)         # => 
    # ~> -:1:in `fetch': key not found (IndexError)
    
  • Custom error
    opt = {}.fetch(:required_opt) do
      raise ArgumentError, "Missing option!"
    end
    # ~> -:2: Missing option! (ArgumentError)
    

#fetch for defaults

  • Using ||
    width = options[:width] || 40
    command << " -W #{width}"
    
  • Using #fetch
    width = options.fetch(:width) {40}
    command << " -W #{width}"
    
  • A more explicit default
  • One less conditional

Don't use nil as a default    Incremental

  • Where did that nil come from?
    @logger.info "Something happened" # => 
    # ~> -:3: undefined method `info' for nil:NilClass (NoMethodError)
    

Symbol Default

@logger = :no_logger_set
# ...
@logger.info "Something happened" # => 
# ~> -:3: undefined method `info' for :no_logger_set:Symbol (NoMethodError)

Null Object Default

def initialize(logger=NullObject.new)
  @logger = logger
end

Null Object with Origin

class NullObject
  def initialize
    @origin = caller.first
  end
  # ...
end

Stub Object Default

# $? is either nil or a Process::Status object
def check_child_exit_status!(status=$?)
  status ||= OpenStruct.new(:exitstatus => 0)
  unless [0,172].include?(status.exitstatus)
    raise ArgumentError, 
          "Command exited with status #{status.exitstatus.status}"
  end      
end

Step 2: Perform Work    Incremental

  • PIE Principle
    Program Intently and Expressively pad.jpg
  • Avoid digressions for error/input handling

Conditionals for Business Logic    Incremental

  • Programming without IF
    http://programmingwithoutifs.blogspot.com/
  • Conditionals are essential
  • Reserve conditionals for business logic
  • Minimize conditionals for error, input handling
  • Isolate error/input handling from program flow

Business Logic Conditional

if post.published?
  # ...
end

Non-Business Logic Conditional

if post
  # ...
end

Chaining Work

def slug(text)
  Maybe(text).downcase.strip.tr_s('^a-z0-9', '-')
end

slug("Confident Code")          # => "confident-code"
slug(nil)                       # => #<NullObject:0xb780863c>

Iteration over singular objects    Incremental

  • A style exemplified by jQuery
    // From "jQuery in Action"
    $("div.notLongForThisWorld").fadeOut().
        addClass("removed");
    
  • Single object operations are implicitly one-or-error
  • Iteration is implicitly 0-or-more
  • Chains of enumerable operations are self-nullifying
  • Cowsay#say uses an iterative style…

Iterative Style

def say(message, options={})
  # ...
  messages = Array(message)
  message.map { |message|
    # ...
  }
  # ...
end

cow.say(nil) # => ""

Iterative Style Tips

  • Use *args
    def say(*messages) # ...
    
  • Use #compact to get rid of nils
    msgs = ["moo", nil, "belch"]
    cow.say(*msgs.compact)
    

Step 3: Deliver Results    Incremental

  • Moving on…

Step 4: Handle Errors    Incremental

  • Put the happy path first
  • Put error-handling at the end
  • Or in other methods.

The Ruby Exception Handling Idiom

def foo
  # ...
rescue => error
  # ...
end

Extract error handling methods

  • Bouncer Method
  • Checked Method

Bouncer Method

  • Method whose job is to raise or do nothing
    def check_child_exit_status
      result = yield
      status = $? || OpenStruct.new(:exitstatus => 0)
      unless [0,172].include?(status.exitstatus)
        raise ArgumentError, 
              "Command exited with status #{status.exitstatus}"
      end
      result
    end
    

Bouncer Method in Use

# Before:
@io_class.popen(command, "w+") # ...
# ...
if $? && ![0,172].include?($?.exitstatus)
  raise ArgumentError, 
        "Command exited with status #{$?.exitstatus.to_s}"
end

# After:
check_child_exit_status {
  @io_class.popen(command, "w+") # ...
}

Checked Method

def checked_popen(command, mode, fail_action)
  check_child_exit_status do
    @io_class.popen(command, "w+") do |process|
      yield(process)
    end
  end
rescue Errno::EPIPE
  fail_action.call
end

Checked Method in Use

# Before:
@io_class.popen(command, "w+") do |process|
  results << begin
               # ...
             rescue Errno::EPIPE
               message
             end
end

# After:
checked_popen(command, "w+", lambda{message}) do |process|
  # ...
end

The Final Product

confident-code-annotated.png

Observations on the Final Product    Incremental

  • It has a coherent narrative structure
  • It has lower complexity
  • It's not necessarily shorter

Why Confident Code?    Incremental

  • Fewer paths == fewer bugs
  • Easier to debug
  • Self-documenting
    "Write code for people first, the computer second"

Review    Incremental

  • A style, not a set of techniques
  • PIE: Program Intently and Expressively
  • Consistent Narrative structure
  • Handle input, perform work, deliver output, handle errors.
  • State your demands up-front
  • Terminate nils with extreme prejudice
  • Avoid digressions
  • Isolate error handling from the main flow

Flog Complexity

flog.png

Metrics Diff

metrics-diff.png

Questions?