James Edward Gray II
I’ve been in the
community for long
time now
I’ve written code and
docs for Ruby
I’m a Ruby Rogue
Slide 4
Slide 4 text
Edge Cases
Slide 5
Slide 5 text
The ultimate edge case
Slide 6
Slide 6 text
Stargate Universe
The ultimate edge case
Slide 7
Slide 7 text
Figuring Out How to Survive
Flung to the far end of the universe, the SGU
crew has to go back to the basics of survival
Slide 8
Slide 8 text
Edge Cases are the
Currency of Programmers
Most of our development effort is spent on them
If we didn’t have them, programming would be easy
(maybe even automatic)
How many bugs aren’t edge cases?
This is important stuff!
Slide 9
Slide 9 text
Numbers are Behind Letters
What are the minimal cards you have to flip
to prove there’s an even behind each vowel?
Slide 10
Slide 10 text
Numbers are Behind Letters
What are the minimal cards you have to flip
to prove there’s an even behind each vowel?
Slide 11
Slide 11 text
Numbers are Behind Letters
What are the minimal cards you have to flip
to prove there’s an even behind each vowel?
Slide 12
Slide 12 text
Numbers are Behind Letters
What are the minimal cards you have to flip
to prove there’s an even behind each vowel?
Slide 13
Slide 13 text
Reprogramming Their Brains
The crew had to rework their thoughts
to match their new circumstances
Slide 14
Slide 14 text
Edge Case Thinking
Edge cases are the important stuff
The rest of our work is at least easier
Given that, I simply prefer to focus on the edge cases
first, when possible
Slide 15
Slide 15 text
A Stargate Safari
As Destiny travels, Stargates allow the crew
to explore many worlds
Slide 16
Slide 16 text
nil: The Edge of Nothing
Slide 17
Slide 17 text
The Trouble With nil
It’s an edge case, by definition
Given that, it’s best avoided as much as possible
Slide 18
Slide 18 text
Returning nil
This is an example of a common,
but pointless edge case
module GooglePlus
module_function
def has_any_circles?(user)
!!check_api_for_any_circles?(user)
end
def circles(user)
if has_any_circles? user
get_circles_from_api(user)
else
nil
end
end
# ...
end
Slide 19
Slide 19 text
Using nil
We are forced into an edge case
at this end as well
if circles = GooglePlus.circles(user)
circles.each do |circle|
# use with circle here...
end
end
Slide 20
Slide 20 text
My Favorite De-edger
This unusual core Ruby method can guarantee
an Array (Enumerable) result
Array([1, 2, 3]) # => [1, 2, 3]
Array(:not_an_array) # => [:not_an_array]
Array(nil) # => []
Slide 21
Slide 21 text
Dodging the Edge Case
Remove the edge case instead of work with it,
when possible
Array(GooglePlus.circles(user)).each do |circle|
# use with circle here...
end
Slide 22
Slide 22 text
Avoiding Returning nil
This method has a much better contract:
I’ll return to you a list of circles
module GooglePlus
module_function
def circles(user)
Array(get_circles_from_api(user))
end
# ...
end
Slide 23
Slide 23 text
Ideal Usage
Getting the method contract right
automatically fixed this code too
GooglePlus.circles(user).each do |circle|
# use with circle here...
end
Slide 24
Slide 24 text
We Haven’t Lost Anything
We can still handle the no circles case, if needed
circles = GooglePlus.circles(user)
if circles.empty?
# handle no circles here...
else
circles.each do |circle|
# use with circle here...
end
end
Slide 25
Slide 25 text
Slurping Your Food Data
Slide 26
Slide 26 text
Too Much Heat?
When Destiny begins flying straight into a sun,
the crew assumes they are about to burn up
Slide 27
Slide 27 text
Data as a Stream
Avoid “slurping” a ton of data in whenever possible
Big data is a scary edge case
Draw only what you need at once
Come back for more as needed
This applies to all data, including database queries
Slide 28
Slide 28 text
No Slurping
Some exceptions: you produce and consume,
you ensure a reasonable size, etc.
File.read(path).each_line do |line|
# use line here...
end
Slide 29
Slide 29 text
No Slurping
Some exceptions: you produce and consume,
you ensure a reasonable size, etc.
File.read(path).each_line do |line|
# use line here...
end
Slide 30
Slide 30 text
A Better Way
It’s rarely any harder to work with data in chunks
File.foreach(path) do |line|
# use line here...
end
Slide 31
Slide 31 text
The Rails Version
In Rails, all() without a where() is generally
the same as read()
@comments = @post.comments.all
Slide 32
Slide 32 text
The Rails Version
In Rails, all() without a where() is generally
the same as read()
@comments = @post.comments.all
Slide 33
Slide 33 text
Better
It’s OK to grab subsets of data
@comments = @post.comments
.where("created_at >= ?", 1.day.ago)
.all
# ... or ...
@comments = @post.comments.paginate(params[:page])
Slide 34
Slide 34 text
Best
You can even walk all of the data in subsets
@post.comments.find_each do |comment|
# use comment here...
end
Slide 35
Slide 35 text
Edgy Tests
Slide 36
Slide 36 text
Risk Management
Tests are about managing concerns
Edge cases are the primary concerns
Focus your efforts there
Slide 37
Slide 37 text
Test Fodder
Given a trivial method…
class Post < ActiveRecord::Base
def editable?
created_at >= 1.hour.ago
end
# ...
end
Slide 38
Slide 38 text
Boring Tests
We often test it like this
describe Post do
it "is editable within an hour of creation" do
Post.new(created_at: 10.minutes.ago).must_be :editable?
end
it "is not editable beyond an hour of creation" do
Post.new(created_at: 1.day.ago).wont_be :editable?
end
end
Slide 39
Slide 39 text
Boring Tests
We often test it like this
describe Post do
it "is editable within an hour of creation" do
Post.new(created_at: 10.minutes.ago).must_be :editable?
end
it "is not editable beyond an hour of creation" do
Post.new(created_at: 1.day.ago).wont_be :editable?
end
end
Slide 40
Slide 40 text
Moving to the Edge
Spend your paranoia on the edges instead
describe Post do
it "is editable within an hour of creation" do
Post.new(created_at: 1.hour.ago).must_be :editable?
end
it "is not editable beyond an hour of creation" do
Post.new(created_at: (1.hour + 1.minute).ago).wont_be :editable?
end
end
Slide 41
Slide 41 text
Moving to the Edge
Spend your paranoia on the edges instead
describe Post do
it "is editable within an hour of creation" do
Post.new(created_at: 1.hour.ago).must_be :editable?
end
it "is not editable beyond an hour of creation" do
Post.new(created_at: (1.hour + 1.minute).ago).wont_be :editable?
end
end
Slide 42
Slide 42 text
Edges Everywhere
There are so many edges in programming
The relationships between objects
Interactions with frameworks
Networks and other systems
So much more…
Slide 43
Slide 43 text
It Pays to be Prepared
When investigating alien technology, the crew
protects themselves from the environment
Slide 44
Slide 44 text
The Hardline
Make edge cases
impossible, when
you can
# guarantee we do not touch API's
class Geokit::Geocoders::MultiGeocoder
def self.geocode(*args)
fail "Tried to contact Google"
end
end
class Boomerang::APICall
def initialize(*args)
fail "Tried to contact Amazon"
end
end
module SendGridDisabler
def make_api_call(*args)
fail "Tried to contact SendGrid"
end
private :make_api_call
end
SendGrid.extend(SendGridDisabler)
Slide 45
Slide 45 text
All at Once But in Order
Slide 46
Slide 46 text
Eli: An Edge Case Thinker
As the crew’s boy genius, Eli is always
thinking his way around their edge cases
Slide 47
Slide 47 text
Don’t Do Extra Work!
I’m not advocating heavy planning here
There are so many great answers to edge cases
besides hard work:
It won’t affect us
We accept the limitations
There’s a easier way
Etc.
Slide 48
Slide 48 text
Linear Requests (Part 1)
Faraday allows us to abstract away the
process of making requests and use middleware
require "faraday_middleware" # load Faraday plus extra middleware
ua = Faraday.new("https://api.github.com/") do |builder|
builder.use Faraday::Response::ParseJson # parse JSON responses
builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses
builder.adapter Faraday.default_adapter
end
# ...
Slide 49
Slide 49 text
Linear Requests (Part 1)
Faraday allows us to abstract away the
process of making requests and use middleware
require "faraday_middleware" # load Faraday plus extra middleware
ua = Faraday.new("https://api.github.com/") do |builder|
builder.use Faraday::Response::ParseJson # parse JSON responses
builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses
builder.adapter Faraday.default_adapter
end
# ...
Slide 50
Slide 50 text
Linear Requests (Part 2)
We request the list of repos, then fetch
each repo in turn for an issue count
# ...
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
Slide 51
Slide 51 text
Linear Requests (Part 2)
We request the list of repos, then fetch
each repo in turn for an issue count
# ...
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
Slide 52
Slide 52 text
Linear Requests (Part 2)
We request the list of repos, then fetch
each repo in turn for an issue count
# ...
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
Slide 53
Slide 53 text
Linear Requests (Part 2)
We request the list of repos, then fetch
each repo in turn for an issue count
# ...
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
Slide 54
Slide 54 text
Linear Requests (Part 2)
We request the list of repos, then fetch
each repo in turn for an issue count
# ...
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
Slide 55
Slide 55 text
The Request Tree
After fetching the list of repos,
the remaining requests are independent
Slide 56
Slide 56 text
Parallel Requests (Part 1)
Thanks to the abstraction,
switching HTTP clients is trivial
require "faraday_middleware" # load Faraday plus extra middleware
ua = Faraday.new("https://api.github.com/") do |builder|
builder.use Faraday::Response::ParseJson # parse JSON responses
builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses
builder.adapter :typhoeus
end
# ...
Slide 57
Slide 57 text
Parallel Requests (Part 1)
Thanks to the abstraction,
switching HTTP clients is trivial
require "faraday_middleware" # load Faraday plus extra middleware
ua = Faraday.new("https://api.github.com/") do |builder|
builder.use Faraday::Response::ParseJson # parse JSON responses
builder.use Faraday::Response::RaiseError # raise on 40x and 50x responses
builder.adapter :typhoeus
end
# ...
Slide 58
Slide 58 text
Parallel Requests (Part 2)
We can now fetch the list, then handle
the repo requests in parallel
# ...
ua.in_parallel(Typhoeus::Hydra.hydra) do
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
end
Slide 59
Slide 59 text
Parallel Requests (Part 2)
We can now fetch the list, then handle
the repo requests in parallel
# ...
ua.in_parallel(Typhoeus::Hydra.hydra) do
repos = ua.get("/users/JEG2/repos")
repos.on_complete do
repos.body.each do |repo|
name = repo["name"]
project = ua.get("/repos/JEG2/#{name}")
project.on_complete do
issues = project.body["open_issues"]
puts "#{name}: #{issues}" if issues.nonzero?
end
end
end
end
Slide 60
Slide 60 text
The Parallel Advantage
We’ve cut the time for these requests down
by about 84%
$ time ruby linear_requests.rb
highline: 1
faster_csv: 1
real 0m11.087s
user 0m0.466s
sys 0m0.121s
$ time ruby parallel_requests.rb
highline: 1
faster_csv: 1
real 0m1.721s
user 0m0.403s
sys 0m0.104s
Slide 61
Slide 61 text
The Parallel Advantage
We’ve cut the time for these requests down
by about 84%
$ time ruby linear_requests.rb
highline: 1
faster_csv: 1
real 0m11.087s
user 0m0.466s
sys 0m0.121s
$ time ruby parallel_requests.rb
highline: 1
faster_csv: 1
real 0m1.721s
user 0m0.403s
sys 0m0.104s