Your SlideShare is downloading. ×
0
API DESIGN
The James scale of likes and dislikes
WHAT DO WE LIKE?
WHAT DO WE LIKE?


• We   work with a lot of API’s
WHAT DO WE LIKE?


• We   work with a lot of API’s

• We  generally have strong
 opinions about what “feels”
 right to us
WHAT DO WE LIKE?


• We   work with a lot of API’s

• We  generally have strong
 opinions about what “feels”
 right to us
...
I HAVE NO CLUE
So we’re just going to talk about what I like
I HAVE NO CLUE
So we’re just going to talk about what I like
API’S JAMES LIKES
RUBY’S STANDARD LIBRARY
    Where many treasures can be found
RUBY’S STANDARD LIBRARY
    Where many treasures can be found
FILEUTILS
Unix style file manipulations
require "fileutils"

                               FileUtils.cd   dir,      options
                               FileUti...
GOOD DOESN’T MEAN…
  Complex, powerful, all encompassing, etc.
GOOD DOESN’T MEAN…
  Complex, powerful, all encompassing, etc.
OPENURI
The Web as just another IO
#!/usr/bin/env ruby -wKU

                             require "open-uri"



   OPENURI
                             open(...
FAMILIAR
IS GOOD
But don’t be afraid
to add little extras
FAMILIAR
IS GOOD
But don’t be afraid
to add little extras
PATHNAME
Object oriented paths
#!/usr/bin/env ruby -wKU

                        require "pathname"

                        # using plain Ruby
         ...
FRESH IDEAS
Sometimes you just need to try something different
FRESH IDEAS
Sometimes you just need to try something different
PSTORE
A hash in a file
#!/usr/bin/env ruby -wKU

                  require "pstore"


PSTORE            store = PStore.new("data_file.pstore")
   ...
A POWERFUL COMBINATION
 There’s no easier and safer way to manage a little data
A POWERFUL COMBINATION
 There’s no easier and safer way to manage a little data
API’S JAMES DOESN’T LIKE
S3 LIBRARIES
It seems like all S3 wrappers for Ruby are missing some details
S3 LIBRARIES
It seems like all S3 wrappers for Ruby are missing some details
AWS
Streaming double-interface
#!/usr/bin/env ruby -wKU

                             require "rubygems"


        AWS
                             requi...
#!/usr/bin/env ruby -wKU

                             require "rubygems"
                             require "aws"



  ...
OUT OF
  PLACE
Why support half of
a feature in an API?
OUT OF
  PLACE
Why support half of
a feature in an API?
DON’T USE WITH THREADS!
   The many-threads-sharing-one-connection code is
a textbook perfect example of how not to do thr...
DON’T USE WITH THREADS!
   The many-threads-sharing-one-connection code is
a textbook perfect example of how not to do thr...
FOG
Warnings galore
#!/usr/bin/env ruby -wKU

                  require "rubygems"

   FOG            require "fog"

                  s3 = Fo...
!!! DEPRECATION WARNING !!!
                  Hey Champ!  I see you're using Ruby 1.8.6!  While I applaud you for sticking...
(eval):1: warning: method redefined; discarding old reject
                  (eval):1: warning: method redefined; discardi...
PERL WHIPS
  RUBY IN
ONE AREA
 Perl programmers
   use warnings!
PERL WHIPS
  RUBY IN
ONE AREA
 Perl programmers
   use warnings!
FREE
DEBUGGING
Who hates free bug finding?
#!/usr/bin/env ruby -wKU


   FREE
                             class Name
                              def initialize(fir...
#!/usr/bin/env ruby -KU

                             class Name
                              def initialize(first, last)
...
LESS HOOPS
 TO JUGGLE
  In programming,
 that’s always a win
LESS HOOPS
 TO JUGGLE
  In programming,
 that’s always a win
RANDOM COMPARISONS
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
RANDOM COMPARISONS
               AWS          Fog           S3         AWS::S3
                        Not just no,    Pr...
DO WE HAVE A WINNER?
  None of them feel ribbon worthy to me
DO WE HAVE A WINNER?
  None of them feel ribbon worthy to me
DESIGN CHOICES
LET’S TALK TWITTER
Ruby’s Twitter libraries take pretty different approaches
LET’S TALK TWITTER
Ruby’s Twitter libraries take pretty different approaches
THE
TWITTER GEM
A typical object oriented API
#!/usr/bin/env ruby -wKU

                                require "rubygems"


    THE
                                req...
WHAT WE
LIKE TO RIDE
 Most API wrappers
   work like this
WHAT WE
LIKE TO RIDE
 Most API wrappers
   work like this
THE GRACKLE GEM
Grackle is a dynamic wrapper over Twitter’s raw API
#!/usr/bin/env ruby -wKU

require "rubygems"
require "grackle"

client = Grackle::Client.new( :auth => {
 :type        => ...
TEACH
 GRACKLE
NEW TRICKS
All Twitter API updates are
     instantly supported
TEACH
 GRACKLE
NEW TRICKS
All Twitter API updates are
     instantly supported
WHAT ABOUT MONGODB?
How do we bridge the gap to this new kind of storage?
WHAT ABOUT MONGODB?
How do we bridge the gap to this new kind of storage?
MONGO-
  MAPPER
DataMapper for MongoDB
#!/usr/bin/env ruby -wKU

                         require "rubygems"
                         require "mongo_mapper"

   ...
ALMOST THE NORM
This is the kind of data access ActiveRecord has
               trained us to expect
ALMOST THE NORM
This is the kind of data access ActiveRecord has
               trained us to expect
CANDY
A very different approach
#!/usr/bin/env ruby -wKU

require "rubygems"
require "candy"

class Person
 include Candy::Piece
end

me        = Person.n...
#!/usr/bin/env ruby -wKU

require "rubygems"
require "candy"

class People
 include Candy::Collection
 collects :person # ...
ALL ABOUT
THE MAGIC
There’s no save() method!
ALL ABOUT
THE MAGIC
There’s no save() method!
THIS ENDS THE TOUR
THIS ENDS THE TOUR

• It’s
     hard to know what’s
  good or bad in an API
THIS ENDS THE TOUR

• It’s
     hard to know what’s
  good or bad in an API

• They  definitely seem to have
  a “feel” to ...
THIS ENDS THE TOUR

• It’s
     hard to know what’s
  good or bad in an API

• They  definitely seem to have
  a “feel” to ...
ALL IMAGES COURTESY OF
     BLACK AND WTF
     http://blackandwtf.tumblr.com/
DISCUSSION
    TOPIC
Can we see anything in here
that applies to PortableHole?
DISCUSSION
    TOPIC
Can we see anything in here
that applies to PortableHole?
Upcoming SlideShare
Loading in...5
×

API Design

1,631

Published on

This is a tour of API's good, poor, an unusual given to OK.rb in June.

Published in: Technology
0 Comments
5 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
1,631
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
32
Comments
0
Likes
5
Embeds 0
No embeds

No notes for slide




























































  • Transcript of "API Design"

    1. 1. API DESIGN The James scale of likes and dislikes
    2. 2. WHAT DO WE LIKE?
    3. 3. WHAT DO WE LIKE? • We work with a lot of API’s
    4. 4. WHAT DO WE LIKE? • We work with a lot of API’s • We generally have strong opinions about what “feels” right to us
    5. 5. WHAT DO WE LIKE? • We work with a lot of API’s • We generally have strong opinions about what “feels” right to us • How do we decide?
    6. 6. I HAVE NO CLUE So we’re just going to talk about what I like
    7. 7. I HAVE NO CLUE So we’re just going to talk about what I like
    8. 8. API’S JAMES LIKES
    9. 9. RUBY’S STANDARD LIBRARY Where many treasures can be found
    10. 10. RUBY’S STANDARD LIBRARY Where many treasures can be found
    11. 11. FILEUTILS Unix style file manipulations
    12. 12. require "fileutils" FileUtils.cd dir, options FileUtils.cd dir, options do # ... end FileUtils.mkdir dir, options FileUtils.mkdir_p dir, options FileUtils.ln src, dest, options FileUtils.ln_s src, dest, options FILEUTILS FileUtils.cp src, dest, options FileUtils.cp_r src, dest, options FileUtils.mv src, dest, options FileUtils.rm files, options Unix style file manipulations FileUtils.rm_r files, FileUtils.rm_rf files, options options FileUtils.chmod mode, files, options FileUtils.chmod_R mode, files, options FileUtils.touch files, options # # or # # FileUtils::Verbose... # FileUtils::NoWrite... # FileUtils::DryRun... #
    13. 13. GOOD DOESN’T MEAN… Complex, powerful, all encompassing, etc.
    14. 14. GOOD DOESN’T MEAN… Complex, powerful, all encompassing, etc.
    15. 15. OPENURI The Web as just another IO
    16. 16. #!/usr/bin/env ruby -wKU require "open-uri" OPENURI open(__FILE__) do |f| p f.gets # >> "#!/usr/bin/env ruby -wKUn" end The Web as just another IO open("http://blog.grayproductions.net") do |u| p u.gets # >> "<!DOCTYPE HTML PUBLIC …" p u.content_type # >> "text/html" end
    17. 17. FAMILIAR IS GOOD But don’t be afraid to add little extras
    18. 18. FAMILIAR IS GOOD But don’t be afraid to add little extras
    19. 19. PATHNAME Object oriented paths
    20. 20. #!/usr/bin/env ruby -wKU require "pathname" # using plain Ruby if File.exist? some_dir data = File.read( File.join( File.dirname(some_dir), PATHNAME "another_dir", File.basename(some_file) ) ) # ... Object oriented paths end # or using Pathname some_dir = Pathname.new("some_dir") some_file = Pathname.new("some_file") if some_dir.exist? data = ( some_dir.dirname + "another_dir" + some_file.basename ).read # ... end
    21. 21. FRESH IDEAS Sometimes you just need to try something different
    22. 22. FRESH IDEAS Sometimes you just need to try something different
    23. 23. PSTORE A hash in a file
    24. 24. #!/usr/bin/env ruby -wKU require "pstore" PSTORE store = PStore.new("data_file.pstore") store.transaction do # begin transaction store[:single_object] = "My data..." store[:obj_heirarchy] = { "Kev Jackson" => A hash in a file [ "rational.rb", "pstore.rb" ], "James Gray" => [ "erb.rb", "pstore.rb" ] } end # commit changes to file
    25. 25. A POWERFUL COMBINATION There’s no easier and safer way to manage a little data
    26. 26. A POWERFUL COMBINATION There’s no easier and safer way to manage a little data
    27. 27. API’S JAMES DOESN’T LIKE
    28. 28. S3 LIBRARIES It seems like all S3 wrappers for Ruby are missing some details
    29. 29. S3 LIBRARIES It seems like all S3 wrappers for Ruby are missing some details
    30. 30. AWS Streaming double-interface
    31. 31. #!/usr/bin/env ruby -wKU require "rubygems" AWS require "aws" s3 = Aws::S3.new( "KEY_ID", Streaming double-interface ) "SECRET_KEY" bucket = s3.bucket("graysoftinc", :create) open(__FILE__) do |f| bucket.put(File.basename(__FILE__), f) end
    32. 32. #!/usr/bin/env ruby -wKU require "rubygems" require "aws" AWS s3 = Aws::S3Interface.new( "KEY_ID", "SECRET_KEY" ) Streaming double-interface ary_of_hshs = s3.list_bucket("graysoftinc") if hsh = ary_of_hshs.first open("downloaded.rb", "w") do |f| s3.get("graysoftinc", hsh[:key]) do |chunk| f << chunk end end end
    33. 33. OUT OF PLACE Why support half of a feature in an API?
    34. 34. OUT OF PLACE Why support half of a feature in an API?
    35. 35. DON’T USE WITH THREADS! The many-threads-sharing-one-connection code is a textbook perfect example of how not to do threading!
    36. 36. DON’T USE WITH THREADS! The many-threads-sharing-one-connection code is a textbook perfect example of how not to do threading!
    37. 37. FOG Warnings galore
    38. 38. #!/usr/bin/env ruby -wKU require "rubygems" FOG require "fog" s3 = Fog::AWS::S3.new( Warnings galore :aws_access_key_id => "SECRET", :aws_secret_access_key => "SECRET" ) p s3.directories.all
    39. 39. !!! DEPRECATION WARNING !!! Hey Champ!  I see you're using Ruby 1.8.6!  While I applaud you for sticking to your guns and using The One True Ruby, I have to let you know that we're going FOG to stop supporting 1.8.6.  I know, it's sad.  But, we just don't have time to support every version of Ruby out there.  Whether we like it or not, time moves forward and so does our software. On August 1, 2010, we will no longer support Ruby 1.8.6.  If nokogiri happens to Warnings galore work on 1.8.6 after that date, then great!  We will hownever, no longer test, use, or endorse 1.8.6 as a supported platform. Thanks,   Team Nokogiri
    40. 40. (eval):1: warning: method redefined; discarding old reject (eval):1: warning: method redefined; discarding old select /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/collection.rb:40: warning: method redefined; discarding old clear /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/collection.rb:55: warning: method redefined; discarding old inspect /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws.rb:95: warning: redefine image_id /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/terremark/parser.rb:6: warning: method redefined; discarding old parse /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/s3.rb:7: warning: instance variable @required not initialized FOG /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/models/s3/file.rb:20: warning: method redefined; discarding old body /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/models/s3/file.rb:53: warning: method redefined; discarding old owner= /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/models/s3/files.rb:80: warning: method redefined; discarding old directory= Warnings galore /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/parsers/s3/ get_bucket_logging.rb:8: warning: method redefined; discarding old reset /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog/aws/parsers/s3/ get_bucket_logging.rb:13: warning: method redefined; discarding old end_element /usr/local/lib/ruby/gems/1.8/gems/fog-0.1.8/lib/fog.rb:77: warning: instance variable @mocking not initialized /usr/local/lib/ruby/gems/1.8/gems/excon-0.0.26/lib/excon/connection.rb:90: warning: using default DH parameters.   <Fog::AWS::S3::Directories     [       <Fog::AWS::S3::Directory         key="graysoftinc",         creation_date=Wed Jun 09 03:10:07 UTC 2010       >     ]   >
    41. 41. PERL WHIPS RUBY IN ONE AREA Perl programmers use warnings!
    42. 42. PERL WHIPS RUBY IN ONE AREA Perl programmers use warnings!
    43. 43. FREE DEBUGGING Who hates free bug finding?
    44. 44. #!/usr/bin/env ruby -wKU FREE class Name def initialize(first, last) @first = first DEBUGGING @last = last end Who hates free bug finding? def full "#{@firt} #{@last}".strip end end name = Name.new("James", "Gray") puts name.full
    45. 45. #!/usr/bin/env ruby -KU class Name def initialize(first, last) @first = first FREE @last = last end DEBUGGING def first @firt end Who hates free bug finding? end name = Name.new("James", "Gray") Thread.new(name.first) do |first| sleep 3 puts first.capitalize end sleep
    46. 46. LESS HOOPS TO JUGGLE In programming, that’s always a win
    47. 47. LESS HOOPS TO JUGGLE In programming, that’s always a win
    48. 48. RANDOM COMPARISONS
    49. 49. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    50. 50. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    51. 51. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    52. 52. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    53. 53. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    54. 54. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    55. 55. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    56. 56. RANDOM COMPARISONS AWS Fog S3 AWS::S3 Not just no, Present, Docs Good Good but hell no! but weird Works Not for me Yes Yes Yes Streaming With pain Yes No Yes Logs and Features Logs Basics only Fancy logs versions Thread-safe Don’t do it! Yes No No Warnings Yes Uh, yeah! Yes Yes, a lot Command- Extras No Shell No line tool
    57. 57. DO WE HAVE A WINNER? None of them feel ribbon worthy to me
    58. 58. DO WE HAVE A WINNER? None of them feel ribbon worthy to me
    59. 59. DESIGN CHOICES
    60. 60. LET’S TALK TWITTER Ruby’s Twitter libraries take pretty different approaches
    61. 61. LET’S TALK TWITTER Ruby’s Twitter libraries take pretty different approaches
    62. 62. THE TWITTER GEM A typical object oriented API
    63. 63. #!/usr/bin/env ruby -wKU require "rubygems" THE require "twitter" oauth = Twitter::OAuth.new( "consumer token", "consumer secret" ) TWITTER GEM oauth.authorize_from_access( "access token", "access secret" ) client = Twitter::Base.new(oauth) client.friends_timeline.each do |tweet| A typical object oriented API p tweet end client.user_timeline.each do |tweet| p tweet end client.replies.each do |tweet| p tweet end client.update("Heeeyyyyoooo from Twitter Gem!")
    64. 64. WHAT WE LIKE TO RIDE Most API wrappers work like this
    65. 65. WHAT WE LIKE TO RIDE Most API wrappers work like this
    66. 66. THE GRACKLE GEM Grackle is a dynamic wrapper over Twitter’s raw API
    67. 67. #!/usr/bin/env ruby -wKU require "rubygems" require "grackle" client = Grackle::Client.new( :auth => { :type => :oauth, :consumer_key => "KEY", :consumer_secret => "SECRET", :token => "TOKEN", :token_secret => "TOKEN_SECRET" }) # http://twitter.com/users/show.json?screen_name=some_user client.users.show? :screen_name => "some_user" # POST to http://twitter.com/statuses/update.json client.statuses.update! :status => "this status is from grackle" client[:rest].users.show? :id => "hayesdavis" client[:v1].users.show? :id => "hayesdavis" THE GRACKLE GEM Grackle is a dynamic wrapper over Twitter’s raw API
    68. 68. TEACH GRACKLE NEW TRICKS All Twitter API updates are instantly supported
    69. 69. TEACH GRACKLE NEW TRICKS All Twitter API updates are instantly supported
    70. 70. WHAT ABOUT MONGODB? How do we bridge the gap to this new kind of storage?
    71. 71. WHAT ABOUT MONGODB? How do we bridge the gap to this new kind of storage?
    72. 72. MONGO- MAPPER DataMapper for MongoDB
    73. 73. #!/usr/bin/env ruby -wKU require "rubygems" require "mongo_mapper" MongoMapper.database = "testing" MONGO- class User include MongoMapper::Document key :first_name, String, MAPPER :required => true key :last_name, String, :required => true key :token, String, DataMapper for MongoDB :default => lambda { "some random string" } key :age, Integer key :skills, Array key :friend_ids, Array, :typecast => "ObjectId" timestamps! end User.collection.remove # empties collection john = User.create( :first_name => "John", :last_name => "Nunemaker", :age => 28, :skills => ["ruby", "mongo", "javascript"], )
    74. 74. ALMOST THE NORM This is the kind of data access ActiveRecord has trained us to expect
    75. 75. ALMOST THE NORM This is the kind of data access ActiveRecord has trained us to expect
    76. 76. CANDY A very different approach
    77. 77. #!/usr/bin/env ruby -wKU require "rubygems" require "candy" class Person include Candy::Piece end me = Person.new me.last_name = "Eley" # New record created and saved to Mongo me.id # => ObjectID(4bb606f9609c8417cf00004b) me[:height] = 67 # Or me.height = 67 me.favorites = { composer: "Yoko Kanno", seafood: "Maryland blue crabs", scotch: ["Glenmorangie Port Wood Finish", "Balvenie Single Barrel"]} me.spouse = Person.piece(first_name: "Anna", eyes: :blue) me.spouse.eyes # => :blue me.favorites.scotch[1] # => "Balvenie Single Barrel" Person.last_name("Smith") # Returns the first Smith Person.age(21) # Returns the first legal drinker (in the U.S.) Person(12345) # Returns the person with an _id of 12345 CANDY A very different approach
    78. 78. #!/usr/bin/env ruby -wKU require "rubygems" require "candy" class People include Candy::Collection collects :person # Declares the Mongo collection is 'Person' end # (and so is the Candy::Piece class) People.last_name('Smith') # Returns an enumeration of all Smiths People.age(19).sort(:birthdate, :down).limit(10) # We can chain options People(limit: 47, occupation: :ronin) # Or People.all(params) or People.new(params) People.each(|p| p.shout = 'Norm!') # Where everybody knows your name... CANDY A very different approach
    79. 79. ALL ABOUT THE MAGIC There’s no save() method!
    80. 80. ALL ABOUT THE MAGIC There’s no save() method!
    81. 81. THIS ENDS THE TOUR
    82. 82. THIS ENDS THE TOUR • It’s hard to know what’s good or bad in an API
    83. 83. THIS ENDS THE TOUR • It’s hard to know what’s good or bad in an API • They definitely seem to have a “feel” to them though
    84. 84. THIS ENDS THE TOUR • It’s hard to know what’s good or bad in an API • They definitely seem to have a “feel” to them though • Aim for the natural feel, if you can find it
    85. 85. ALL IMAGES COURTESY OF BLACK AND WTF http://blackandwtf.tumblr.com/
    86. 86. DISCUSSION TOPIC Can we see anything in here that applies to PortableHole?
    87. 87. DISCUSSION TOPIC Can we see anything in here that applies to PortableHole?
    1. A particular slide catching your eye?

      Clipping is a handy way to collect important slides you want to go back to later.

    ×