SOLID Object-Oriented Design

66ad01cf37c098bfdb76906a490e018f?s=47 Sandi Metz
February 26, 2013

SOLID Object-Oriented Design

This talk explains the object-oriented design principles that underly the SOLID acronym. It defines the principles in plain language and shows practical examples of their use by walking a simple bit of Ruby code through a series of refactorings that move it from specific and concrete to general and abstract.

66ad01cf37c098bfdb76906a490e018f?s=128

Sandi Metz

February 26, 2013
Tweet

Transcript

  1. 5.

    @sandimetz Feb 2013 Rigid Every change forces a cascade of

    related changes. Thursday, February 28, 13
  2. 8.

    @sandimetz Feb 2013 Fragile Each change breaks distant and apparently

    unrelated things. Thursday, February 28, 13
  3. 11.
  4. 14.
  5. 22.

    @sandimetz Feb 2013 If your project succeeds, it will continue

    to cost you money. $$$ Thursday, February 28, 13
  6. 23.

    @sandimetz Feb 2013 Design Principles and Design Patterns Robert Martin

    http://www.objectmentor.com Thursday, February 28, 13
  7. 24.
  8. 25.

    @sandimetz Feb 2013 Single Responsibility A class should serve a

    single purpose. Thursday, February 28, 13
  9. 26.

    @sandimetz Feb 2013 Open/Closed A module should be open for

    extension but closed for modi cation. Thursday, February 28, 13
  10. 27.
  11. 28.

    @sandimetz Feb 2013 Liskov Substitution Subclasses should be substitutable for

    their base classes. Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T. Thursday, February 28, 13
  12. 29.

    @sandimetz Feb 2013 Interface Segregation Many client speci c interfaces

    are better than one general purpose interface. Thursday, February 28, 13
  13. 30.

    @sandimetz Feb 2013 Dependency Inversion Depend upon abstractions, do not

    depend upon concretions. Thursday, February 28, 13
  14. 34.

    @sandimetz Feb 2013 Changeable Apps Loosely Coupled Highly Cohesive Easily

    Composable Context Independent Thursday, February 28, 13
  15. 35.

    @sandimetz Feb 2013 Achieving Independence Single Responsibility Dependency Inversion Open/Closed

    Liskov Substitution Interface Segregation Thursday, February 28, 13
  16. 37.

    @sandimetz Feb 2013 Get a CSV le from an FTP

    server. Store it in a local database. Remote Patents Patent Job Local Patents Thursday, February 28, 13
  17. 38.

    @sandimetz Feb 2013 FTP Server host:       localhost

    login:       anon password:   anon path:       Public/prod/ filename:patents.csv Thursday, February 28, 13
  18. 39.

    @sandimetz Feb 2013 person_id,name,title cycler1,"Anti-­‐Gravity  Simulator","A  device  to  make  riding

     uphill  easier" cycler1,"Exo-­‐Skello  Jello","An  after  dinner  treat  to  bulk  up  the  feeble" sleeper2,"Nap  Compressor","A  device  which  allows  a  30  minute  nap  is  just  3  minutes" Test Data FTP Server host:       localhost login:       anon password:   anon path:       Public/prod/ filename:patents.csv Thursday, February 28, 13
  19. 40.

    @sandimetz Feb 2013 person_id,name,title cycler1,"Anti-­‐Gravity  Simulator","A  device  to  make  riding

     uphill  easier" cycler1,"Exo-­‐Skello  Jello","An  after  dinner  treat  to  bulk  up  the  feeble" sleeper2,"Nap  Compressor","A  device  which  allows  a  30  minute  nap  is  just  3  minutes" Test Data FTP Server host:       localhost login:       anon password:   anon path:       Public/prod/ filename:patents.csv Patent model object Thursday, February 28, 13
  20. 41.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "downloads  the  csv  file  from  the  ftp  server"          it  "asks  Patent  to  overwrite  existing  patents" end Thursday, February 28, 13
  21. 42.

    @sandimetz Feb 2013 PatentJob  Spec    it  "downloads  the  csv

     file  from  the  ftp  server"  do        job  =  PatentJob.new        f  =  File.read(job.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end Thursday, February 28, 13
  22. 43.

    @sandimetz Feb 2013 PatentJob  Spec    it  "downloads  the  csv

     file  from  the  ftp  server"  do        job  =  PatentJob.new        f  =  File.read(job.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end Thursday, February 28, 13
  23. 44.

    @sandimetz Feb 2013 PatentJob  Spec    it  "downloads  the  csv

     file  from  the  ftp  server"  do        job  =  PatentJob.new        f  =  File.read(job.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end Thursday, February 28, 13
  24. 45.

    @sandimetz Feb 2013 PatentJob  Spec        it  "asks

     Patent  to  overwrite  existing  patents"  do        rows  =[{"person_id"=>"cycler1",  "name"=>"Anti-­‐Gravity  Simu                      {"person_id"=>"cycler1",  "name"=>"Exo-­‐Skello  Jello"                      {"person_id"=>"sleeper2",  "name"=>"Nap  Compressor",        job  =  PatentJob.new        Patent.should_receive(:overwrite).with(rows)        job.run    end Thursday, February 28, 13
  25. 46.

    @sandimetz Feb 2013 PatentJob  Spec        it  "asks

     Patent  to  overwrite  existing  patents"  do        rows  =[{"person_id"=>"cycler1",  "name"=>"Anti-­‐Gravity  Simu                      {"person_id"=>"cycler1",  "name"=>"Exo-­‐Skello  Jello"                      {"person_id"=>"sleeper2",  "name"=>"Nap  Compressor",        job  =  PatentJob.new        Patent.should_receive(:overwrite).with(rows)        job.run    end Thursday, February 28, 13
  26. 47.

    @sandimetz Feb 2013 PatentJob  Spec        it  "asks

     Patent  to  overwrite  existing  patents"  do        rows  =[{"person_id"=>"cycler1",  "name"=>"Anti-­‐Gravity  Simu                      {"person_id"=>"cycler1",  "name"=>"Exo-­‐Skello  Jello"                      {"person_id"=>"sleeper2",  "name"=>"Nap  Compressor",        job  =  PatentJob.new        Patent.should_receive(:overwrite).with(rows)        job.run    end Thursday, February 28, 13
  27. 48.

    @sandimetz Feb 2013 PatentJob  Spec    it  "downloads  the  csv

     file  from  the  ftp  server"  do        job  =  PatentJob.new        f  =  File.read(job.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end    it  "asks  Patent  to  overwrite  existing  patents"  do        rows  =[{"person_id"=>"cycler1",  "name"=>"Anti-­‐Gravity  Simu                      {"person_id"=>"cycler1",  "name"=>"Exo-­‐Skello  Jello"                      {"person_id"=>"sleeper2",  "name"=>"Nap  Compressor",        job  =  PatentJob.new        Patent.should_receive(:overwrite).with(rows)        job.run    end Thursday, February 28, 13
  28. 49.

    @sandimetz Feb 2013 class  Patent  <  ActiveRecord::Base    def  self.overwrite(rows)

           #  lots  of  real  database  i/o    end end Thursday, February 28, 13
  29. 50.

    @sandimetz Feb 2013 class  PatentJob      def  run  

         temp  =  download_file        rows  =  parse(temp)        update_patents(rows)    end      def  download_file      def  parse(temp)      def  update_patents(rows) end Thursday, February 28, 13
  30. 51.

    @sandimetz Feb 2013 class  PatentJob      def  run  

         temp  =  download_file        rows  =  parse(temp)        update_patents(rows)    end      def  download_file      def  parse(temp)      def  update_patents(rows) end Thursday, February 28, 13
  31. 52.

    @sandimetz Feb 2013 class  PatentJob      def  run  

         temp  =  download_file        rows  =  parse(temp)        update_patents(rows)    end      def  download_file      def  parse(temp)      def  update_patents(rows) end Thursday, February 28, 13
  32. 53.

    @sandimetz Feb 2013 class  PatentJob      def  run  

         update_patents(parse(download_file))    end      def  download_file      def  parse(temp)      def  update_patents(rows) end Thursday, February 28, 13
  33. 54.

    @sandimetz Feb 2013 class  PatentJob      def  run  

         update_patents(parse(download_file))    end      def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end Thursday, February 28, 13
  34. 55.

    @sandimetz Feb 2013 class  PatentJob    def  run    

       update_patents(parse(download_file))    end        def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end      def  parse(temp)        CSV.read(temp,  :headers  =>  true).map(&:to_hash)    end      def  update_patents(rows)        Patent.overwrite(rows)    end   end Entire PatentJob class Thursday, February 28, 13
  35. 58.

    @sandimetz Feb 2013 I’m uneasy About the code What if

    the ftp host/user/password changes? What if other ftp’ing jobs are required? About the test I hate ftp’ing the le in every test Thursday, February 28, 13
  36. 61.

    @sandimetz Feb 2013 Resistance is a Resource Listen to your

    code Embrace the friction Fix the problem Thursday, February 28, 13
  37. 64.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  38. 65.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  39. 66.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "downloads  the  csv  file  from  the  ftp  server"          it  "asks  Patent  to  overwrite  existing  patents" end Thursday, February 28, 13
  40. 67.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "downloads  the  csv  file  from  the  ftp  server"          it  "asks  Patent  to  overwrite  existing  patents" end downloads and asks Thursday, February 28, 13
  41. 68.

    @sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job

    Local Patents Downloader Thursday, February 28, 13
  42. 69.

    @sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job

    Local Patents Patent Downloader Thursday, February 28, 13
  43. 70.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent

    Downloader Change PatentJob to use PatentDownloader Thursday, February 28, 13
  44. 71.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  45. 72.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  46. 73.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  47. 74.

    @sandimetz Feb 2013 Dependency Injection creates a Seam Remote Patents

    Patent Job Local Patents Patent Downloader Thursday, February 28, 13
  48. 76.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  49. 77.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  50. 78.

    @sandimetz Feb 2013 PatentJob  Spec describe  PatentJob  do    it

     "asks  Patent  to  overwrite  existing  patents"  do        f        =  './spec/fixtures/patents.csv'        rows  =  CSV.read(f,  :headers  =>  true).map(&:to_hash)          downldr  =  double("Downloader")        downldr.stub(:download_file).and_return(f)          job  =  PatentJob.new(downldr)        Patent.should_receive(:overwrite).with(rows)        job.run    end end Thursday, February 28, 13
  51. 79.

    @sandimetz Feb 2013    def  run        temp

     =  download_file        rows  =  parse(temp)        update_patents(rows) Thursday, February 28, 13
  52. 80.

    @sandimetz Feb 2013    def  run        temp

     =  download_file    def  run        temp  =  PatentDownloader.new.download_file Thursday, February 28, 13
  53. 81.

    @sandimetz Feb 2013    def  run        temp

     =  download_file    def  run        temp  =  PatentDownloader.new.download_file create a dependency Thursday, February 28, 13
  54. 82.

    @sandimetz Feb 2013    def  run        temp

     =  download_file    def  run        temp  =  PatentDownloader.new.download class  PatentJob    attr_reader  :downloader      def  initialize(downloader=PatentDownloader.new)        @downloader  =  downloader    end      def  run        temp  =  downloader.download_file Thursday, February 28, 13
  55. 83.

    @sandimetz Feb 2013    def  run        temp

     =  download_file    def  run        temp  =  PatentDownloader.new.download class  PatentJob    attr_reader  :downloader      def  initialize(downloader=PatentDownloader.new)        @downloader  =  downloader    end      def  run        temp  =  downloader.download_file inject a dependency Thursday, February 28, 13
  56. 84.

    @sandimetz Feb 2013    def  run        temp

     =  download_file    def  run        temp  =  PatentDownloader.new.download class  PatentJob    attr_reader  :downloader      def  initialize(downloader=PatentDownloader.new)        @downloader  =  downloader    end      def  run        temp  =  downloader.download_file depend on the message Thursday, February 28, 13
  57. 85.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent

    Downloader Write PatentDownloader Thursday, February 28, 13
  58. 86.

    @sandimetz Feb 2013 PatentDownloader  Spec describe  PatentDownloader  do    it

     "downloads  the  csv  file  from  the  ftp  server"  do        upload_test_file('localhost',  'anon',  'anon',  'patents.csv                downldr  =  PatentDownloader.new        f  =  File.read(downldr.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end end Thursday, February 28, 13
  59. 87.

    @sandimetz Feb 2013 PatentDownloader  Spec describe  PatentDownloader  do    it

     "downloads  the  csv  file  from  the  ftp  server"  do        upload_test_file('localhost',  'anon',  'anon',  'patents.csv                downldr  =  PatentDownloader.new        f  =  File.read(downldr.download_file)        f.should  have(250).characters        f.should  include("just  3  minutes")    end end Copied from PatentJob Spec Thursday, February 28, 13
  60. 88.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Thursday, February 28, 13
  61. 89.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Copied from PatentJob Class Thursday, February 28, 13
  62. 93.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  63. 94.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  64. 95.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Thursday, February 28, 13
  65. 96.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end FTP Thursday, February 28, 13
  66. 97.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end FTP Configuration Thursday, February 28, 13
  67. 99.

    @sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job

    Local Patents Patent Downloader Con g Thursday, February 28, 13
  68. 100.

    @sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job

    Local Patents Patent Downloader Patent Con g Thursday, February 28, 13
  69. 101.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent

    Downloader Patent Con g Use double at this seam? Thursday, February 28, 13
  70. 102.

    @sandimetz Feb 2013 Write PatentConfig Remote Patents Patent Job Local

    Patents Patent Downloader Patent Con g Thursday, February 28, 13
  71. 103.

    @sandimetz Feb 2013 PatentConfig  Spec    it  "knows  the  common

     configuration  values"  do        conf  =  PatentConfig.new        conf.host.should          eql('localhost')        conf.filename.should  eql('patents.csv')        conf.login.should        eql('anon')        conf.password.should  eql('anon')    end Thursday, February 28, 13
  72. 104.

    @sandimetz Feb 2013 PatentConfig  Spec    it  "knows  the  common

     configuration  values"  do        conf  =  PatentConfig.new        conf.host.should          eql('localhost')        conf.filename.should  eql('patents.csv')        conf.login.should        eql('anon')        conf.password.should  eql('anon')    end Thursday, February 28, 13
  73. 105.

    @sandimetz Feb 2013 PatentConfig  Spec    describe  "knows  the  correct

     path  for"  do        it  "production"    do            conf  =  PatentConfig.new('production')            conf.path.should  eql('Public/prod')        end          it  "test"  do            conf  =  PatentConfig.new('test')            conf.path.should  eql('Public/test')        end    end Thursday, February 28, 13
  74. 106.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :env    

     def  initialize(env='production')        @env  =  env    end      def  host        'localhost'    end      def  path        "Public/"  +  ((env  ==  'production')  ?  'prod'  :  'test')    end      def  login        'anon'    end    #  ... Thursday, February 28, 13
  75. 107.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :env    

     def  initialize(env='production')        @env  =  env    end      def  host        'localhost'    end      def  path        "Public/"  +  ((env  ==  'production')  ?  'prod'  :  'test')    end      def  login        'anon'    end    #  ... Yuck Thursday, February 28, 13
  76. 108.

    @sandimetz Feb 2013 Refactor, not because you know the abstraction,

    but because you want to nd it. Thursday, February 28, 13
  77. 109.

    @sandimetz Feb 2013 defaults:  &defaults    host:      

                           localhost    login:                            anon    password:                      anon    filename:                      patents.csv    path:                              Public/test   test:    <<:  *defaults   development:    <<:  *defaults   production:    <<:  *defaults    path:                            Public/prod patent.yml Thursday, February 28, 13
  78. 110.

    @sandimetz Feb 2013 PatentConfig  Spec    describe  "knows  the  correct

     path  for"  do        it  "production"    do            conf  =  PatentConfig.new('production')            conf.path.should  eql('Public/prod')        end          it  "test"  do            conf  =  PatentConfig.new('test')            conf.path.should  eql('Public/test')        end    end Thursday, February 28, 13
  79. 111.

    @sandimetz Feb 2013 PatentConfig  Spec    describe  "knows  the  correct

     path  for"  do        it  "production"    do            conf  =  PatentConfig.new({env:  'production'})            conf.path.should  eql('Public/prod')        end          it  "test"  do            conf  =  PatentConfig.new({env:  'test'})            conf.path.should  eql('Public/test')        end    end Thursday, February 28, 13
  80. 112.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))    end    #  ...    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  81. 113.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))    end    #  ...    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  82. 114.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))    end    #  ...    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  83. 115.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))    end    #  ...    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  84. 116.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))    end    #  ...    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  85. 117.

    @sandimetz Feb 2013 class  PatentConfig      #  ...  

     def  host        data[env]['host']    end      def  login        data[env]['login']    end      def  password        data[env]['password']    end      def  path        data[env]['path']    end    #  ...   end Thursday, February 28, 13
  86. 118.

    @sandimetz Feb 2013 class  PatentConfig      #  ...  

     def  host        data[env]['host']    end      def  login        data[env]['login']    end      def  password        data[env]['password']    end      def  path        data[env]['path']    end    #  ...   end Yuck Thursday, February 28, 13
  87. 119.

    @sandimetz Feb 2013    def  host        data[env]['host']

       end        def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      “localhost”                end                                                  #  end            EOS        end    end Thursday, February 28, 13
  88. 120.

    @sandimetz Feb 2013    def  host        data[env]['host']

       end        def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      “localhost”                end                                                  #  end            EOS        end    end Thursday, February 28, 13
  89. 121.

    @sandimetz Feb 2013    def  host        data[env]['host']

       end        def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      “localhost”                end                                                  #  end            EOS        end    end Thursday, February 28, 13
  90. 122.

    @sandimetz Feb 2013    def  host        data[env]['host']

       end        def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      “localhost”                end                                                  #  end            EOS        end    end Thursday, February 28, 13
  91. 123.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  92. 124.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  93. 125.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end PatentConfig has nothing to do with Patents. Thursday, February 28, 13
  94. 126.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'patent.yml'}    end end Thursday, February 28, 13
  95. 127.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'config.yml'}    end end Thursday, February 28, 13
  96. 128.

    @sandimetz Feb 2013 class  PatentConfig    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'config.yml'}    end end Thursday, February 28, 13
  97. 129.

    @sandimetz Feb 2013 class  Configuration    attr_reader  :data,  :env  

       def  initialize(args={})    def  define_methods_for_environment    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'config.yml'}    end end Thursday, February 28, 13
  98. 130.

    @sandimetz Feb 2013 class  Configuration    attr_reader  :data,  :env  

     def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))        define_methods_for_environment    end    def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      “localhost”                end                                                  #  end            EOS        end    end    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'config.yml'}    end end Entire Configuration class Thursday, February 28, 13
  99. 131.

    @sandimetz Feb 2013 Configuration is an abstraction has a single

    responsibility is open for extension and closed for modi cation Thursday, February 28, 13
  100. 132.

    @sandimetz Feb 2013 Use Configuration in PatentDownloader Remote Patents Patent

    Job Local Patents Patent Downloader Con guration Thursday, February 28, 13
  101. 133.

    @sandimetz Feb 2013 class  PatentDownloader    def  download_file    

       temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Last we saw Thursday, February 28, 13
  102. 134.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Thursday, February 28, 13
  103. 135.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Thursday, February 28, 13
  104. 136.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end end Thursday, February 28, 13
  105. 137.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  106. 138.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent

    Downloader Con guration Thursday, February 28, 13
  107. 139.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  108. 140.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end PatentDownloader has nothing to do with Patents. Thursday, February 28, 13
  109. 141.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  110. 142.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  111. 143.

    @sandimetz Feb 2013 class  PatentDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  112. 144.

    @sandimetz Feb 2013 class  FtpDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end Thursday, February 28, 13
  113. 145.

    @sandimetz Feb 2013 class  FtpDownloader    attr_reader  :config    def

     initialize(config=Configuration.new({filename:  'patent.yml'}))        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                tempname)        end        tempname    end end ??? Thursday, February 28, 13
  114. 146.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent

    Downloader Con guration Thursday, February 28, 13
  115. 147.
  116. 148.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp

    Downloader Con guration Thursday, February 28, 13
  117. 151.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  118. 152.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  119. 153.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v Thursday, February 28, 13
  120. 154.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v PatentJob depends on FtpDownloader Thursday, February 28, 13
  121. 155.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v FtpDownloader depends on Configuration Thursday, February 28, 13
  122. 156.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v FtpDownloader  also depends on patent.yml Thursday, February 28, 13
  123. 157.

    @sandimetz Feb 2013 Less More Likelihood of Change Configuration Ftp

    Downloader PatentJob patent.yml Thursday, February 28, 13
  124. 158.

    @sandimetz Feb 2013 Less More Likelihood of Change Configuration PatentJob

    patent.yml Always depend on things to your left Ftp Downloader Thursday, February 28, 13
  125. 159.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v Thursday, February 28, 13
  126. 160.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v Thursday, February 28, 13
  127. 161.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(downloader=FtpDownloader.new)        @downloader  =  downloader    end class  FtpDownloader    attr_reader  :config    def  initialize(config=Configuration.new(                                                  {filename:  'patent.yml'}))        @config  =  config    end class  Configuration    attr_reader  :data,  :env      def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename])) patent.yml v Thursday, February 28, 13
  128. 162.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  129. 163.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  130. 164.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  131. 165.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  132. 166.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  133. 167.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  134. 168.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp

    Downloader Con guration Thursday, February 28, 13
  135. 169.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp

    Downloader Con guration Thursday, February 28, 13
  136. 172.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  137. 173.

    @sandimetz Feb 2013 Red Green Refactor Is it DRY? Does

    it have a one responsibility? Does everything in it change at the same rate? Does it depend on more stable things? Thursday, February 28, 13
  138. 174.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  139. 175.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end Thursday, February 28, 13
  140. 176.

    @sandimetz Feb 2013 defaults:  &defaults    host:      

                           localhost    login:                            anon    password:                      anon    filename:                      patents.csv    path:                              Public/test    downloader_class:      FtpDownloader   test:    <<:  *defaults   development:    <<:  *defaults   production:    <<:  *defaults    path:                            Public/prod patent.yml Thursday, February 28, 13
  141. 177.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  FtpDownloader.new(config)    end   Thursday, February 28, 13
  142. 178.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    

     def  initialize(opts={})        config              =  opts[:config]          ||=  \                                  Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                  config.connector_class.const_get.new(config)    end   Thursday, February 28, 13
  143. 179.

    @sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp

    Downloader Con guration Thursday, February 28, 13
  144. 182.

    @sandimetz Feb 2013 class  PatentJob    def  run    

       update_patents(parse(download_file))    end        def  download_file        temp  =  Tempfile.new('patents')        tempname  =  temp.path        temp.close        Net::FTP.open('localhost',  'anon',  'anon')  do  |ftp|            ftp.getbinaryfile('Public/prod/patents.csv',  tempname)        end        tempname    end      def  parse(temp)        CSV.read(temp,  :headers  =>  true).map(&:to_hash)    end      def  update_patents(rows)        Patent.overwrite(rows)    end   end Thursday, February 28, 13
  145. 184.

    @sandimetz Feb 2013 class  PatentJob    attr_reader  :downloader    def

     initialize(opts={})        config            =  opts[:config]          ||=  \                                      Configuration.new({filename:  'patent.yml'})        @downloader  =  opts[:downloader]  ||=  \                                    config.connector_class.const_get.new(config)    end      def  run        update_patents(parse(downloader.download_file))    end      def  parse(temp)        CSV.read(temp,  :headers  =>  true).map(&:to_hash)    end      def  update_patents(rows)        Patent.overwrite(rows)    end   end Thursday, February 28, 13
  146. 185.

    @sandimetz Feb 2013 class  FTPDownloader    attr_reader  :config    def

     initialize(config)        @config  =  config    end    def  download_file        temp  =  Tempfile.new(config.filename)        tempname  =  temp.path        temp.close        Net::FTP.open(config.host,                                      config.login,                                      config.password)  do  |ftp|            ftp.getbinaryfile(File.join(config.path,  config.filename),                                                                tempname)        end        tempname    end end Thursday, February 28, 13
  147. 186.

    @sandimetz Feb 2013 class  Configuration    attr_reader  :data,  :env  

     def  initialize(args={})        args    =  defaults.merge(args)        @env    =  args[:env]        @data  =  YAML::load_file(File.join(args[:path],                                                                            args[:filename]))        define_methods_for_environment    end    def  define_methods_for_environment        data[env].each  do  |name,  value|            instance_eval  <<-­‐EOS                def  #{name}                                  #  def  host                    "#{value}"                                #      data[env]['host']                end                                                  #  end            EOS        end    end    def  defaults        {env:            'production',          path:          File.join('config'),          filename:  'config.yml'}    end end Thursday, February 28, 13
  148. 188.

    @sandimetz Feb 2013 SFTP Web Scraper via Mechanize Sql File

    Copy Many New Downloaders Thursday, February 28, 13
  149. 194.

    http://www. ickr.com/photos/freshwater2006/693945631/ http://www. ickr.com/photos/shainerin/3033898043/ http://www. ickr.com/photos/87255087@N00/4184665341/ http://www. ickr.com/photos/robwallace/154301240/ http://www. ickr.com/photos/jpstanley/239788572/

    http://www. ickr.com/photos/nationalgalleries/3110282571/ http://www. ickr.com/photos/familymwr/5112367957/ http://www. ickr.com/photos/marine_corps/5132830788/ http://www. ickr.com/photos/atolsma/6945061383/ http://www. ickr.com/photos/whiteafrican/409072927/ http://www. ickr.com/photos/tabor-roeder/5652475824/in/photostream/ http://www. ickr.com/photos/76526962@N00/7982132871/in/photostream/ http://www. ickr.com/photos/johnspooner/2899977876/ http://www. ickr.com/photos/bike/7721413266/ http://www. ickr.com/photos/stevengrayphotography/6893448452/ http://www. ickr.com/photos/thelearningcurvedotca/5793648808/ http://www. ickr.com/photos/whateverthing/241245603/ Photo Credits Thursday, February 28, 13