ã¯ããã«
ããã«ã¡ã¯ãå æ¥ã®RubyKaigi 2016ã«åå ãã¦ãã@yosuã§ãã Ruby 3ã«åãã話é¡ãå¹ åºããã¼ãã§æ¥½ããã¨ã¦ãåºæ¿ã«ãªãã¾ããã
ãããªä¸åãç¹ã«å°è±¡ã«æ®ã£ãã®ã2æ¥ç®ã®ãã¼ãã¼ããJustin Searlsããã®ãFearlessly Refactoring Legacy Rubyãã§ããç´è¨³ããã¨ãã¬ã¬ã·ã¼ãªRubyã³ã¼ããæãããªãã¡ã¯ã¿ãªã³ã°ãããã§ããããã
ãã¼ã¯ã®å 容ãç´ æ´ãããã£ãã®ã§ããããã®ãã¼ã¯ããã¼ã¹ã«TDDï¼ãã¼ã¯ã»ããªãã³ã»ãããããã¡ã³ãï¼ã§Sutureã¨ããGemã ä½ããã¦ãã¦ãæåãã¦è§¦ã£ã¦ã¿ãã®ã§ä»åã¯ãã¡ãã®Gemãç´¹ä»ãããã¨æãã¾ãã
ã¹ã©ã¤ããå ¬éããã¦ããï¼Surgical Refactors by Justin Searlsï¼ããã¡ãã«ãã®Gemãä½ã£ãèæ¯ãç®æãã´ã¼ã«ã«ã¤ãã¦æ¸ããã¦ãã¾ãã®ã§æ¯éè¦ã¦ã¿ã¦ä¸ããï¼
æ¦è¦
Sutureã¯ã¬ã¬ã·ã¼ã³ã¼ããå®å ¨ã«ãªãã¡ã¯ã¿ãªã³ã°ããããã®ãã¼ã«ã§ãã ããã§ããã¬ã¬ã·ã¼ã³ã¼ãã¯
Legacy Code - Code we don't understand well enough to change confidently.
ã¨å®ç¾©ããã¦ãã¦ããã£ãã訳ãã¨ãèªä¿¡ããã£ã¦å¤æ´ã§ããã»ã©ã«ã¯ååã«ç解ãã¦ããªãã³ã¼ããã®ãã¨ã§ãã
ãããã£ãã³ã¼ãã®ãªãã¡ã¯ã¿ãªã³ã°ã¯å®éã«è¡ãªãäºãé£ãããã¨ããããã¨ãªããã é«ãã³ã¹ãã«å¯¾ãã¦ãã®ãã¸ãã¹ç価å¤ãè©ä¾¡ãããããããä¼ããã説å¾ãã¦å®æ½ããã®ãé£ããã¨ããåé¡ãããã¾ãã
Sutureã¯ãããã£ãã³ã¼ãã®ãªãã¡ã¯ã¿ãªã³ã°ã³ã¹ãããªã¹ã¯ãä¸ããå®æ½ããããããæ çµã¿ãæä¾ãã¾ãã
Sutureãå©ç¨ãããªãã¡ã¯ã¿ãªã³ã°ã®æµã
Sutureã¯ãªãã¡ã¯ã¿ãªã³ã°ãå¿ è¦ãªç®æã®èµ·ç¹ã«ã³ã¼ãã¬ã¤ã¤ã¼ãå·®ãè¾¼ããã¨ã§ã æ¯ãèãã®è¨é²ããæ°æ§ã³ã¼ãã®å®è¡ãã³ã³ããã¼ã«ã§ããããã«ãã¾ãã
Sutureã¯ãã®ã¬ã¤ã¤ã¼ãå©ç¨ãã¦éçºï¼ãã¹ãï¼ãã¹ãã¼ã¸ã³ã°ããããã¯ã·ã§ã³ï¼æ¬çªï¼ã®ããããã§ãªãã¡ã¯ã¿ãªã³ã°ããµãã¼ããã¾ãã
éçºæã®ãµãã¼ã
éçºæã«Sutureãå©ç¨ãããªãã¡ã¯ã¿ãªã³ã°ã®æµãã¯æ¬¡ã®ããã«ãªãã¾ãã
- æ°ã³ã¼ãã¨æ§ã³ã¼ããå·®ãæ¿ããç®æãè¦ã¤ãã
- æ§ã³ã¼ãå®è¡æã®æ¯ãèããSutureã§è¨é²ãã
- è¨é²ããæ¯ãèãããçæããããã¹ããå®æ½ãã
- ãã¹ããå©ç¨ãã¦æ°ã³ã¼ããå®è£ ãã
ã§ã¯ãã®æµããè¦ã¦ããã¾ãããã
æ°ã³ã¼ãã¨æ§ã³ã¼ããå·®ãæ¿ããç®æãè¦ã¤ãã
ã¾ãã次ã®ãããªã³ã¼ãã®å·®ãæ¿ãã«é©ããç®æãè¦ã¤ãã¾ãï¼Sutureã®ããã¥ã¡ã³ãã§ã¯seam=縫ãç®ã¨å¼ãã§ãã¾ãï¼ã
- åé¢ãã¦å®è¡ããããï¼ç¬ç«ãã¦å®è¡ã§ããï¼
- å¼æ°ãåãåã£ã¦å¤ãè¿ã
- å¯ä½ç¨ããªãï¼ã¾ãã¯ã§ããã ãå°ãªãï¼
ä¾ã¨ãã¦Sutureã®ããã¥ã¡ã³ãã«ããã³ã¼ãä¾ã§è¦ã¦ããã¾ãã
class MyWorker def do_work(id) MyMailer.send(LegacyWorker.new.call(id)) end end class LegacyWorker def call(id) thing = Thing.find(id) # ⦠Still 99 lines. Still terrible ⦠thing.result end end
ãã®å ´åãç½®ãæãããã®ã¯ do_work()
ä¸ã® LegacyWorker#call
ã®å®è£
ã§ãããã«Sutureã®ã¬ã¤ã¤ã¼ãå·®ãè¾¼ã¿ã¾ãã
class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, args: [id] })) end end
Sutureãå·®ãè¾¼ãã§ãã¾ãããå¤æ´åã¨åãããã«LegacyWorker.new.call(id)
ãå®è¡ããã¾ãã
Suture.create()
ã®ç¬¬ä¸å¼æ° :worker
ã¯å¾ã§ãã¹ãã³ã¼ãããåç
§ããããã®ååã§ãåºå¥ãä»ãã°ãªãã§ãããã§ãï¼å¾è¿°ï¼ã
old
ã«ã¯ call()
ãå¼ã¹ããã®ï¼MethodãProc/lambdaãªã©ï¼ã渡ãã args
ã«ãã® call()
ã¡ã½ããã®å¼æ°ã渡ãã¾ãã
ä¾ãã°å
ã»ã©ã®ä¾ã§LegacyWorkerã®ã¡ã½ããåã call
ã§ã¯ãªã run
ã ã£ãå ´åã¯æ¬¡ã®ããã«æ¸ãã¾ãã
class LegacyWorker def run(id) thing = Thing.find(id) # ⦠Still 99 lines. Still terrible ⦠thing.result end end class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new.method(:run), # old: -> id { LegacyWorker.new.run(id) }, # old: Proc.new { |id| LegacyWorker.new.run(id) }, args: [id] })) end end
ããã§æ¯ãèããè¨é²ããæºåãã§ãã¾ããã
æ§ã³ã¼ãã®æ¯ãèããè¨é²ãã
ç°å¢å¤æ°ã®SUTURE_RECORD_CALLS=true
ãè¨å®ãã¦ãå
ã»ã©ã®ã³ã¼ããå®è¡ããã¨å®è¡æã®å¼æ°ã¨çµæãsqliteã®ãã¼ã¿ãã¼ã¹ã«è¨é²ããã¾ããRailsã§ããã° SUTURE_RECORD_CALLS=true bundle exec rails s
ã§éçºãµã¼ãã¼ãç«ã¡ä¸ãã¦è§¦ã£ã¦ã¿ãã¤ã¡ã¼ã¸ã§ãã
ããã©ã«ãã®ãã¼ã¿ãã¼ã¹ãã¡ã¤ã«ã¯ db/suture.sqlite3
ã«ä¿åããã¾ãã
â»è¨é²ã®æå¹åï¼ç¡å¹åããã¼ã¿ãã¼ã¹ãã¡ã¤ã«ã®ãã¹ã¯configã§æå®ãããã¨ãã§ãã¾ã
è¨é²ãããDBãã¡ã¤ã«ãè¦ãã¨observationsãã¼ãã«ã«ãã¼ã¿ãè¨é²ããã¦ãããã¨ãåããã¾ãã
$ sqlite3 db/suture.sqlite3 sqlite> .schema CREATE TABLE suture_schema_info ( version integer unique ); CREATE TABLE observations ( id integer primary key, name varchar(255) not null, args clob not null, result clob, error clob, unique(name, args) );
ããã¦ãname
㨠args
ã«ã¦ãã¼ã¯å¶ç´ãã¤ãã¦ãããã¨ãããå¼æ°ã«ãã£ã¦çµæã決ã¾ãå¿
è¦ããããã¨ãåããã¾ãã
å®éãè¤æ°ååãå¼æ°ã§å¼ã³åºããã¨ãã¦ãè¨é²ãããã®ã¯1åã ãã§ãã
ãããåãå¼æ°ã§å¼ã³åºãã¦ãçµæãå¤ãããããªå ´åï¼ä¾ãã°å é¨ã«ç¶æ ãä¿ã¤ã«ã¦ã³ã¿ã¼ã§ã¤ã³ã¯ãªã¡ã³ãçµæãè¿ããªã©ï¼ãSutureã¯ã¨ã©ã¼ãåºåãã¦çµæãè¨é²ãã¾ãããåãå¼æ°ã§éãçµæãè¿ãããã ã¨å¾è¿°ã®ãã¹ãã§ä½¿ããªãããã§ãã
ãã®ãããªå ´åã¯ãä½ããã®æ¹æ³ã§å¼æ°ã¨çµæã®å¯¾å¿ãä¸æã«ãªãããã«å·¥å¤«ãã¦ãããå¿ è¦ãããã¾ãã
çµæã«ç¡è¦ãã¦ãè¯ãå¤ã®å¤åï¼ä¾ãã°ã¿ã¤ã ã¹ã¿ã³ãï¼ãå«ã¾ããå ´åã¯æ¯è¼ã¯ã©ã¹ãã«ã¹ã¿ãã¤ãºãããã¨ã§åé¿ã§ãã¾ãã
å®éSutureããã©ã«ãã®æ¯è¼ã¯ã©ã¹ã§ã¯Railsã®ActiveRecordãæ¯è¼ããå ´åã« created_at
㨠updated_at
ãç¡è¦ãã¾ãã
å é¨ç¶æ ã«ãã£ã¦çµæãå¤ãããããªå ´åã¯ããã®ç¶æ ãå¼æ°ã¨ãã¦æ¸¡ãã¦å®è¡ã§ããããã«ãããªã©ã®å·¥å¤«ãå¿ è¦ã«ãªãã¾ãã
ä¾å¤ã®è¨é²
äºæãã¦ããä¾å¤ãããå ´åã¯ãexpected_error_types
ã«æå®ãã¦ããããã¨ã§Sutureã«ä¼ãããã¨ãã§ãã¾ãã
ããã«ããä¾å¤ãè¨é²ãã¦ãã¹ãã«å©ç¨ã§ãããããã©ã¼ã«ããã¯ãããã©ããï¼å¾è¿°ï¼ã®å¤æã«å©ç¨ããã¾ãã
class SomeError < StandardError; end class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, args: [id], expected_error_types: [SomeError] })) end end
ç¾ç¶ã®æ¯ãèãããã¹ããã
ååã«æ¯ãèããè¨é²ããããè¨é²ãããã¼ã¿ã使ã£ã¦ç¾ç¶ã®æ¯ãèãããã¹ããã¾ãã
class MyWorkerCharacterizationTest < Minitest::Test def setup super # Load the test data needed to resemble the environment when recording end def test_that_it_still_works Suture.verify(:worker, { subject: LegacyWorker.new, fail_fast: true }) end end
Suture.verify
ã¯è¨é²ãããã¼ã¿ã使ã£ã¦ :subjectã§æå®ãã対象ããã¹ããã¾ãã
ããè¨é²æã¨ç°ãªãçµæãè¿ã£ã¦ããå ´åã Suture.verify
ã¯å¤±æãã¾ãã
ãããããå¤æ´åã®ã³ã¼ãã§ãã¹ããããã¨ã¯ãã¹ãç°å¢ãæ£ããä½ãã¦ããããï¼ä¾ãã°ãã¼ã¿ãã¼ã¹ã使ã£ã¦ããå ´åãè¨é²æã¨åãã¹ãããã·ã§ããã復å ã§ãã¦ããããªã©ï¼ããã¹ãã«ãã¬ãã¸ãè¦ãã®ã«å½¹ç«ã¡ã¾ãã
ã«ãã¬ãã¸ã足ãã¦ããªããã°ãéããªãã£ãã³ã¼ããã¹ãéãããã«å度ã¬ã³ã¼ãã£ã³ã°ãã¾ãããã
æ°ããã³ã¼ããã¹ã追å ãã
ãã¹ããå ¨ã¦ãã¹ãããæ°ããã³ã¼ããæ¸ãå§ããæºåã¯OKã§ãã æ°ããã³ã¼ããå®è¡ããããã«ããã«ã¯Sutureã®å¼ã³åºããå¤æ´ãã¾ãã
class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, new: NewWorker.new, args: [id] })) end end class NewWorker def call(id) end end
ãããããã¨ã§oldã¯å¼ã°ãã NewWoker#call
ãå¼ã°ããããã«ãªãã¾ãããã ããå®å
¨ã«æ§ã³ã¼ããåé¤ã§ããããã«ãªãã¾ã§ã¯oldã®æ¹ããã®ã¾ã¾æ®ãã¦ããã¾ãã
ã¾ããæ°ã³ã¼ãã®ãã¹ãã追å ãã¾ãã
class MyWorkerCharacterizationTest < Minitest::Test def setup super # Load the test data needed to resemble the environment when recording end def test_that_it_still_works Suture.verify(:worker, { subject: LegacyWorker.new, fail_fast: true }) end def test_new_thing_also_works Suture.verify(:worker, { subject: NewWorker.new, fail_fast: false }) end end
ãã¨ã¯ãã®ãã¹ããéãããã«ãªãã¾ã§æ°ããã³ã¼ããå®è£ ãã¦ããã¾ãï¼ãã³ã: Refactor or Reimplement the legacy codeï¼
ã¹ãã¼ã¸ã³ã°ç°å¢ã§ã®ãµãã¼ã
ããã¾ã§ã§èªä¿¡ãæã£ã¦æ§ã³ã¼ããåé¤ã§ããããã«ãªã£ã¦ããã°ããã®ã§ããã ã¾ã æ§ã³ã¼ããæ¶ãã«ã¯å¿ãã¨ãªãã¨ãã«ãã¹ãã¼ã¸ã³ã°ç°å¢ã§ç¢ºèªããããã®æ©è½ãããã¾ãã
Sutureã®å¼ã³åºãã« call_both: true
ãæå®ãããã¨ã§æ°æ§ä¸¡æ¹ã®ã³ã¼ããå®è¡ããããçµæãç°ãªãå ´åã¯ä¾å¤ãæããããã«ãªãã¾ãã
ãã¡ãããããå®å
¨ã«å®æ½ã§ããã®ã¯åé¡ã«ãªããããªå¯ä½ç¨ããªãã¨ãã§ãã
class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, new: NewWorker.new, args: [id], call_both: true })) end end
ããã¨ã©ã¼ãå®éã«èµ·ããã°ã¨ã©ã¼å 容ãåèã«ä¿®æ£ãããã¨ã«ãªãã¾ãã
è£è¶³ï¼ raise_on_result_mismatch
ãã©ã¡ã¼ã¿ã false
ã«æå®ãããã¨ã§ä¾å¤ãæããã«ãã°ã ãæ®ããã¨ãã§ãã¾ã
ãããã¯ã·ã§ã³ç°å¢ã§ã®ãµãã¼ã
Sutureã«ã¯ã¹ãã¼ã¸ã³ã°ã§ååã«ãã¹ãããã¨ãã¦ãã¾ã 確å®ã«èªä¿¡ãæã¦ãªãå ´åã®ããã«ãæ¬çªç°å¢ãèæ ®ããæ©è½ãããã¾ãã
ãã°ã®ã«ã¹ã¿ãã¤ãºï¼ãã°ã¬ãã«ãåºåå ãªã©ï¼
ã¢ããªã±ã¼ã·ã§ã³ã®ãã°ã¨ã¯å¥ã«Sutureãæ¤ç¥ããã¨ã©ã¼ãã°ãæ®ãã¨ä¾¿å©ã§ãã 以ä¸ã®è¨å®ã§æ°ããã³ã¼ããã¹ã§ã¨ã©ã¼ãçºçããå ´åãæå®ãããã¡ã¤ã«ã«ãã°ãæ®ãããã«ãªãã¾ãã
Suture.config({ log_level: "WARN", #<-- defaults to "INFO" log_stdout: false, #<-- defaults to true log_io: StringIO.new, #<-- defaults to nil log_file: "log/suture.log" #<-- defaults to nil })
ã«ã¹ã¿ã ã¨ã©ã¼ãã³ãã©
ã«ã¹ã¿ã ã®ã¨ã©ã¼ãã³ãã©ãæå®ãããã¨ã§ãæ°ã³ã¼ãã§ã¨ã©ã¼ãåºãå ´åã«å¤é¨ãµã¼ãã¹ã«éç¥ãããªã©ã®å¦çãè¨è¿°ãããã¨ãã§ãã¾ãã
class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, new: NewWorker.new, args: [id], on_error: -> (name, args) { ErrorHandler.new.notify(name, args) } })) end end
失ææã®ãªãã©ã¤ï¼ãã©ã¼ã«ããã¯ï¼
fallback_on_error
ã« true
ãè¨å®ãããã¨ã§ãæ°ã³ã¼ãã§ã¨ã©ã¼ãåºãå ´åãæ§ã³ã¼ããå®è¡ããã®çµæãè¿ãããã«ã§ãã¾ãã
class MyWorker def do_work(id) MyMailer.send(Suture.create(:worker, { old: LegacyWorker.new, new: NewWorker.new, args: [id], fallback_on_error: true })) end end
ãã®æ©è½ãå©ç¨ããå ´åãæ§ã³ã¼ããåãé¤ãåã«ã¯ãã°ãè¦ã¦åé¡ãèµ·ãã¦ããªãã£ãããã§ãã¯ããå¿ è¦ãããã¾ãããã¾ããã£ã¦ããããã«è¦ãã¦ãããå®ã¯ãã©ã¼ã«ããã¯ã®ãããã ã£ãããªãã¦ãããã¨ããªãããã«ã
è£è¶³ï¼ ã¹ãã¼ã¸ã³ã°æã®æ©è½ã§ç´¹ä»ãã call_both
ã¨çµã¿åããã¦ãçµæãç°ãªãå ´åã¯ãã©ã¼ã«ããã¯ãããã¨ãããããªãã¨ã¯ã§ãã¾ããã ããã¾ã§ã¹ãã¼ã¸ã³ã°ã®æã«å©ç¨ããæ©è½ã¨ãããã¯ã·ã§ã³ã§å©ç¨ããæ©è½ãåãã¦ããããã§ãã
ãã®ä»
Sutureã®ãªãã¸ããªã«ã¯ä»åç´¹ä»ããå 容ã®ä»ã«ããã«è©³ããAPIã®ããã¥ã¡ã³ããããµã³ãã«ã®Railsããã¸ã§ã¯ããããã¾ãã®ã§ãã¡ããåèã«ãªãã¨æãã¾ãã
ã¾ã¨ã
ãããã ã£ãã§ããããã
ã¾ã å ¬éãããã°ããã§å©ç¨çµé¨ã¯ããã¾ããããååãªæ©è½ã¨æè»æ§ã§è©¦ãã¦ã¿ã価å¤ã¯ããã¨æãã¾ãã
ã¾ããå®éã«Sutureãå©ç¨ããªãã«ãã¦ããã©ããã£ãæé ã§ãªãã¡ã¯ã¿ãªã³ã°ãã¦ããã¹ããããã»ã¹ã«ã¤ãã¦èãããã ä¸æ©ãè¸ã¿åºããã£ãããä¸ãã¦ãããã¨æãã¾ãã
ã¯ã©ã¦ãã¯ã¼ã¯ã¹ã§ã¯è¿ã ãã¸ãã¯ãè¤éãªç®æã§å©ç¨ããã¦ããããã«ã¦ã§ã¢ã®ã¢ãããã¼ããäºå®ãã¦ããã®ã§ã ãããã£ãã±ã¼ã¹ã§ãæ´»ç¨ã§ããªããã¨æã£ã¦ãã¾ãã
ãã®è¨äºããã£ããã§Sutureãå©ç¨ãã¦ã¿ãããã¬ã¬ã·ã¼ã³ã¼ãããªãã¡ã¯ã¿ãªã³ã°ããä¸å©ã«ãªãã°å¹¸ãã§ãã
We're hiring!
ã¯ã©ã¦ãã½ã¼ã·ã³ã°ã®ã¯ã©ã¦ãã¯ã¼ã¯ã¹ã§ã¯ã³ã¼ãã¨ã¨ãã«ãµã¼ãã¹ãæé·ãããã¨ã³ã¸ãã¢ãåéãã¦ãã¾ãï¼
èå³ã®ããæ¹ã¯ã寿å¸ã©ã³ããç¡æã§é£ã¹ãªããã話ãã¦ã¿ã¾ãããï¼