Confident Code
Avdi Grimm
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
- A style of Code Construction
What this talk is not about
- New Ruby tricks
- New libraries
- Just a collection of techniques
What is confident code?
- The opposite of timid code
Timid Code
- Examples, then themes
Source Code
- cowsay.rb
- An interface to the "cowsay" program
- http://github.com/avdi/cowsay
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
@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
Timid Code
- Lives in fear
- Constantly second-guessing itself
- Randomly mixes input gathering, error handling, and business logic
- Imposes cognitive load on the reader
Confident Code
- Says exactly what it intends to do
- Without provisos or digressions
- 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
Haphazard structure (Annotated)
Step 1: Gather Input
On Duck Typing
- 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
#to_s
,#to_i
,#to_sym
,Array()
- Decorators
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>
- Prefer
Array()
toto_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()
- 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
- 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
- Dynamically
#extend
objects
obj.extend(MyMethods)
- Dynamically add methods
def obj.foo # ... end
Reject Unexpected Values
- 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
- Assertions are documentation
- FailFast
http://fail-fast.rubyforge.org/
Ignore Unnacceptable Values
- Guard Clause
- Null Object
Guard Clause
- 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
"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
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
- nil is overused in Ruby code
- It means too many things
- Error
- Missing Data
- Flag for "default behavior"
- Uninitialized variable
- Error
- 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
{}[: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
- 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
- PIE Principle
Program Intently and Expressively - Avoid digressions for error/input handling
Conditionals for Business Logic
- 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
- 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
- Moving on…
Step 4: Handle Errors
- 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
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
- 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
Shameless Plug
- Caliper
http://getcaliper.com/ - Caliper for the Cowsay project
http://getcaliper.com/caliper/project?repo=git://github.com/avdi/cowsay.git
Flog Complexity
Metrics Diff