Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SOLID Object-Oriented Design

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.

Sandi Metz

February 26, 2013
Tweet

More Decks by Sandi Metz

Other Decks in Programming

Transcript

  1. Sandi Metz
    @sandimetz Feb 2013
    SOLID
    Thursday, February 28, 13

    View Slide

  2. @sandimetz Feb 2013
    Your application will
    change.
    Thursday, February 28, 13

    View Slide

  3. @sandimetz Feb 2013
    Your application will
    change.
    How will that work out?
    Thursday, February 28, 13

    View Slide

  4. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  5. @sandimetz Feb 2013
    Rigid
    Every change
    forces a cascade
    of related changes.
    Thursday, February 28, 13

    View Slide

  6. @sandimetz Feb 2013
    Rigid
    Thursday, February 28, 13

    View Slide

  7. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  8. @sandimetz Feb 2013
    Fragile
    Each change
    breaks distant and
    apparently unrelated
    things.
    Thursday, February 28, 13

    View Slide

  9. @sandimetz Feb 2013
    Fragile
    Thursday, February 28, 13

    View Slide

  10. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  11. @sandimetz Feb 2013
    Immobile
    The code is
    hopelessly entangled;
    reuse is impossible.
    Thursday, February 28, 13

    View Slide

  12. @sandimetz Feb 2013
    Immobile
    Thursday, February 28, 13

    View Slide

  13. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  14. @sandimetz Feb 2013
    Viscous
    Behaving badly
    is the most attractive
    alternative.
    Thursday, February 28, 13

    View Slide

  15. @sandimetz Feb 2013
    Viscous
    Thursday, February 28, 13

    View Slide

  16. @sandimetz Feb 2013
    It didn’t start that way.
    Thursday, February 28, 13

    View Slide

  17. @sandimetz Feb 2013
    In the
    beginning,
    your
    application
    was
    perfect.
    Thursday, February 28, 13

    View Slide

  18. @sandimetz Feb 2013
    And then
    it changed.
    Thursday, February 28, 13

    View Slide

  19. @sandimetz Feb 2013
    Dependencies are
    killing you.
    Thursday, February 28, 13

    View Slide

  20. @sandimetz Feb 2013
    Dependencies are
    killing you.
    Design can save you.
    Thursday, February 28, 13

    View Slide

  21. @sandimetz Feb 2013
    http://www.martinfowler.com/bliki/DesignStaminaHypothesis.html
    Design Stamina Hypothesis
    Thursday, February 28, 13

    View Slide

  22. @sandimetz Feb 2013
    If your project succeeds,
    it will continue
    to cost you money.
    $$$
    Thursday, February 28, 13

    View Slide

  23. @sandimetz Feb 2013
    Design Principles and
    Design Patterns
    Robert Martin
    http://www.objectmentor.com
    Thursday, February 28, 13

    View Slide

  24. @sandimetz Feb 2013
    Single Responsibility
    Open/Closed
    Liskov Substitution
    Interface Segregation
    Dependency Inversion
    Principles
    Thursday, February 28, 13

    View Slide

  25. @sandimetz Feb 2013
    Single Responsibility
    A class should serve
    a single purpose.
    Thursday, February 28, 13

    View Slide

  26. @sandimetz Feb 2013
    Open/Closed
    A module should be
    open for extension
    but closed for modi cation.
    Thursday, February 28, 13

    View Slide

  27. @sandimetz Feb 2013
    Liskov Substitution
    Subclasses should be
    substitutable
    for their base classes.
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  29. @sandimetz Feb 2013
    Interface Segregation
    Many client speci c interfaces
    are better than
    one general purpose interface.
    Thursday, February 28, 13

    View Slide

  30. @sandimetz Feb 2013
    Dependency Inversion
    Depend upon abstractions,
    do not depend upon
    concretions.
    Thursday, February 28, 13

    View Slide

  31. @sandimetz Feb 2013
    Design is all about
    dependencies.
    Thursday, February 28, 13

    View Slide

  32. @sandimetz Feb 2013
    When you know something
    you depend on it.
    Thursday, February 28, 13

    View Slide

  33. @sandimetz Feb 2013
    If it changes
    you might change.
    Thursday, February 28, 13

    View Slide

  34. @sandimetz Feb 2013
    Changeable Apps
    Loosely Coupled
    Highly Cohesive
    Easily Composable
    Context Independent
    Thursday, February 28, 13

    View Slide

  35. @sandimetz Feb 2013
    Achieving Independence
    Single Responsibility
    Dependency Inversion
    Open/Closed
    Liskov Substitution
    Interface Segregation
    Thursday, February 28, 13

    View Slide

  36. @sandimetz Feb 2013
    Example App
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  38. @sandimetz Feb 2013
    FTP Server
    host:       localhost
    login:    
      anon
    password:  
    anon
    path:       Public/prod/
    filename:patents.csv
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  49. @sandimetz Feb 2013
    class  Patent  <  ActiveRecord::Base
       def  self.overwrite(rows)
           #  lots  of  real  database  i/o
       end
    end
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  56. @sandimetz Feb 2013
    Remote
    Patents
    Patent
    Job
    Local
    Patents
    Thursday, February 28, 13

    View Slide

  57. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  59. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  60. @sandimetz Feb 2013
    General Rule
    Leave it until you need it
    Thursday, February 28, 13

    View Slide

  61. @sandimetz Feb 2013
    Resistance is a Resource
    Listen to your code
    Embrace the friction
    Fix the problem
    Thursday, February 28, 13

    View Slide

  62. @sandimetz Feb 2013
    Testing
    If testing seems hard,
    examine your design.
    Thursday, February 28, 13

    View Slide

  63. @sandimetz Feb 2013
    4 refactorings ...
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  68. @sandimetz Feb 2013
    Separate the Responsibilities
    Remote
    Patents Patent Job
    Local
    Patents
    Downloader
    Thursday, February 28, 13

    View Slide

  69. @sandimetz Feb 2013
    Separate the Responsibilities
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Thursday, February 28, 13

    View Slide

  70. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Change PatentJob
    to use PatentDownloader
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  74. @sandimetz Feb 2013
    Dependency Injection creates a Seam
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Thursday, February 28, 13

    View Slide

  75. @sandimetz Feb 2013
    Patent Job
    Downloader
    Test Double
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  79. @sandimetz Feb 2013
       def  run
           temp  =  download_file
           rows  =  parse(temp)
           update_patents(rows)
    Thursday, February 28, 13

    View Slide

  80. @sandimetz Feb 2013
       def  run
           temp  =  download_file
       def  run
           temp  =  PatentDownloader.new.download_file
    Thursday, February 28, 13

    View Slide

  81. @sandimetz Feb 2013
       def  run
           temp  =  download_file
       def  run
           temp  =  PatentDownloader.new.download_file
    create a dependency
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  85. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Write PatentDownloader
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  90. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Thursday, February 28, 13

    View Slide

  91. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  92. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  98. @sandimetz Feb 2013
    Triangle of Responsibility Refactoring
    Extract
    Inject
    Refactor
    Thursday, February 28, 13

    View Slide

  99. @sandimetz Feb 2013
    Separate the Responsibilities
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Con g
    Thursday, February 28, 13

    View Slide

  100. @sandimetz Feb 2013
    Separate the Responsibilities
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Patent
    Con g
    Thursday, February 28, 13

    View Slide

  101. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Patent
    Con g
    Use double at this seam?
    Thursday, February 28, 13

    View Slide

  102. @sandimetz Feb 2013
    Write PatentConfig
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Patent
    Con g
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  108. @sandimetz Feb 2013
    Refactor,
    not because
    you know the abstraction,
    but because
    you want to nd it.
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  132. @sandimetz Feb 2013
    Use Configuration in PatentDownloader
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  138. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  146. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Patent
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  147. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Con guration
    Thursday, February 28, 13

    View Slide

  148. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Ftp
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  149. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  150. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  157. @sandimetz Feb 2013
    Less More
    Likelihood of Change
    Configuration
    Ftp
    Downloader
    PatentJob patent.yml
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  168. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Ftp
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  169. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Ftp
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  170. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  171. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  179. @sandimetz Feb 2013
    Remote
    Patents Patent Job
    Local
    Patents
    Ftp
    Downloader
    Con guration
    Thursday, February 28, 13

    View Slide

  180. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  181. @sandimetz Feb 2013
    Before
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  183. @sandimetz Feb 2013
    After
    Thursday, February 28, 13

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  187. @sandimetz Feb 2013
    ActTraining
    RequiredUserNoti cation
    OESO
    RCC
    PayrollFunding
    ActiveKeyPerson
    Award
    Many New Jobs
    Thursday, February 28, 13

    View Slide

  188. @sandimetz Feb 2013
    SFTP
    Web Scraper via Mechanize
    Sql
    File Copy
    Many New Downloaders
    Thursday, February 28, 13

    View Slide

  189. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  190. @sandimetz Feb 2013
    The future is coming
    Thursday, February 28, 13

    View Slide

  191. @sandimetz Feb 2013
    Let’s enjoy it
    Thursday, February 28, 13

    View Slide

  192. @sandimetz Feb 2013
    Thursday, February 28, 13

    View Slide

  193. Thanks
    Sandi Metz
    @sandimetz
    https://joind.in/7876
    Thursday, February 28, 13

    View Slide

  194. http://www. ickr.com/photos/freshwater2006/693945631/
    http://www. ickr.com/photos/shainerin/3033898043/
    http://www. ickr.com/photos/[email protected]/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/[email protected]/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

    View Slide

  195. @sandimetz Feb 2013
    http://poodr.info
    Thursday, February 28, 13

    View Slide

  196. @sandimetz Feb 2013
    Hot
    off
    the
    press
    http://poodr.info
    Thursday, February 28, 13

    View Slide

  197. Questions?
    https://joind.in/7876
    Thursday, February 28, 13

    View Slide

  198. @sandimetz Feb 2013
    http://poodr.info
    Thursday, February 28, 13

    View Slide