What this talk is about
- A style of Code Construction
What is confident code?
The opposite of timid code. Duh.
Timid Code
First, a story…
Poor Storytelling
- Tangents
- Digressions
- Lack of certainty
- …code can have these qualities as well.
Timid Code
Timid Code
- Lives in fear
- Uncertain
- Prone to digressions
- Constantly second-guessing itself
- Randomly mixes input gathering, error handling, and business logic
- Imposes cognitive load on the reader
Confident Code
Confident Code
- Says exactly what it intends to do
- No provisos or digressions
- Has a consistent narrative structure
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 # or `cauldron stir cowsay` # or whatever you Mac kids are using now
Cowsay.rb
require 'lib/cowsay' cow = Cowsay::Cow.new puts cow.say "Baaa" # >> ______ # >> < Baaa > # >> ------ # >> \ ^__^ # >> \ (oo)\_______ # >> (__)\ )\/\ # >> ||----w | # >> || ||
Cowsay.rb
- Contrived for the sake of example
- Drawn from real
production codecrap I've written
Timid Code Structure
Timid Code Structure (Annotated)
The Narrative structure
- Gather input
- Perform work
- Deliver results
- handle failure
In that order!
Step 1: Gather Input
- To be confident, we must be sure of our inputs
On Duck Typing
- True duck typing is a confident style
- Duck typing doesn't ask "are you a duck?"
object.is_a?(String)
- Or even "can you quack?"
object.respond_to?(:each)
- Treat the object like a duck
(If it isn't it will complain)
- Some objects need a little help discovering their duck nature
The Switch (case
) Smell
- Typecasing: Code that switches on the type of an object
- The opposite of duck typing
- A reaction to input variance
nil
checks are the most common typecase
- It doesn't have to be this way.
Dealing with Uncertain Input
- Coerce
- Reject
- Ignore
When in doubt, coerce
- 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>
Array()
- The "arrayification operator"
Array([1,2,3]) # => [1, 2, 3] Array("foo") # => ["foo"] Array(nil) # => [] # Gotcha: Array("foo\nbar") # => ["foo\n", "bar"]
Prefer Array()
to to_a
Object.new.to_a # => [#<Object:0xb78a665c>] # !> default `to_a' will be obsolete
Using Array()
- Before
messages = case message when Array then message when nil then [] else [message] end
- After
messages = Array(message)
Gluing feathers to a pig
- When there is no consistent interface
- Decorator Pattern
- Wraps an object and adds a little extra
Decorator Candidate
destination = case options[:out] when nil then "return value" when File then options[:out].path else options[:out].inspect # String? 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
- Dynamically
#extend
objects
obj.extend(MyMethods)
- Dynamically add methods
def obj.foo # ... end
Reject Unexpected Values
- Not a duck. Not even a bird.
- Will eventually cause an error
- …but not before doing some damage
- May not be clear where the error originated
Assertive Code
- Confident Code asserts itself
- Preconditions: state your demands up-front
- Part of Design by Contract
- 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*$/)
Assertion Notes
Exception
skips default rescue
begin raise Exception, "Can't catch me!" rescue # rescues StandardError puts "Rescued!" end # ~> -:2: Can't catch me! (Exception)
- Third argument to
raise
sets stack trace
raise Exception, message, caller
More on Assertions
- Assertions are documentation
- FailFast
http://github.com/avdi/failfast
Ignore Unnacceptable Values
- Guard Clause
- Special Case
- Null Object
Guard Clause
- Short-circuit on nil message
def say(message, options={}) return "" if message.nil? # ... end
- Avoids special cases later in method
Special Case Pattern
- Some arguments just have to be special
- An OO way to deal with special cases
- An object to represent the special case
- Avoids
if
,&&
,try()
&&
and try()
# $? is either nil or a Process::Status object def check_child_exit_status!(status=$?) unless [0,172].include?(status && status.exitstatus) raise ArgumentError, "Command exited with status "\ "#{status.try(exitstatus)}" end end
Substitute Special Case for nil
# $? 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}" end end
Null Object
"A Null Object is an object with defined neutral ("null") behavior." (Wikipedia)
- The special case value is usually
nil
- The special case for
nil
is usually "do nothing"
- A special case of Special Case
- 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
The Black Hole Null Object
- Returns
self
from every call
- Nullifies chains of calls
foo = nil Maybe(foo).bar.baz + buz # => nil
Using the Null Object
if
statement
if options[:out] options[:out] << output end
- Null Object
Maybe(options[:out]) << output
Zero Tolerance for nil
nil
is overused in Ruby code
- It means too many things
- Error
- Missing Data
- Flag for "default behavior"
- Uninitialized variable
- Default return value for
if
,unless
, empty methods.
- Error
- Nil checks are the most common form of timid code
Where'd that nil
come from?!
post_id = params[:post_id] post = if params[:blog] blog = get_blog(params[:blog]) blog.posts.find(:first, post_id) end post.date # => # ~> -:4: undefined method `date' # for nil:NilClass (NoMethodError)
Eliminating nil
Kill it with fire.
Use Hash#fetch
when appropriate
collection.fetch(key) { fallback_action }
#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
- Where did that
nil
come from?
@logger = options[:logger] # ... @logger.info "Something happened" # => # ~> -:3: undefined method `info' for # nil:NilClass (NoMethodError)
Symbol Default
@logger = options.fetch(: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
nil
Null Object with Origin
foo = NullObject.new bar = foo.bar baz = bar.baz # Now let's see where that nil originated baz
#<NullObject:0x7fcace309800 @origin="(irb):68:in `new'">
Step 2: Perform Work
- Keep the focus on the work
- Avoid digressions for error/input handling
PIE Principle
- Program Intently and Expressively
- Say what you mean!
Conditionals for Business Logic
- Conditionals give you more to think about
- Reserve conditionals for business logic
- Minimize conditionals for error, input handling
- Isolate error/input handling from program flow
Conditionals
- Business logic
if post.published? # ... end
- Business logic?
if post # ... end
Confident Styles of Work
Confident Style: Chaining Work
def slug(text) Maybe(text).downcase.strip.tr_s('^a-z0-9', '-') end slug("Confident Code") # => "confident-code" slug(nil) # => #<NullObject:0xb780863c>
Confident Style: Iteration
- As 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([]) # => ""
Step 3: Deliver Results
Don't return nil
- Help your callers be confident
- Return a Special Case or Null Object
- Or raise an error
Step 4: Handle Failure
- Put the happy path first
- Put error-handling at the end
- Or in other methods.
The Ruby Exception Handling Idiom
def foo # ... rescue => error # --- business/failure divider --- # ... end
- Neatly divides method into "work" and "error handling"
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+") # ... }
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
Checked Method
def checked_popen(command, mode, fail_action) check_child_exit_status do @io_class.popen(command, mode) do |process| yield(process) end end rescue Errno::EPIPE fail_action.call end
Checked Method in Use
checked_popen(command, "w+", lambda{message}) do |process| # ... end
The Final Product
Observations on the Final Product
- It has a coherent narrative structure
- It has lower complexity
- It's not necessarily shorter
Why Confident Code?
- Fewer paths == fewer bugs
- Easier to debug
- Self-documenting
"Write code for people first, the computer second"
Review
- A style, not a set of techniques
- Consistent narrative structure
- Handle input, perform work, deliver output, handle failure.
- PIE: Program Intently and Expressively
- Avoid digressions
- State your demands up front
- Terminate nils with extreme prejudice
- Isolate error handling from the main flow
Thank You
-
Book!
- ExceptionalRuby.com
-
25% off:
RAILSCONF11
- Rate!
-
Contact!
- avdi.org / @avdi