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!
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
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
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
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
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
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
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
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])
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
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
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
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
Edges Everywhere There are so many edges in programming The relationships between objects Interactions with frameworks Networks and other systems So much more…
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)
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.
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 # ...
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 # ...
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
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
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
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
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
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 # ...
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 # ...
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
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
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
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