cleanliness is next to domain-specificity

69
© Copyright 2007 Viget Labs, LLC – www.viget.com 4 November 2007 Cleanliness is Next to Domain-Specificity Ben Scofield Senior Developer Viget Labs

Upload: viget-labs

Post on 12-May-2015

2.851 views

Category:

Technology


0 download

DESCRIPTION

Ben Scofield discusses linguistics, DSLs in Ruby, and how writing domain-specific code improves your software

TRANSCRIPT

Page 1: Cleanliness is Next to Domain-Specificity

© Copyright 2007 Viget Labs, LLC – www.viget.com4 November 2007

Cleanliness is Next to Domain-SpecificityBen ScofieldSenior DeveloperViget Labs

Page 2: Cleanliness is Next to Domain-Specificity

Part 1: LinguisticsPart 2: Refactoring

Page 3: Cleanliness is Next to Domain-Specificity

The Ruby Community

Page 4: Cleanliness is Next to Domain-Specificity

Interdisciplinaryhttp://www.flickr.com/photos/jesper/1395418767/

Page 5: Cleanliness is Next to Domain-Specificity

Linguistics

Page 6: Cleanliness is Next to Domain-Specificity

Categories

Page 7: Cleanliness is Next to Domain-Specificity

Regional Dialects

Page 8: Cleanliness is Next to Domain-Specificity

(more)

RegionalDialects

Page 9: Cleanliness is Next to Domain-Specificity

Jargons Cants

Page 10: Cleanliness is Next to Domain-Specificity

Pidgins and Creoles

Page 11: Cleanliness is Next to Domain-Specificity

VocabularyGrammar

Page 12: Cleanliness is Next to Domain-Specificity

Ruby Domain-Specific Code

Real DSLs

Page 13: Cleanliness is Next to Domain-Specificity

ActiveRecord

Page 14: Cleanliness is Next to Domain-Specificity

RSpec

Page 15: Cleanliness is Next to Domain-Specificity

Same Grammar,Different Vocabulary

Page 16: Cleanliness is Next to Domain-Specificity

Who Cares?

Page 17: Cleanliness is Next to Domain-Specificity

DSLsIntimidate

and Frighten

DSL

http://www.flickr.com/photos/cwsteeds/58514985/

Page 18: Cleanliness is Next to Domain-Specificity

Write a Parser?No, Thanks.

http://www.flickr.com/photos/rooreynolds/243810988/

Page 20: Cleanliness is Next to Domain-Specificity

Change the VocabularyChange the World

Heroes on NBC - Mondays at 9 PM

Page 21: Cleanliness is Next to Domain-Specificity

API vs. Dialect

Page 22: Cleanliness is Next to Domain-Specificity

Why DSanything?

Page 23: Cleanliness is Next to Domain-Specificity

Who Are We?

Page 24: Cleanliness is Next to Domain-Specificity

http://www.oreillynet.com/onlamp/blog/2006/05/sapirwhorf_is_not_a_klingon.htmlhttp://tech.puredanger.com/2006/11/08/does-your-programming-language-affect-how-you-think/http://snakesgemscoffee.blogspot.com/2006_11_01_archive.htmlhttp://talklikeaduck.denhaven2.com/articles/2007/06/11/sapir-whorfhttp://adams.id.au/blog/2007/10/what-is-behaviour-driven-development/http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/75914http://www.weiqigao.com/blog/2007/09/10/an_interesting_experiment_sapir_whorf_hypothesis.htmlhttp://www.ibm.com/developerworks/blogs/page/pmuellr?tag=rubyhttp://intertwingly.net/blog/2007/10/05/NOChttp://brooders.net/category/perl/http://gilesbowkett.blogspot.com/2007/02/sapir-worf-in-action_19.htmlhttp://blogs.msdn.com/daveremy/archive/2005/04/06/sapirwhorfs.aspxhttp://erlangish.blogspot.com/2007/05/shape-of-your-mind.htmlhttp://www.oreillynet.com/onlamp/blog/2006/06/how_does_a_programming_languag.html http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/105678

Page 25: Cleanliness is Next to Domain-Specificity

Linguistic Determinism

Page 26: Cleanliness is Next to Domain-Specificity

The Hopi

Page 27: Cleanliness is Next to Domain-Specificity

Linguistic Relativism

Page 28: Cleanliness is Next to Domain-Specificity

Snowqanukkaneqkanevvluknatquiknevlukaniuqanikcaqmuruaneqnutaryukqanisqineqqengarukutvaknavcaqpirtapirtuk...

avalancheblizzarddustingflurryfrosthailhardpackigloo pingo powdersleetslushsnowsnowflakesnowstorm...

http://www.flickr.com/photos/maxhunter/79993854/

Page 29: Cleanliness is Next to Domain-Specificity

Color Perception

http://www.flickr.com/photos/thedeplorableword/140856437/

Page 30: Cleanliness is Next to Domain-Specificity

Direction of CausalityDegree of Influence

Page 31: Cleanliness is Next to Domain-Specificity

RSpec

Sapir-Whorf

Page 32: Cleanliness is Next to Domain-Specificity

Testing is Too Late

Page 33: Cleanliness is Next to Domain-Specificity

Specifications Come First

Page 34: Cleanliness is Next to Domain-Specificity

RSpec Leads You in the Right Direction

Page 35: Cleanliness is Next to Domain-Specificity

DSDs are Built on Linguistic Relativism

Page 36: Cleanliness is Next to Domain-Specificity

Keep Your Head in the Domain

Page 37: Cleanliness is Next to Domain-Specificity

Refactoring

Page 38: Cleanliness is Next to Domain-Specificity

Tastes Vary

Page 39: Cleanliness is Next to Domain-Specificity

?

Page 40: Cleanliness is Next to Domain-Specificity

Finding a Ticket

Page 41: Cleanliness is Next to Domain-Specificity

kayak.com

Page 42: Cleanliness is Next to Domain-Specificity

What Does This Do?

Page 43: Cleanliness is Next to Domain-Specificity

Ruby? Awesome!

Page 44: Cleanliness is Next to Domain-Specificity

#! /usr/bin/rubyrequire 'net/http'require 'rexml/document' require 'uri'

@@port = 80

@@token ='YOUR TOKEN HERE'@@hostname = 'www.kayak.com'

@@sparkleinstance = ''

class Trip def initialize() self.legs = [ ] end attr :price, true attr :url, true attr :legs, trueend # class trip

class Hotel def initialize() end

def to_s() end

attr :name, true attr :stars, true attr :price, true attr :hiprice, true attr :loprice, true attr :url, true attr :phone, true attr :address, true attr :city, true attr :region, true attr :country, trueend # class hotel

class Leg def initialize() end

def to_s() str = '' str << self.airlinecode str << ':' str << self.origin str << '>' str << self.destination str << ':' str << self.depart str << ':' str << self.arrive str << ':' str << self.stops str << ':' str << self.duration str << 'min' end

attr :airlinecode, true attr :depart, true attr :arrive, true attr :stops, true attr :duration, true attr :origin, true attr :destination, trueend # class leg

def getsession(token) sid = nil Net::HTTP.start(@@hostname, @@port) do |http| response = http.get("/k/ident/apisession?token=#{token}") body = response.body xml = REXML::Document.new(body) sid = xml.elements['//sid'].text end return sidend

def start_flight_search(sid, oneway, origin, destination, dep_date, ret_date, travelers) url = "/s/apisearch?basicmode=true&oneway=n&origin=#{origin}&destination=#{destination}&destcode=&depart_date=#{dep_date}&depart_time=a&return_date=#{ret_date}&return_time=a&travelers=#{travelers}&cabin=e&action=doflights&apimode=1&_sid_=#{sid}" return start_search(url)end

def start_hotel_search(sid, citystatecountry, dep_date, ret_date, travelers) csc = URI.escape(citystatecountry) dep_date = URI.escape(dep_date) ret_date = URI.escape(ret_date) url = "/s/apisearch?basicmode=true&othercity=#{csc}&checkin_date=#{dep_date}&checkout_date=#{ret_date}&minstars=-1&guests1=#{travelers}&guests2=1&rooms=1&action=dohotels&apimode=1&_sid_=#{sid}" return start_search(url)end

def start_search(url) searchid = nil Net::HTTP.start(@@hostname, @@port) do |http| response = http.get(url) body = response.body puts body File.open("ksearchid.xml", "w") do |f| f.puts(body) end xml = REXML::Document.new(body) searchid = xml.elements['//searchid'] if searchid searchid = searchid.text else puts "search error:" puts body return nil end end return searchidend

@@results = []@@lastcount = 0

# for debugging only, load results# from a file.def poll_results_file(searchtype) f = File.new("ksearchresults.xml", "r") xmltext = f.read return handle_results(searchtype, xmltext)end

def poll_results(searchtype, sid, searchid, count) url = "" case when searchtype == 'f': url = "/s#{@@sparkleinstance}/apibasic/flight?searchid=#{searchid}&apimode=1&_sid_=#{sid}" when searchtype == 'h': url = "/s#{@@sparkleinstance}/apibasic/hotel?searchid=#{searchid}&apimode=1&_sid_=#{sid}" end more = nil Net::HTTP.start(@@hostname, @@port) do |http| if count url += "&c=#{count}" end response = http.get(url) body = response.body File.open("ksearchbody.xml", "w") do |f| f.puts(body) end more = handle_results(searchtype, body) if more != 'true' # save the body, so we can test without doin # an actual search File.open("ksearchresults.xml", "w") do |f| f.puts(body) end end end return moreend

# process the xml result stringdef handle_results(searchtype, body) xml = REXML::Document.new(body) more = xml.elements['/searchresult/morepending'] @@lastcount = xml.elements['/searchresult/count'].text @@sparkleinstance = xml.elements['/searchresult/searchinstance'].text if more more = more.text end if more != 'true' @@results = [] #puts "count=#{@@lastcount}" if (searchtype == 'f') xml.elements.each("/searchresult/trips/trip") do |e| trip = Trip.new() e.each_element("price") do |t| trip.price = t.text trip.url = t.attribute("url") end e.each_element("legs") do |legs| legs.each_element("leg") do |l| leg = Leg.new l.each_element do |ld| # extract the detail from each leg case when ld.name == 'airline': leg.airlinecode = ld.text when ld.name == 'orig': leg.origin = ld.text when ld.name == 'dest': leg.destination = ld.text when ld.name == 'stops': leg.stops = ld.text when ld.name == 'depart': leg.depart = ld.text when ld.name == 'arrive': leg.arrive = ld.text when ld.name == 'duration_minutes': leg.duration = ld.text end end trip.legs << leg end # leg in legs loop end # legs in trip loop #e.each_element("/searchresult/trips/trip/price") { |p| trip.price = p.text } #puts "trip: #{trip.price}" @@results << trip end # each trip end # if flight search

if (searchtype == 'h') xml.elements.each("/searchresult/hotels/hotel") do |e| hotel = Hotel.new() e.each_element("price") do |t| hotel.price = t.text hotel.url = t.attribute("url") end e.each_element("name") { |t| hotel.name = t.text } e.each_element("address") { |t| hotel.address = t.text } e.each_element("city") { |t| hotel.city = t.text } e.each_element("region_code") { |t| hotel.region = t.text_code } e.each_element("city") { |t| hotel.city = t.text } e.each_element("stars") { |t| hotel.stars = t.text} e.each_element("phone") { |t| hotel.phone = t.text} e.each_element("pricehistoryhi") { |t| hotel.hiprice = t.text} e.each_element("pricehistorylo") { |t| hotel.loprice = t.text} @@results << hotel end # each hotel end # if hotel search end return moreend begin if ARGV.size < 4 || ARGV.size > 5 puts "USAGE:" puts "ksearch f ORIGIN_AIPORT DESTINATION_AIRPORT DEPART_DATE [RETURN_DATE]" puts "ksearch h \"city, RC, CC\" CHECKIN_DATE CHECKOUT_DATE" end

searchtype = ARGV[0]

sid = getsession(@@token); if !sid puts "bad token, sorry" exit 1 end puts "session id = #{sid}" if (searchtype == 'f') searchid = start_flight_search(sid, 'n', ARGV[1], ARGV[2], ARGV[3], ARGV[4], 1) end if (searchtype == 'h') searchid = start_hotel_search(sid, ARGV[1], ARGV[2], ARGV[3], 1) end if !searchid puts "search failed. see error document." exit 1 end puts "search id = #{searchid}" sleep(2)

# now poll results (only gets "top 10" each time) more = poll_results(searchtype, sid, searchid, nil) while more == 'true' do more = poll_results(searchtype, sid, searchid, nil) puts "more to come: #{more} #{@@lastcount} so far" sleep(3) end #one final call to get all results (instead of only 10) #poll_results(searchtype, sid, searchid, @@lastcount) poll_results(searchtype, sid, searchid, 10)

# for load test, skip crap exit 0 @@results.each do |r| if searchtype == 'h' puts "#{r.price} url=#{r.url}" puts "#{r.stars} #{r.name} $#{r.loprice} - $#{r.hiprice}" elsif searchtype == 'f' puts "#{r.price} url=#{r.url}" r.legs.each do |leg| puts " #{leg}" end end end

exit(0) more = poll_results_file(searchtype) @@results.each do |r| puts "#{r.price} #{r.url}" r.legs.each do |leg| puts " #{leg}" end endend

Whoa.

Page 45: Cleanliness is Next to Domain-Specificity

Start at the End

Page 46: Cleanliness is Next to Domain-Specificity

I Want to:find flights from CLT to RDU leaving today

and returning in one week

Page 47: Cleanliness is Next to Domain-Specificity

I Want to:find :flights, :from => :CLT, :to => :RDU, :leaving => Date.today, :returning => Date.today + 7

Page 48: Cleanliness is Next to Domain-Specificity

4840

The Old Waysid = getsession(@@token)searchid = start_flight_search(sid, ‘n’, ‘CLT’, ‘RDU’, Date.today, nil, 1)

more = poll_results('f', sid, searchid, nil)while more == 'true' do more = poll_results('f', sid, searchid, nil) sleep(3)end

def poll_results(searchtype, sid, searchid, count) url = "/s#{@@sparkleinstance}/apibasic/flight?searchid=#{searchid}&apimode=1&_sid_=#{sid}" more = nil Net::HTTP.start(@@hostname, @@port) do |http| if count url += "&c=#{count}" end response = http.get(url) body = response.body File.open("ksearchbody.xml", "w") do |f| f.puts(body) end more = handle_results(searchtype, body) if more != 'true' # save the body, so we can test without doin # an actual search File.open("ksearchresults.xml", "w") do |f| f.puts(body) end end end return moreend

Page 49: Cleanliness is Next to Domain-Specificity

4840

The Old Way, cont.def handle_results(searchtype, body) xml = REXML::Document.new(body) more = xml.elements['/searchresult/morepending'] @@lastcount = xml.elements['/searchresult/count'].text @@sparkleinstance = xml.elements['/searchresult/searchinstance'].text if more more = more.text end if more != 'true' @@results = [] #puts "count=#{@@lastcount}" xml.elements.each("/searchresult/trips/trip") do |e| trip = Trip.new() e.each_element("price") do |t| trip.price = t.text trip.url = t.attribute("url") end e.each_element("legs") do |legs| legs.each_element("leg") do |l| leg = Leg.new l.each_element do |ld| # extract the detail from each leg case when ld.name == 'airline': leg.airlinecode = ld.text #... end end trip.legs << leg end # leg in legs loop end # legs in trip loop #e.each_element("/searchresult/trips/trip/price") { |p| trip.price = p.text } #puts "trip: #{trip.price}" @@results << trip end # each trip end return moreend

Page 50: Cleanliness is Next to Domain-Specificity

40

Outputsession_url = "/k/ident/apisession?token=#{token}"

search_url = "/s/apisearch?basicmode=true&oneway=n&origin=#{origin}&destination=#{destination}&destcode=&depart_date=#{dep_date}&depart_time=a&return_date=#{ret_date}&return_time=a&travelers=#{travelers}&cabin=e&action=doflights&apimode=1&_sid_=#{sid}"

results_url = "/s#{@@sparkleinstance}/apibasic/flight?searchid=#{searchid}&apimode=1&_sid_=#{sid}"

Page 51: Cleanliness is Next to Domain-Specificity

40

Expectationsclass KayakTest < Test::Unit::TestCase def test_find_should_call_out_to_session_endpoint setup_mocks_for_find Kayak.find :flights end private def setup_mocks_for_find response = mock(:body => '<?xml version="1.0"?> <ident> <uid>uid</uid> <sid>0123456789</sid> <token>12345</token> <error></error> </ident>') success = mock() success.expects(:get).with('/k/ident/apisession?token=12345').returns (response) Net::HTTP.expects(:start).at_least_once.yields(success) endend

Page 52: Cleanliness is Next to Domain-Specificity

40

Parsing Responsesclass KayakTest < Test::Unit::TestCase def test_session_response_should_be_parsed_for_session_id setup_mocks_for_find Kayak.find :flights assert_equal '0123456789', Kayak.session_id end private def setup_mocks_for_find response = mock(:body => '<?xml version="1.0"?> <ident> <uid>uid</uid> <sid>0123456789</sid> <token>12345</token> <error></error> </ident>') success = mock() success.expects(:get).with('/k/ident/apisession?token=12345').returns (response) Net::HTTP.expects(:start).at_least_once.yields(success) endend

Page 53: Cleanliness is Next to Domain-Specificity

class Kayak @@session_id = nil @@search_id = nil @@search_options = {} TOKEN = '12345' HOSTNAME = 'www.kayak.com' PORT = 80

class << self def session_id @@session_id end def method_missing(name, *args) @@search_options[name] end def find(type, conditions = {}) session_id ||= initialize_session @@search_options[:origin] = conditions[:from] @@search_options[:destination] = conditions[:to] @@search_options[:depart_date] = conditions[:leaving].strftime('%m/%d/%Y') if conditions[:leaving] @@search_options[:leave_date] = conditions[:returning].strftime('%m/%d/%Y') if conditions[:returning]

search_id ||= initialize_search self end def initialize_search Net::HTTP.start(HOSTNAME, PORT) do |http| response = http.get("/s/apisearch?basicmode=true&oneway=n&destcode=&depart_time=a&... if body = response.body xml = REXML::Document.new(body) @@search_id = xml.elements['//searchid'].text end end end

def initialize_session Net::HTTP.start(HOSTNAME, PORT) do |http| response = http.get("/k/ident/apisession?token=#{TOKEN}") if body = response.body xml = REXML::Document.new(body) @@session_id = xml.elements['//sid'].text end end end end end

First Cut

Page 54: Cleanliness is Next to Domain-Specificity

module Kayak TOKEN = '12345' HOSTNAME = 'www.kayak.com' PORT = 80

class Flight attr_accessor :session, :search_id, :search_options

def method_missing(name, *args) search_options[name] end

def initialize(conditions = {}) session ||= Kayak::Session.new self.search_options = {} self.search_options[:origin] = conditions[:from] self.search_options[:destination] = conditions[:to] self.search_options[:depart_date] = conditions[:leaving].strftime('%m/%d/%Y') if conditions[:leaving] self.search_options[:return_date] = conditions[:returning].strftime('%m/%d/%Y') if conditions[:returning]

Net::HTTP.start(HOSTNAME, PORT) do |http| response = http.get("/s/apisearch?basicmode=true&oneway=n&destcode=&depart_time=a&return_date=... self.search_id = Kayak.retrieve(response, '//searchid') end end

def self.find(args) self.new(args) end end class Session attr_accessor :session_id

def initialize Net::HTTP.start(Kayak::HOSTNAME, Kayak::PORT) do |http| response = http.get("/k/ident/apisession?token=#{Kayak::TOKEN}") self.session_id ||= Kayak.retrieve(response, '//sid') end end end

def self.find(type, *args) case type when :flights Kayak::Flight.find(*args) end end

...

Second Cut

Page 55: Cleanliness is Next to Domain-Specificity

I Want to:find :flights, :from => :CLT, :to => :RDU, :leaving => Date.today, :returning => Date.today + 7

Page 56: Cleanliness is Next to Domain-Specificity

Third Cut

?

Page 57: Cleanliness is Next to Domain-Specificity

Always Room for Improvement

Page 58: Cleanliness is Next to Domain-Specificity

Tips

Page 59: Cleanliness is Next to Domain-Specificity

:symbolignore the colon

Page 60: Cleanliness is Next to Domain-Specificity

I Want to:find :flights, :from => :CLT, :to => :RDU, :leaving => Date.today, :returning => Date.today + 7

Page 61: Cleanliness is Next to Domain-Specificity

Optional Parentheseslooks like a sentence

Page 62: Cleanliness is Next to Domain-Specificity

I Want to:find :flights, :from => :CLT, :to => :RDU, :leaving => Date.today, :returning => Date.today + 7

Page 63: Cleanliness is Next to Domain-Specificity

Blocks for AllIn other languages, you have to specify explicitly that a function can accept another function as an argument. But in Ruby, any method can be called with a block as an implicit argument.

Matz, 2003

Page 64: Cleanliness is Next to Domain-Specificity

*arrays from anything

Page 65: Cleanliness is Next to Domain-Specificity

Optional Bracesnot that common

Page 66: Cleanliness is Next to Domain-Specificity

I Want to:find :flights, :from => :CLT, :to => :RDU, :leaving => Date.today, :returning => Date.today + 7

Page 67: Cleanliness is Next to Domain-Specificity

•Start Modestly• Stay in the Domain•Get Better

Page 68: Cleanliness is Next to Domain-Specificity

56

That’s Ittravel safely

Page 69: Cleanliness is Next to Domain-Specificity

© Copyright 2007 Viget Labs, LLC – www.viget.com4 November 2007

Ben [email protected]

http://www.extendviget.com/http://www.culann.com/