Share
Embed
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
Transcript
198 slides
-
@sandimetz Feb 2013 Your application will change. How will that
work out? Thursday, February 28, 13 -
@sandimetz Feb 2013 Rigid Every change forces a cascade of
related changes. Thursday, February 28, 13 -
@sandimetz Feb 2013 Fragile Each change breaks distant and apparently
unrelated things. Thursday, February 28, 13 -
@sandimetz Feb 2013 Immobile The code is hopelessly entangled; reuse
is impossible. Thursday, February 28, 13 -
@sandimetz Feb 2013 Viscous Behaving badly is the most attractive
alternative. Thursday, February 28, 13 -
@sandimetz Feb 2013 In the beginning, your application was perfect.
Thursday, February 28, 13 -
@sandimetz Feb 2013 Dependencies are killing you. Design can save
you. Thursday, February 28, 13 -
@sandimetz Feb 2013 If your project succeeds, it will continue
to cost you money. $$$ Thursday, February 28, 13 -
@sandimetz Feb 2013 Design Principles and Design Patterns Robert Martin
http://www.objectmentor.com Thursday, February 28, 13 -
@sandimetz Feb 2013 Single Responsibility Open/Closed Liskov Substitution Interface Segregation
Dependency Inversion Principles Thursday, February 28, 13 -
@sandimetz Feb 2013 Single Responsibility A class should serve a
single purpose. Thursday, February 28, 13 -
@sandimetz Feb 2013 Open/Closed A module should be open for
extension but closed for modi cation. Thursday, February 28, 13 -
@sandimetz Feb 2013 Liskov Substitution Subclasses should be substitutable for
their base classes. Thursday, February 28, 13 -
@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 -
@sandimetz Feb 2013 Interface Segregation Many client speci c interfaces
are better than one general purpose interface. Thursday, February 28, 13 -
@sandimetz Feb 2013 Dependency Inversion Depend upon abstractions, do not
depend upon concretions. Thursday, February 28, 13 -
@sandimetz Feb 2013 When you know something you depend on
it. Thursday, February 28, 13 -
@sandimetz Feb 2013 If it changes you might change. Thursday,
February 28, 13 -
@sandimetz Feb 2013 Changeable Apps Loosely Coupled Highly Cohesive Easily
Composable Context Independent Thursday, February 28, 13 -
@sandimetz Feb 2013 Achieving Independence Single Responsibility Dependency Inversion Open/Closed
Liskov Substitution Interface Segregation Thursday, February 28, 13 -
@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 -
@sandimetz Feb 2013 FTP Server host: localhost
login: anon password: anon path: Public/prod/ filename:patents.csv Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 class Patent < ActiveRecord::Base def self.overwrite(rows)
# lots of real database i/o end end Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 General Rule Leave it until you need
it Thursday, February 28, 13 -
@sandimetz Feb 2013 Resistance is a Resource Listen to your
code Embrace the friction Fix the problem Thursday, February 28, 13 -
@sandimetz Feb 2013 Testing If testing seems hard, examine your
design. Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job
Local Patents Downloader Thursday, February 28, 13 -
@sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job
Local Patents Patent Downloader Thursday, February 28, 13 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Change PatentJob to use PatentDownloader Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Dependency Injection creates a Seam Remote Patents
Patent Job Local Patents Patent Downloader Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 def run temp
= download_file rows = parse(temp) update_patents(rows) Thursday, February 28, 13 -
@sandimetz Feb 2013 def run temp
= download_file def run temp = PatentDownloader.new.download_file Thursday, February 28, 13 -
@sandimetz Feb 2013 def run temp
= download_file def run temp = PatentDownloader.new.download_file create a dependency Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Write PatentDownloader Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Triangle of Responsibility Refactoring Extract Inject Refactor
Thursday, February 28, 13 -
@sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job
Local Patents Patent Downloader Con g Thursday, February 28, 13 -
@sandimetz Feb 2013 Separate the Responsibilities Remote Patents Patent Job
Local Patents Patent Downloader Patent Con g Thursday, February 28, 13 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Patent Con g Use double at this seam? Thursday, February 28, 13 -
@sandimetz Feb 2013 Write PatentConfig Remote Patents Patent Job Local
Patents Patent Downloader Patent Con g Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Refactor, not because you know the abstraction,
but because you want to nd it. Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Configuration is an abstraction has a single
responsibility is open for extension and closed for modi cation Thursday, February 28, 13 -
@sandimetz Feb 2013 Use Configuration in PatentDownloader Remote Patents Patent
Job Local Patents Patent Downloader Con guration Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Con guration Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Patent
Downloader Con guration Thursday, February 28, 13 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Con
guration Thursday, February 28, 13 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp
Downloader Con guration Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Less More Likelihood of Change Configuration Ftp
Downloader PatentJob patent.yml Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp
Downloader Con guration Thursday, February 28, 13 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp
Downloader Con guration Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 Remote Patents Patent Job Local Patents Ftp
Downloader Con guration Thursday, February 28, 13 -
@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 -
@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 -
@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 -
@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 -
@sandimetz Feb 2013 ActTraining RequiredUserNoti cation OESO RCC PayrollFunding ActiveKeyPerson
Award Many New Jobs Thursday, February 28, 13 -
@sandimetz Feb 2013 SFTP Web Scraper via Mechanize Sql File
Copy Many New Downloaders Thursday, February 28, 13 -
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