99BottlesofOOPSandiMetz⋅KatrinaOwen–0.3,2016-10-06|beta-2
TableofContents
ColophonDedicationPreface
WhatThisBookIsAboutWhoShouldReadThisBookBeforeYouReadThisBookHowtoreadthisbookCodeExamplesErrataAbouttheAuthors
Introduction1.RediscoveringSimplicity
1.1.SimplifyingCode1.1.1.IncomprehensiblyConcise
ConsistencyDuplicationNames
1.1.2.SpeculativelyGeneral1.1.3.ConcretelyAbstract1.1.4.ShamelessGreen
1.2.JudgingCode
1.2.1.EvaluatingCodeBasedonOpinion1.2.2.EvaluatingCodeBasedonFacts
SourceLinesofCodeCyclomaticComplexityAssignments,BranchesandConditions(ABC)Metric
1.2.3.ComparingSolutions1.3.Summary
2.TestDrivingShamelessGreen2.1.UnderstandingTesting2.2.WritingtheFirstTest2.3.RemovingDuplication2.4.UnderstandingTransformations2.5.ToleratingDuplication2.6.HewingtothePlan2.7.ExposingResponsibilities2.8.ChoosingNames2.9.RevealingIntentions2.10.WritingCost-EffectiveTests2.11.AvoidingtheEcho-Chamber2.12.ConsideringOptions2.13.Summary
3.UnearthingConcepts3.1.ListeningtoChange3.2.StartingWiththeOpen/ClosedPrinciple3.3.RecognizingCodeSmells3.4.IdentifyingtheBestPointofAttack3.5.RefactoringSystematically3.6.FollowingtheFlockingRules
3.7.ConvergingonAbstractions3.7.1.FocusingonDifference3.7.2.SimplifyingHardProblems3.7.3.NamingConcepts3.7.4.MakingMethodicalTransformations3.7.5.RefactoringGradually
3.8.Summary4.PracticingHorizontalRefactoring
4.1.ReplacingDifferenceWithSameness4.2.EquivocatingAboutNames4.3.DerivingNamesFromResponsibilities4.4.ChoosingMeaningfulDefaults4.5.SeekingStableLandingPoints4.6.ObeyingtheLiskovSubstitutionPrinciple4.7.TakingBiggerSteps4.8.DiscoveringDeeperAbstractions4.9.DependingonAbstractions4.10.Summary
5.SeparatingResponsibilities5.1.SelectingtheTargetCodeSmell
5.1.1.IdentifyingPatternsinCode5.1.2.SpottingCommonQualities5.1.3.EnumeratingFlockedMethodCommonalities5.1.4.InsistingUponMessages
5.2.ExtractingClasses5.2.1.ModelingAbstractions5.2.2.NamingClasses5.2.3.ExtractingBottleNumber
5.2.4.RemovingArguments5.2.5.TrustingtheProcess
5.3.AppreciatingImmutability5.4.AssumingFastEnough5.5.CreatingBottleNumbers5.6.RecognizingLiskovViolations5.7.Summary
6.ReplacingConditionalswithObjectsAppendixA:Prerequisites
A.1.RubyA.2.Minitest
AppendixB:InitialExerciseB.1.GettingtheexerciseB.2.DoingtheexerciseB.3.TestSuite
Acknowledgements
ColophonVersion:0.3
VersionDate:2016-10-06
VersionNotes:beta-2
ISBN-10:1-944823-00-X
ISBN-13:978-1-944823-00-9
PublishedBy:PotatoCanyonSoftware,LLC
1stEdition
Copyright:2016
CoverDesignandArtbyLindseyMorris.
CreatedusingAsciidoctor.
DedicationSandi
ToAmy,foreverythingsheisanddoes,andtoJasper,whotaughtmethatnothingtrumpsagoodwalk.
Katrina
ToSander,whosepersistenceisoutofthisworld.
PrefaceItturnsoutthateverythingyouneedtoknowaboutObject-OrientedDesign(OOD)canbelearnedfromthe99BottlesofBeersong.
Well,perhapsnoteverything,butquitecertainly,agreatmanythings.
Thesongissimultaneouslyeasytounderstandandfullofhiddencomplexity,whichmakesittheperfectskeletonuponwhichtohanglessonsinOOD.Thelessonsembeddedwithinthesongaresouseful,andsobroad,thatoverthelastthreeyearsithasbecomeacorepartofthecurriculumofSandiMetz’sPracticalObject-OrientedDesigncourse.
ThethoughtsinthisbookreflectcountlesshoursofdiscussionandcollaborationbetweenSandiandKatrinaOwen.Theseideashavebeenbattle-testedbyhundredsofstudents,andrefinedbyaseriesofdeeplythoughtfulco-instructors,beginningwithKatrina.WhileneitherKatrinanorSandihavethehubristoclaimperfectunderstanding,bothhavelearnedagreatdealaboutObject-OrientedDesignfromteachingthissong,andhavecometofeelthatit’stimetobuckitupandwriteitdown.
Therefore,thisbook.Wehopethatyoufinditbothusefulandenjoyable.
WhatThisBookIsAboutThisbookisaboutwritingcost-effective,maintainable,andpleasingcode.
Chapter1exploreshowtodecideifcodeis"goodenough."Thischapterusesmetricstocompareseveralpossiblesolutionstothe
99Bottlesproblem.ItintroducesatypeofsolutionknownasShamelessGreen,andarguesthatalthoughShamelessGreenisneitherclevernorchangeable,itisthebestinitialsolutiontomanyproblems.
Chapter2isaprimerforTest-DrivenDevelopment(TDD),whichisusedtofindShamelessGreen.Thischapterisconcernedwithdecidingwhattotest,andwithcreatingteststhathappilytoleratechangestotheunderlyingcode.
Chapter3introducesanewrequirement(six-pack),whichleadstoadiscussionofhowtodecidewheretostartwhenchangingcode.ThischapterexaminestheOpen/ClosedPrinciple,andthenexplorescodesmells.ThechapterthendefinesasimplesetofFlockingRuleswhichguideastep-by-steprefactoringofcode.
Chapter4continuesthestep-by-steprefactoringbeguninChapter3.ItiterativelyappliestheFlockingRules,eventuallystumblesacrosstheneedfortheLiskovSubstitutionPrinciple,andultimatelyunearthsadeeplyhiddenabstraction.
Chapter5inventoriestheexistingcodeforsmells,choosesthemostprominentone,andusesittotriggerthecreationofanewclass.Alongthewayittakesahardlookatimmutability,performance,andcaching.
Chapter6isnotyetavailable.Thischapterperformsamiraclewhichnotonlyremovesallconditionals,butalsoallowsyoutofinallyimplementthenewsix-packrequirementwithoutalteringanyexistingcode.
WhoShouldReadThisBookThelessonsinthebookhavebeenfoundusefulbyprogrammerswithabroadrangeofexperience,fromranknovicethroughgrizzledveteran.Despitewhatonemightpredict,novicesoftenhaveaneasiertimewiththismaterial.Astheyare
unencumberedbypriorknowledge,theirmindsareopen,andeasilyabsorbtheseideas.
It’stheveteranswhostruggle.Theirhabitsaredeeplyingrained.Theyknowthemselvestobegoodatprogramming.Theyfeelquick,andefficient,andsoresistnewtechniques,especiallywhenthosetechniquestemporarilyslowthemdown.
Thisbookwillbeusefulifyouareaveteran,butitcannotbedeniedthatitteachesprogrammingtechniquesthatlikelycontradictyourcurrentpractice.Changingentrenchedideascanbepainful.However,youcannotmakeinformeddecisionsaboutthevalueofnewideasunlessyouthoroughlyunderstandthem,andtounderstandthemyoumustcommit,wholeheartedly,tolearningthem.
Therefore,ifyouareaveteran,it’sbesttoadoptthenovicemindsetbeforereadingon.Setasidepriorbeliefs,anddedicateyourselftowhatfollows.Whilereading,resisttheurgetoresist.Readtheentirebook,worktheproblems,andonlythendecidewhethertointegratetheseideasintoyourdailypractice.
BeforeYouReadThisBookYou’lllearnmorefromthisbookifyouspend30minutesworkingonthe99BottlesofBeerproblembeforestartingtoread.Seetheappendixforinstructions.
Ifyoujustwanttoreadonbutyoudon’tknowRuby,havenofear.Thesyntaxofthelanguageissostraightforwardthatyou’llhavenotroubleunderstandingwhatfollows.TheideasinthisbookarenotaboutRuby,they’reaboutobject-orientedprogramminganddesign.
Howtoreadthisbook
Thechaptersbuildupononeanother,andsoshouldbereadinorder.Whileisolatedsectionsmaybeuseful,thewholeismorethanthesumofitsparts.Theideasgainpowerinrelationtooneanother.
Togetthemostfromthebook,workthecodesamplesasyoureadalong.Withactiveparticipationyou’lllearnmore,understandbetter,andretainlonger.Whilereadinghasvalue,doinghasmore.
CodeExamplesTheexamplesarewritteninRuby,andtheexercisesrelyonMinitest.Thecodeisavailableinthe99bottlesrepositoryonGitHub,whichcontainsabranchforeachchapter.
ErrataAcurrentlistoferrataislocatedatwww.sandimetz.com/99bottles/errata.Ifyoufindadditionalerrors,[email protected].
AbouttheAuthorsSandiMetz
SandiistheauthorofPracticalObject-OrientedDesigninRuby.Shehasthirtyyearsofexperienceworkingonlargeobject-orientedapplications.She’sspokenaboutprogramming,object-orienteddesignandrefactoringatnumerousconferencesincludingAgileAllianceTechnicalConference,CraftConf,Øredev,RailsConf,andRubyConf.Shebelievesinsimplecodeandstraightforwardexplanations,andistheproudrecipientofaRubyHeroawardforhercontributiontotheRubycommunity.Sheprefersworkingsoftware,practicalsolutionsandlengthybicycletrips(notnecessarilyinthatorder).Findoutmoreabout
Sandiatsandimetz.com.
KatrinaOwen
KatrinaworksforGitHubasanAdvocateontheOpenSourceteam.KatrinahastenyearsofexperienceandworksprimarilyinGoandRuby.Sheisthecreatorofexercism.io,aplatformforprogrammingskilldevelopmentinmorethan30languages.She’sspokenaboutrefactoringandopensourceatinternationalconferencessuchasNordicRuby,Mix-IT,SoftwareCraftsmanshipNorthAmerica,OSCON,BathRubyandRailsConf.ShereceivedaRubyHeroawardforhercontributiontotheRubycommunity.Whenprogramming,herfocusisonautomation,workflowoptimization,andrefactoring.FindoutmoreaboutKatrinaatkytrinyx.com.
IntroductionThisbookcreatesasimplesolutiontothe99BottlesofBeersongproblem,andthenappliesaseriesofrefactoringstoimprovethedesignofthecode.
Putthatway,thetopicsoundssopainfullyobviousthatonemightreasonablywonderifthisentiretomecouldbereplacedbyafewsamplesofcode.Theserefactoring"endpoints"wouldbeafractionofthesizeofthisbook,andavastlyquickerread.Unfortunately,theywouldteachyoualmostnothingaboutprogramming.Writingcodeistheprocessofworkingyourwaytothenextstableendpoint,nottheendpointitself.Youdon’tknowtheanswerinadvance,insteadyouareseekingit.
Thisbookdocumentseverystepdowneverypathofcode,andsoprovidesaguided-tourofthedecisionsmadealongtheway.Itnotonlyshowshowgoodcodelookswhenit’sdone,itrevealsthethoughtsthatproducedit.Itaimstoleavenothingout.Itflingsbacktheveilandexposesthesausagebeingmade.
Onefinalnotebeforedivingintothebookproper.Thechaptersthatfollowapplyageneral,broad,solutiontoaspecific,narrow,problem.Theauthorscheerfullystipulatetothefactthatyouareunlikelytoencounterthe99BottlesofBeersonginyourdailywork,andthatproblemsofsimilarsizearebestsolvedverysimply.Forthepurposesofthisbook,99Bottlesisconvenientbecauseit’ssimultaneouslyeasilyunderstandableandsurprisinglycomplex,andsoprovidesarefreshingstand-inforlargerproblems.Onceyouunderstandthesolutionshere,you’llbeabletoapplythemtothemuchlargerrealworld.
Withthat,ontothebook.
1.RediscoveringSimplicityWhenyouwerenewtoprogrammingyouwrotesimplecode.Althoughyoumaynothaveappreciateditatthetime,thiswasagreatstrength.Sincethen,you’velearnednewskills,tackledharderproblems,andproducedincreasinglycomplexsolutions.Experiencehastaughtyouthatmostcodewillsomedaychange,andyou’vebeguntocraftitinanticipationofthatday.Complexityseemsbothnaturalandinevitable.
Whereyouonceoptimizedcodeforunderstandability,younowfocusonitschangeability.Yourcodeislessconcretebutmoreabstract—you’vemadeitinitiallyhardertounderstandinhopesthatitwillultimatelybeeasiertomaintain.
ThisisthebasicpromiseofObject-OrientedDesign(OOD):thatifyou’rewillingtoacceptincreasesinthecomplexityofyourcodealongsomedimensions,you’llberewardedwithdecreasesincomplexityalongothers.OODdoesn’tclaimtobefree;itmerelyassertsthatitsbenefitsoutweighitscosts.
Designdecisionsinevitablyinvolvetrade-offs.There’salwaysacost.Forexample,ifyou’veduplicatedabitofcodeinmanyplaces,theDon’tRepeatYourself(DRY)principletellsyoutoextracttheduplicationintoasinglecommonmethodandtheninvokethisnewmethodinplaceoftheoldcode.DRYisagreatidea,butthatdoesn’tmeanit’sfree.ThepriceyoupayforDRYingoutcodeisthattheinvokerofthenewmethodnolongerknowstheresult,onlythemessageitshouldsend.Ifyou’rewillingtopaythisprice(i.e.beingwillinglyignorantoftheactualbehavior),therewardyoureapisthatwhenthebehaviorchanges,youneedalteryourcodeinonlyoneplace.TheargumentthatOODmakesisthatthisbargainwillsaveyoumoney.
Didyoudivideonelargeclassintomanysmallones?Youcannowreusethenewclassesindependentlyofoneanother,butit’snolongerobvioushowtheyfittogetherfortheoriginalcase.Haveyouinjectedadependencyinsteadofhard-codingtheclassnameofacollaborator?Thereceivercannowfreelydependonnewandpreviouslyunforeseenobjects,butitmustremainignorantoftheiractualclass.
Theexamplesabovechangecodebyincreasingitslevelofabstraction.DRYingoutcodeinsertsalevelofindirectionbetweentheplacethatusesbehaviorandtheplacethatdefinesit.Breakingonelargeclassintomanyforcesthecreationofsomethingnewtoembodytherelationshipbetweenthepieces.Injectingadependencytransformsthereceiverintosomethingthatdependsonanabstractroleratherthanaconcreteclass.
Eachofthesedesignchoiceshascosts,anditonlymakessensetopaythesecostsifyoualsoaccruesomeoffsettingbenefits.Designisthusaboutpickingtherightabstractions.Ifyouchoosewell,yourcodewillbeexpressive,understandableandflexible,andeveryonewilllovebothitandyou.However,ifyougettheabstractionswrong,yourcodewillbeconvoluted,confusing,andcostly,andyourprogrammingpeerswillhateyou.
Unfortunately,abstractionsarehard,andevenwiththebestofintentions,it’seasytogetthemwrong.Well-meaningprogrammerstendtooveranticipateabstractions,inferringthemprematurelyfromincompleteinformation.Earlyabstractionsareoftennotquiterightandthereforetheycreateacatch-22.[1]Youcan’tcreatetherightabstractionuntilyoufullyunderstandthecode,buttheexistenceofthewrongabstractionmaypreventyoufromeverdoingso.Thissuggeststhatyoushouldnotreachforabstractions,butinstead,youshouldresistthemuntiltheyabsolutelyinsistuponbeingcreated.
Thisbookisaboutfindingtherightabstraction.Thisfirstchapterstartsbypeelingawaythefogofcomplexityanddefiningwhatit
meanstowritesimplecode.
1.1.SimplifyingCodeThecodeyouwriteshouldmeettwooftencontradictorygoals.Itmustremainconcreteenoughtobeunderstoodwhilesimultaneouslybeingabstractenoughtoallowforchange.
Imagineacontinuumwith"mostconcrete"atoneendand"mostabstract"attheother.Codeattheconcreteendmightbeexpressedasasinglelongprocedurefullof if statements.Codeattheabstractendmightconsistofmanyclasses,eachwithonemethodcontainingasinglelineofcode.
Thebestsolutionformostproblemsliesnotattheextremesofthiscontinuum,butsomewhereinthemiddle.There’sasweetspotthatrepresentstheperfectcompromisebetweencomprehensionandchangeability,andit’syourjobasaprogrammertofindit.
Thissectiondiscussesfourdifferentsolutionstothe99BottlesofBeerproblem.Thesesolutionsvaryincomplexityandthusillustratedifferentpointsalongthiscontinuum.
Youmustnowmakeadecision.Asyouwereforewarnedinthepreface,thebestwaytolearnfromthisbookistoworktheexercisesyourself.Ifyoucontinuereadingbeforesolvingtheprobleminyourownway,yourideaswillbecontaminatedbythecodethatfollows.Therefore,ifyouplantoworkalong,godothe99Bottlesexercisenow.Whenyou’refinished,you’llbereadytoexaminethefollowingfoursolutions.
1.1.1.IncomprehensiblyConciseHere’sthefirstoffourdifferentsolutionstothe99Bottlessong.
Listing1.1:IncomprehensiblyConcise
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(hi,lo)7 hi.downto(lo).map{|n|verse(n)}.join("\n")8 end9 10 defverse(n)11 "#{n==0?'Nomore':n}bottle#{'s'ifn!=1}"+12 "ofbeeronthewall,"+13 "#{n==0?'nomore':n}bottle#{'s'ifn!=1}ofbeer.\n"+14 "#{n>0?"Take#{n>1?'one':'it'}downandpassitaround"15 :"Gotothestoreandbuysomemore"},"+16 "#{n-1<0?99:n-1==0?'nomore':n-1}bottle#{'s'ifn-1!=1}"+17 "ofbeeronthewall.\n"18 end19 end
Thisfirstsolutionembedsagreatdealoflogicintotheversestring.Thecodeaboveperformsaneattrick.Itmanagestobeconcisetothepointofincomprehensibilitywhilesimultaneouslyretainingloadsofduplication.Thiscodeishardtounderstand
becauseitisinconsistentandduplicative,andbecauseitcontainshiddenconceptsthatitdoesnotname.
ConsistencyThestyleoftheconditionalsisinconsistent.Mostusetheternaryform,asonline11:
Otherstatementsaremadeconditionalbyaddingatrailing if .Line11againprovidesanexample:
Finally,there’stheternarywithinaternaryonline16,whichisbestleftwithoutcomment:
Everytimethestyleoftheconditionalschanges,thereaderhastopressamentalresetbuttonandstartthinkinganew.Inconsistentstylingmakescodeharderforhumanstoparse;itraisescostswithoutprovidingbenefits.
DuplicationThecodeduplicatesbothdataandlogic.Havingmultiplecopiesofthestrings"ofbeer"and"onthewall"isn’tgreat,butatleaststringduplicationiseasytoseeandunderstand.Logic,however,ishardertocomprehendthandata,andduplicatedlogicisdoublyso.Ofcourse,ifyouwanttoachievemaximumconfusion,youcaninterpolateduplicatedlogicinsidestrings,asdoestheverse methodabove.
Forexample,"bottle"pluralizationisdoneinthreeplaces.The
n==0?'Nomore':n
's'ifn!=1
n-1<0?99:n-1==0?'nomore':n-1
codetodothisisidenticalintwooftheplaces,onLines11and13:
Butlater,online16,thepluralizationlogicissubtlydifferent.Suddenlyit’snot n thatmatters,but n-1 :
Duplicationoflogicsuggeststhatthereareconceptshiddeninthecodethatarenotyetvisiblebecausetheyhaven’tbeenisolatedandnamed.Theneedtosometimessay"bottle"andothertimessay"bottles"meanssomething,andtheneedtosometimesuse n andothertimesuse n-1 meanssomethingelse.Thecodegivesnoclueaboutwhatthesemeaningsmightbe;you’relefttofigurethisoutforyourself.
NamesThemostobviouspointtobemadeaboutthenamesinthe versemethodofListing1.1:IncomprehensiblyConciseisthattherearen’tany.Theversestringcontainsembeddedlogic.Eachbitoflogicservessomepurpose,anditisuptoyoutoconstructamentalmapofwhatthesepurposesmightbe.
Thiscodewouldbeeasiertounderstandifitdidnotplacethatburdenuponyou,theintrepidreader.Thelogicthat’shiddeninsidetheversestringshouldbedispersedintomethods,andverse shouldfillitselfwithvaluesbysendingmessages.
Terminology:MethodversusMessageA"method"isdefinedonanobject,and
containsbehavior.Inthepreviousexample,
's'ifn!=1
's'ifn-1!=1
theBottles classdefinesamethodnamedsong .
A"message"issentbyanobjecttoinvokebehavior.Intheaforementionedexample,the song methodsendsthe versesmessagetotheimplicitreceiver self .
Therefore,methodsaredefined,andmessagesaresent.
Theconfusionbetweenthesetermscomesaboutbecauseitiscommonforthereceiverofamessagetodefineamethodwhosenameexactlycorrespondstothatmessage.Considertheexampleabove.The song methodsendsthe versesmessageto self ,whichresultsinaninvocationoftheverses method.Thefactthatthemessagenameandthemethodnameareidenticalmaymakeitseemasifthetermsaresynonymous.
Theyarenot.Thinkofobjectsasblackboxes.Methodsaredefinedwithinablackbox.Messagesarepassedbetweenthem.Therearemanywaysforanobjecttocheerfullyrespondtoamessageforwhichitdoesnotdefineamatchingmethod.Whileitiscommonformessagenamestomapdirectlytomethodnames,thereisnorequirementthatthisbeso.
DrawingadistinctionbetweenmessagesandmethodsimprovesyourOOmindset.Itallowsyoutoisolatetheintentionofthesenderfromtheimplementationinthereceiver.OOpromisesthatifyousendtherightmessage,thecorrectbehaviorwilloccur,regardlessofthenamesofthemethodsthateventuallygetinvoked.
Creatingamethodrequiresidentifyingthecodeyou’dliketoextractanddecidingonamethodname.This,inturn,requires
namingtheconcept,andnamingthingsisjustplainhard.Inthecaseabove,it’sespeciallyhard.Thiscodenotonlycontainsmanyhiddenconcepts,butthoseconceptsaremixedtogether,conflated,suchthattheirindividualnaturesareobscured.Combiningmanyideasintoasmallsectionofcodemakesitdifficulttoisolateandnameanysingleconcept.
Whenyoufirstwriteapieceofcode,youobviouslyknowwhatitdoes.Therefore,duringinitialdevelopment,thepriceyoupayforpoornamesisrelativelylow.However,codeisreadmanymoretimesthanitiswritten,anditsultimatecostisoftenveryhighandpaidbysomeoneelse.Writingcodeislikewritingabook;youreffortsareforotherreaders.Althoughthestruggleforgoodnamesispainful,itisworththeeffortifyouwishyoureffortstosurvivetoberead.Codeclarityisbuiltuponnames.
Problemswithconsistency,duplicationandnamingconspiretomakethecodeinListing1.1:IncomprehensiblyConciselikelytobecostly.
Notethattheaboveassertionis,atthispoint,anunsupportedopinion.Thebestwaytojudgecodewouldbetocompareitsvaluetoitscost,butunfortunatelyit’shardtogetgooddata.Judgmentsaboutcodearethereforecommonlyreducedtoindividualopinion,andhumansarenotalwaysinaccord.There’snoperfectsolutiontothisproblem,buttheJudgingCodesection,laterinthischapter,suggestswaystoacquireempiricaldataaboutthegoodnessofcode.
Independentofalljudgmentabouthowwellabitofcodeisarranged,codeisalsochargedwithdoingwhatit’ssupposedtodonowaswellasbeingeasytoaltersothatitcandomorelater.Whileit’sdifficulttogetexactfiguresforvalueandcost,askingthefollowingquestionswillgiveyouinsightintothepotentialexpenseofabitofcode:
1. Howdifficultwasittowrite?
2. Howhardisittounderstand?
3. Howexpensivewillitbetochange?
Thepast("wasit")isamemory,thefuture("willitbe")isimaginary,butthepresent("isit")istruerightnow.Theveryactoflookingatapieceofcodedeclaresthatyouwishtounderstanditatthismoment.Questions1and3abovemayormaynotconcernyou,butquestion2alwaysapplies.
Codeiseasytounderstandwhenitclearlyreflectstheproblemit’ssolvingandthusopenlyexposesthatproblem’sdomain.IfListing1.1:IncomprehensiblyConciseopenlyexposedthe99Bottlesdomain,abriefglanceatthecodewouldanswerthesequestions:
1. Howmanyversevariantsarethere?
2. Whichversesaremostalike?Inwhatway?
3. Whichversesaremostdifferent,andinwhatway?
4. Whatistheruletodeterminewhichversecomesnext?
Thesequestionsreflectcoreconceptsoftheproblem,yetnoneoftheiranswersareapparentinthissolution.Thenumberofvariants,thedifferencebetweenthevariants,andthealgorithmforloopingaredistressinglyobscure.Thiscodedoesnotreflectitsdomain,andthereforeyoucaninferthatitwasdifficulttowriteandwillbeachallengetochange.IfyouhadtocharacterizethegoalofthewriterofListing1.1:IncomprehensiblyConcise,youmightsuggestthattheirhighestprioritywasbrevity.Brevitymaybethesoulofwit,butitquicklybecomestediousincode.
Incomprehensibleconcisenessisclearlynotthebestsolutionforthe99Bottlesproblem.It’stimetoexamineonethat’smoreverbose.
1.1.2.SpeculativelyGeneralThisnextsolutionerrsinadifferentdirection.Itdoesmanythingswellbutcan’tresistindulginginunnecessarycomplexity.Havealookatthecodebelow:
Listing1.2:SpeculativelyGeneral
1 classBottles2 NoMore=lambdado|verse|3 "Nomorebottlesofbeeronthewall,"+4 "nomorebottlesofbeer.\n"+5 "Gotothestoreandbuysomemore,"+6 "99bottlesofbeeronthewall.\n"7 end8 9 LastOne=lambdado|verse|10 "1bottleofbeeronthewall,"+11 "1bottleofbeer.\n"+12 "Takeitdownandpassitaround,"+13 "nomorebottlesofbeeronthewall.\n"14 end15 16 Penultimate=lambdado|verse|17 "2bottlesofbeeronthewall,"+18 "2bottlesofbeer.\n"+19 "Takeonedownandpassitaround,"+20 "1bottleofbeeronthewall.\n"21 end
22 23 Default=lambdado|verse|24 "#{verse.number}bottlesofbeeronthewall,"+25 "#{verse.number}bottlesofbeer.\n"+26 "Takeonedownandpassitaround,"+27 "#{verse.number-1}bottlesofbeeronthewall.\n"28 end29 30 defsong31 verses(99,0)32 end33 34 defverses(finish,start)35 (finish).downto(start).map{|verse_number|36 verse(verse_number)}.join("\n")37 end38 39 defverse(number)40 verse_for(number).text41 end42 43 defverse_for(number)44 casenumber45 when0thenVerse.new(number,&NoMore)46 when1thenVerse.new(number,&LastOne)47 when2thenVerse.new(number,&Penultimate)48 elseVerse.new(number,&Default)49 end
50 end51 end52 53 classVerse54 attr_reader:number55 definitialize(number,&lyrics)56 @number=number57 @lyrics=lyrics58 end59 60 deftext61 @lyrics.callself62 end63 end
Ifyoufindthiscodelessthanclear,you’renotalone.It’sconfusingenoughtowarrantanexplanation,butbecausetheexplanationnaturallyreflectsthecode,it’sconfusinginitsownright.Don’tworryifthefollowingparagraphsmuddlethingsfurther.Theirpurposeistohelpyouappreciatethecomplexityratherthanunderstandthedetails.
Thecodeabovefirstdefinesfourlambdas(lines2,9,16,and23)andsavesthemasconstants( NoMore , LastOne , Penultimate ,andDefault ).Noticethateachlambdatakesargument verse butonlyDefault actuallyreferstoit.Thecodethendefinesthe song andverses methods.Nextcomesthe verse method,whichpassesthecurrentversenumberto verse_for andsends text totheresult(line40).Thisisthelineofcodethatreturnsthecorrectstringforaverseofthesong.
Thingsgetmoreinterestingin verse_for ,butbeforeponderingthatmethod,lookaheadtothe Verse classonline53. Verseinstancesareinitializedwithtwoarguments, number and &lyrics ,
andtheyrespondtotwomessages, number and text .The numbermethodsimplyreturnstheversenumberthatwaspassedduringinitialize.The text methodismorecomplicated;itsends call tolyrics ,passing self asanargument.
Ifyounowreturnto verse_for andexaminelines45-48,youcanseethatwheninstancesof Verse arecreated,the numberargumentisaversenumberandthe &lyrics argumentisoneofthelambdas.The verse_for methodgetsinvokedforeveryverseofthesong,andtherefore,onehundredinstancesof Verse willbecreated,eachcontainingaversenumberandthelambdathatcorrespondstothatnumber.
Tosummarize,sending verse(number) toaninstanceof Bottlesinvokes verse_for(number) ,whichusesthevalueof number toselectthecorrectlambdaonwhichtocreateandreturnaninstanceofVerse .The verse methodthensends text tothereturned Verse ,whichinturnsends call tothelambda,passing self asanargument.Thisinvokesthelambda,whichmayormaynotactuallyusetheargumentthatwaspassed.Regardless,executingthelambdareturnsastringthatcontainsthelyricsforoneverseofthesong.
Youcanbeforgivenifyoususpectthatthisisundulycomplicated.Itis.However,it’scuriousthatdespitethiscomplexity,Listing1.2:SpeculativelyGeneraldoesamuchbetterjobthanListing1.1:IncomprehensiblyConciseofansweringthedomainquestions:
1. Howmanyversevariantsarethere?Therearefourversevariants(theystartonlines2,9,16and23above).
2. Whichversesaremostalike?Inwhatway?Verses3-99aremostalike(asevidencedbythefactthatallareproducedbythe Default variant).
3. Whichversesaremostdifferent?Inwhatway?Verses0,1and2areclearlydifferentfrom3-99,althoughit’snotobviousinwhatway.
4. Whatistheruletodeterminewhichverseshouldbesungnext?Burieddeepwithinthe NoMore lambdaisahard-coded"99,"whichmightcauseonetoinferthatverse99followsverse0.
Thissolution’sanswerstothefirstthreequestionsabovearequiteanimprovementoverthoseofListing1.1:IncomprehensiblyConcise.However,allisnotperfect;itstilldoespoorlyonthevalue/costquestions:
1. Howdifficultwasittowrite?There’sfarmorecodeherethanisneededtopassthetests.Thisunnecessarycodetooktimetowrite.
2. Howhardisittounderstand?Themanylevelsofindirectionareconfusing.Theirexistenceimpliesnecessity,butyoucouldstudythiscodeforalongtimewithoutdiscerningwhytheyareneeded.
3. Howexpensivewillitbetochange?Themerefactthatindirectionexistssuggeststhatit’simportant.Youmayfeelcompelledtounderstanditspurposebeforemakingchanges.
Asyoucanseefromtheseanswers,thissolutiondoesagoodjobofexposingcoreconcepts,butdoesabadjobofbeingworthitscost.Thisgoodjob/badjobdividereflectsafundamentalfissureinthecode.
Asidefromthe song and verses methods,thecodedoestwobasicthings.First,itdefinestemplatesforeachkindofverse(lines2-28),andsecond,itchoosestheappropriatetemplateforaspecificversenumberandrendersthatverse’slyrics(lines39-63).
Noticethattheversetemplatescontainalloftheinformationneededtoanswerthedomainquestions.Therearefourtemplates,andtherefore,theremustbefourversevariants.TheDefault templatehandlesverses3through99,andtheseversesareclearlymostalike.Verses0,1,and2havetheirownspecialtemplates,soeachmustbeunique.Thefourtemplates(ifyouignorethefactthatthey’restoredinlambdas)areverystraightforward,whichmakesansweringthedomainquestionseasy.
Butit’snotthetemplatesthatarecostly;it’sthecodethatchoosesatemplateandrendersthelyricsforaverse.Thischoosing/renderingcodeisoverlycomplicated,andwhilecomplexityisnotforbidden,itisrequiredtopayitsownway.Inthiscase,complexitydoesnot.
Insteadof1)definingalambdatoholdatemplate,2)creatinganewobjecttoholdthelambda,and3)invokingthelambdawithself asanargument,thecodecouldmerelyhaveputeachofthefourtemplatesintoamethodandthenusedthecasestatementonlines45-48toinvokethecorrectone.Thelambdasaren’tneeded,noristhe Verse class,andtheroutebetweenthemisaseriesofpointlessjumpsthroughneedlesshoops.
Giventheobvioussuperiorityofthisalternativeimplementation,howonearthdidthe"callingalambda"variantcomeabout?Atthisremove,it’sdifficulttobecertainofthemotivation,butthecodegivestheimpressionthatitsauthorfearedthatthelogicforselectingorinvokingatemplatewouldsomedayneedtochange,andsoaddedlevelsofindirectioninamisguidedattempttoprotectagainstthatday.
Theydidnotsucceed.Relativetothealternative,Listing1.2:SpeculativelyGeneralishardertounderstandwithoutbeingeasiertochange.Theadditionalcomplexitydoesnotpayoff.Theauthormayhaveactedwiththebestofintentions,butsomewherealongtheway,theircommitmenttotheplan
overcamegoodsense.
Programmersloveclevercode.It’slikeaneatcardtrickthatusessleightofhandandmisdirectiontomakemagic.Writingit,orsuddenlyunderstandingit,suppliesalittleburstofappreciativepleasure.However,thisverypleasuredistractstheeyeandseducesthemind,andallowsclevernesstowormitswayintoinappropriateplaces.
Youmustresistbeingcleverforitsownsake.IfyouarecapableofconceivingandimplementingasolutionascomplexasListing1.2:SpeculativelyGeneral,itisincumbentuponyoutoacceptthehardertaskandwritesimplercode.
NeitherListing1.2:SpeculativelyGeneralnorListing1.1:IncomprehensiblyConciseisthebestsolutionfor99Bottles.Perhaps,aswastrueforporridge,thethirdsolutionwillbejustright.[2]
1.1.3.ConcretelyAbstractThissolutionvaliantlyattemptstonametheconceptsinthedomain.Here’sthecode:
Listing1.3:ConcretelyAbstract
1 classBottles2 3 defsong4 verses(99,0)5 end6 7 defverses(bottles_at_start,bottles_at_end)8 bottles_at_start.downto(bottles_at_end).mapdo
|bottles|9 verse(bottles)10 end.join("\n")11 end12 13 defverse(bottles)14 Round.new(bottles).to_s15 end16 end17 18 classRound19 attr_reader:bottles20 definitialize(bottles)21 @bottles=bottles22 end23 24 defto_s25 challenge+response26 end27 28 defchallenge29 bottles_of_beer.capitalize+""+on_wall+","+30 bottles_of_beer+".\n"31 end32 33 defresponse34 go_to_the_store_or_take_one_down+","+35 bottles_of_beer+""+on_wall+".\n"36 end37
38 defbottles_of_beer39 "#{anglicized_bottle_count}#{pluralized_bottle_form}of#{beer}"40 end41 42 defbeer43 "beer"44 end45 46 defon_wall47 "onthewall"48 end49 50 defpluralized_bottle_form51 last_beer??"bottle":"bottles"52 end53 54 defanglicized_bottle_count55 all_out??"nomore":bottles.to_s56 end57 58 defgo_to_the_store_or_take_one_down59 ifall_out?60 @bottles=9961 buy_new_beer62 else63 lyrics=drink_beer64 @bottles-=165 lyrics66 end67 end
68 69 defbuy_new_beer70 "Gotothestoreandbuysomemore"71 end72 73 defdrink_beer74 "Take#{it_or_one}downandpassitaround"75 end76 77 defit_or_one78 last_beer??"it":"one"79 end80 81 defall_out?82 bottles.zero?83 end84 85 deflast_beer?86 bottles==187 end88 end
Thissolutionischaracterizedbyhavingmanysmallmethods.Thisisnormallyagoodthing,butsomehowinthiscaseit’sgonebadlywrong.Havealookathowthissolutiondoesonthedomainquestions:
1. Howmanyversevariantsarethere?It’salmostimpossibletotell.
2. Whichversesaremostalike?Inwhatway?
Ditto.
3. Whichversesaremostdifferent?Inwhatway?Ditto.
4. Whatistheruletodeterminewhichverseshouldbesungnext?Ditto.
Itfaresnobetteronthevalue/costquestions.
1. Howdifficultwasittowrite?Difficult.Thisclearlytookafairamountofthoughtandtime.
2. Howhardisittounderstand?Theindividualmethodsareeasytounderstand,butdespitethis,it’stoughtogetasenseoftheentiresong.Thepartsdon’tseemtoadduptothewhole.
3. Howexpensivewillitbetochange?Whilechangingthecodeinsideanyindividualmethodischeap,inmanycases,onesimplechangewillcascadeandforcemanyotherchanges.
It’sobviousthattheauthorofthiscodewascommittedtodoingtherightthing,andthattheycarefullyfollowedtheRed,Green,Refactorstyleofwritingcode.Thevariousstringsthatmakeupthesongareneverrepeated—itlooksasthoughthesestringswererefactoredintoseparatemethodsatthefirstsignofduplication.
ThecodeisDRY,andDRYingoutcodeshouldsaveyoumoney.DRYpromisesthatifyouputachunkofcodeintoamethodandtheninvokethatmethodinsteadofduplicatingthecode,youwillsavemoneylaterifthebehaviorofthatchunkchanges.Whenyouinvokeamethodinsteadofimplementingbehavior,youaddalevelofindirection.Thisindirectionmakesthedetailsofwhat’shappeninghardertounderstand,butDRYpromisesthatin
return,yourcodewillbeeasiertochange.
TheDon’tRepeatYourselfprinciple,likeallprinciplesofobject-orienteddesign,iscompletelytrue.However,despitethatfactthatthecodeaboveisDRY,therearemanywaysinwhichit’sexpensivetochange.
Oneofmanypossibleexamplesisthe beer methodonline42.Thismethodreturnsthestring"beer,"whichoccursnowhereelseinthecode.Tochangethedrinkto"Kool-Aid,"youneedonlychangeline43toreturn"Kool-Aid"insteadof"beer."Asthisonesmallchangeisallthat’sneededtomeetthe"Kool-Aid"requirement,onthesurface,DRYhasfulfilleditspromise.However,stepbackaminuteandconsidertheresultingmethod:
Orpondersomeoftheothermethodnames:
Inlightofthe"Kool-Aid"change,thesenamesareterriblyconfusing.Thesemethodnamesnolongermakesensewheretheyaredefined,andtheyaretotallymisleadinginplaceswheretheyareused.Tomitigatethisconfusion,younotonlyhavetochange"beer"to"Kool-Aid"insidethismethod,butyoualsohavetomakethesamechangetoeverymethodnamethatincludestheword"beer"andthenagaintoeverysenderofoneofthosemessages.
Thissmallchangeinrequirementsforcesachangeinmanyplaces,whichisexactlytheproblemDRYpromisestoavoid.The
defbeer"Kool-Aid"end
defbottles_of_beerdefbuy_new_beerdefdrink_beerdeflast_beer?
faulthere,however,liesnotwiththeDRYprinciple,butwiththenamesofthemethods.
Whenyouchoose beer asthenameofamethodthatreturnsthestring"beer,"you’venamedthemethodafterwhatitdoesrightnow.Unfortunately,whenyounameamethodafteritscurrentimplementation,youcanneverchangethatinternalimplementationwithoutruiningthemethodname.
Youshouldnamemethodsnotafterwhattheydo,butafterwhattheymean,whattheyrepresentinthecontextofyourdomain.Ifyouweretoaskyourcustomerwhat"beer"isinthecontextofthe99Bottlessong,theywouldnotanswer"Beeristhebeer,"theywouldsaysomethinglike"Beeristhethingyoudrink"or"Beeristhebeverage."
"Beer"and"Kool-Aid"arekindsofbeverages;theword"beverage"isonelevelofabstractionhigherthan"beer."Namingthemethodatthisslightlyhigherlevelofabstractionisolatesthecodefromchangesintheimplementationdetails.Ifyouchoosebeverage forthemethodname,goingfrom:
to:
makesperfectsenseandrequiresnootherchange.
Listing1.3:ConcretelyAbstractcontainsmanysmallmethods,andthestringsthatmakeupthesongarecompletelyDRY.Thesetwothingsexertaforceforgoodthatshouldresultincodethat’s
defbeverage"beer"end
defbeverage"Kool-Aid"end
easytochange.However,inConcretelyAbstract,thisforceisovercomebythehighcostofdealingwithmethodsthatarenamedatthewronglevelofabstraction.Thesemethodnamesraisethecostofchange.
Therefore,onelessontobegleanedfromthissolutionisthatyoushouldnamemethodsaftertheconcepttheyrepresentratherthanhowtheycurrentlybehave.However,noticethatevenifyoueditedthecodetoimproveeverymethodname,thiscodestillisn’tquiteright.
Changingthenameofthe beer methodto beverage makesiteasytoreplacethestring"beer"withthestring"Kool-Aid"butdoesnothingtoimprovethiscode’sscoreonthedomainquestions.Theproblemgoesfardeeperthanhavingmethodsthatarenamedatthewronglevelofabstraction.It’snotjustthenamesthatarewrong,butthemethodsthemselves.Manymethodsinthiscoderepresentthewrongabstractions.
Theproblemofidentifyingtherightabstractionsisexploredinfuturechapters,butmeanwhileit’stimetoconsideronemoresolution.
1.1.4.ShamelessGreenNoneofthesolutionsshownthusfardoverywellonthevalue/costquestions.IncomprehensiblyConcisecaresonlyforterseness.SpeculativelyGeneraltriesforextensibilitybutachievesunwarrantedcomplexity.ConcretelyAbstract'sheartisintherightplacebutcan’tgetitsfeetoutofthemud.
Solvingthe99Bottlesprobleminanyofthesewaysrequiresmoreeffortthanisnecessaryandresultsinmorecomplexitythanisneeded.Thesesolutionscosttoomuch;theydotoomanyofthewrongthingsandtoofewoftheright.
SpeculativelyGeneralandConcretelyAbstractwerebothwritten
withaneyetowardreducingfuturecosts,anditisdistressingtoseegoodintentionsfailsospectacularly.It’saparticularshamethattheabstractionsarewrongbecause,giventheopportunitytodoso,thecodeiscompletelywillingtorevealabstractionsthatareright.Thefailurehereisnotbadintention—it’sinsufficientpatience.
Thisnextexampleispatientandsoprovidesanantidoteforallthathascomebefore.ThefollowingsolutionisknownasShamelessGreen:
Listing1.4:ShamelessGreen
1 classBottles2 3 defsong4 verses(99,0)5 end6 7 defverses(starting,ending)8 starting.downto(ending).map{|i|verse(i)}.join("\n")9 end10 11 defverse(number)12 casenumber13 when014 "Nomorebottlesofbeeronthewall,"+15 "nomorebottlesofbeer.\n"+16 "Gotothestoreandbuysomemore,"+17 "99bottlesofbeeronthewall.\n"18 when1
19 "1bottleofbeeronthewall,"+20 "1bottleofbeer.\n"+21 "Takeitdownandpassitaround,"+22 "nomorebottlesofbeeronthewall.\n"23 when224 "2bottlesofbeeronthewall,"+25 "2bottlesofbeer.\n"+26 "Takeonedownandpassitaround,"+27 "1bottleofbeeronthewall.\n"28 else29 "#{number}bottlesofbeeronthewall,"+30 "#{number}bottlesofbeer.\n"+31 "Takeonedownandpassitaround,"+32 "#{number-1}bottlesofbeeronthewall.\n"33 end34 end35 36 end
Themostimmediatelyapparentqualityofthiscodeishowverysimpleitis.There’snothingtrickyhere.Thecodeisgratifyinglyeasytocomprehend.Notonlythat,despiteitslackofcomplexitythissolutiondoesextremelywellonthedomainquestions.
1. Howmanyversevariantsarethere?Clearly,four.
2. Whichversesaremostalike?Inwhatway?3-99,whereonlytheversenumbervaries.
3. Whichversesaremostdifferent?Inwhatway?
0,1and2aredifferentfrom3-99,thoughfiguringouthowrequiresparsingstringswithyoureyes.
4. Whatistheruletodeterminewhichverseshouldbesungnext?Thisisstillnotexplicit.The0versecontainsadeeplyburied,hard-coded99.
TheseanswersareidenticaltothoseachievedbyListing1.2:SpeculativelyGeneral.ShamelessGreenandSpeculativelyGeneraldiffer,though,inhowtheycompareonthevalue/costquestions.ShamelessGreenisasubstantialimprovement.
1. Howdifficultwasthistowrite?Itwaseasytowrite.
2. Howhardisittounderstand?Itiseasytounderstand.
3. Howexpensivewillitbetochange?Itwillbecheaptochange.Eventhoughtheversestringsareduplicated,ifonechangesit’seasytokeeptheothersinsync.
Bythecriteriathathavebeenestablished,ShamelessGreenisclearlythebestsolution,yetalmostnoonewritesit.Itfeelsembarrassinglyeasy,andismissingmanyqualitiesthatyouexpectingoodcode.Itduplicatesstringsandcontainsfewnamedabstractions.
Mostprogrammershaveapowerfulurgetodomore,butsometimesit’sbesttostoprighthere.Ifyouwerechargedwithwritingthecodetoproducethelyricstothe99Bottlessong,itisdifficulttoimaginefulfillingthatrequirementinamorecost-effectiveway.
TheShamelessGreensolutionisdisturbingbecause,althoughthecodeiseasytounderstand,itmakesnoprovisionforchange.Inthisparticularcase,thesongissounlikelytochangethatbetting
thatthecodeis"goodenough"shouldpayoff.However,ifyoupretendthatthisproblemisaproxyforareal,productionapplication,thepropercourseofactionisnotsoclear.
WhenyouDRYoutduplicationorcreateamethodtonameabitofcode,youaddlevelsofindirectionthatmakeitmoreabstract.Intheorytheseabstractionsmakecodeeasiertounderstandandchange,butinpracticetheyoftenachievetheopposite.Oneofthebiggestchallengesofdesignisknowingwhentostop,anddecidingwellrequiresmakingjudgmentsaboutcode.
1.2.JudgingCodeYounowhaveaccesstofivedifferentsolutionstothe99BottlesofBeerproblem;thefourlistedintheprecedingsectionandtheoneyouwroteyourself.
Whichisbest?
Youlikelyhaveanopiniononthisquestion—onewhich,granted,mayhavebeenswayedbythecommentaryabove.However,independentofthatgentleinfluence,thesumofyourexperiencesandexpectationspredisposeyoutoassessthegoodnessofcodeinyourownuniqueway.
Youjudgecodeconstantly.Writingcoderequiresmakingchoices;thechoicesyoumakereflectpersonal,internalizedcriteria.Youintendtowrite"good"codeandif,inyourestimation,you’vewritten"bad"code,youareclearlyawarethatyou’vedoneso.Regardlessofhowimplicit,unachievable,orunhelpfultheymaybe,youalreadyhaverulesaboutcode.
Whilehavingstandardsofanysortisavirtue,thechanceofachievingyourstandardsisimprovediftheyareexplicitandquantifiable.Answeringthequestion"Whatmakescodegood?"thusrequiresdefininggoodnessinconcreteandactionableways.
“
“
“
Thisisharderthanonemightthink.
1.2.1.EvaluatingCodeBasedonOpinionYou’dthinkthatbynow,therewouldexistauniversallyagreedupondefinitionofgoodcodethatcouldunambiguouslyguideourprogrammingbehavior.Theunfortunatetruthisthat,notonlyarethereamultitudeofdefinitions,butthatthesedefinitionsgenerallydescribehowcodelookswhenit’sdonewithoutprovidinganyconcreteguidanceabouthowtogetthere.
Justas"Everybodycomplainsabouttheweatherbutnobodydoesanythingaboutit",[3]everyonehasanopinionaboutwhatgoodcodelookslike,butthoseopinionsusuallydon’ttelluswhatactiontotaketocreateit.Robert"UncleBob"MartinopenshisbookCleanCodebyaskinganumberofluminariesforadefinitionofcleancode.Theirthoughtfulanswerscoulddescribeartorwineaseasilyassoftware.
Ilikemycodetobeelegantandefficient.—BjarneStroustrup
inventorofC++
Cleancodeis…fullofcrispabstractions…—GradyBooch
authorofObjectOrientedAnalysisandDesignwithApplications
Cleancodewaswrittenbysomeonewhocares.—MichaelFeathers
authorofWorkingEffectivelywithLegacyCode
Yourowndefinitionprobablyfollowsalongthesesamelines.Anypileofcodecanbemadetowork;goodcodenotonlyworks,butisalsosimple,understandable,expressiveandchangeable.
Theproblemwiththesedefinitionsisthatalthoughthey
accuratelydescribehowgoodcodelooksonceit’swritten,theygivenohelpwithachievingthisstate,andprovidelittleguidanceforchoosingbetweencompetingsolutions.Theattributestheyusetodescribegoodcodearequalitative,notquantitative.
Whatdoesitmeantobe"elegant?"Whatmakesanabstraction"crisp?"Despitethefactthatthesedefinitionsareundeniablycorrect,nonearepreciseinameasurableway.Thislackofprecisionmeansthatwell-meaningprogrammerscanholdidenticallyhighstandardsandstillhavesignificantdisagreementsaboutrelativegoodness.Thus,wearguefruitlesslyaboutcode.
Sinceformfollowsfunction,goodcodecanalsobedefinedsimply,andsomewhatcircularly,asthatwhichprovidesthehighestvalueforthelowestcost.Oursenseofelegance,expressivenessandsimplicityisanoutgrowthofourexperienceswhenreadingandmodifyingcode.Codethatiseasytounderstandandapleasuretoextendnaturallyfeelssimpleandelegant.
Ifyoucouldidentifyandmeasurethesequalities,youcouldseekafterthemdiligentlyanddeliberately.Therefore,althoughyouropinionsaboutcodematter,youwouldbewellservedbyfacts.
1.2.2.EvaluatingCodeBasedonFactsA"metric"isameasureofsomequalityofcode.Metricsare,obviously,createdbypeople,soonecouldarguethattheymerelyexpressoneindividual’sopinion.Thatassertion,however,vastlyunderstatestheirworth.Measuresthatrisetobecomemetricsarebackedbyresearchthathasstoodthetestoftime.They’vebeenscrutinizedbymanypeopleovermanyyears.Youcanthinkofmetricsascrowd-sourcedopinionsaboutthequalityofcode.
Ifyouapplythesamemetrictotwodifferentpiecesofsourcecode,youcanthencomparethatcode(atleastintermsofwhat
themetricmeasures)bycomparingtheresultingnumbers.Whileit’spossibletodisagreewiththepremiseofaspecificmetric,andtoinsistthatthethingitmeasuresisn’tuseful,therulesofmathematicsrequirealltoconcedethatthenumbersproducedbymetricsarefacts.
Itwouldbeextremelyhandytohaveagreed-uponfactswithwhichtocomparecode.Insearchofthesefacts,thissectionexaminesthreedifferentmetrics:SourceLinesofCode,CyclomaticComplexity,andABC.
SourceLinesofCodeInthedaysofyore,thedesireforreproducible,reliableinformationaboutthecostofdevelopingapplicationsledtothecreationofametricknownsimplyasSourceLinesofCode(SLOC,sometimesshortenedtojustLOC).Thisonenumberhasbeenusedtopredictthetotaleffortneededtodevelopsoftware,tomeasuretheproductivityofthosewhowriteit,andtopredictthecostofmaintainingit.
Themetrichastheadvantageofbeingeasilygarneredandreproduced,butsuffersfrommanyflaws.
UsingSLOCtopredictthedevelopmenteffortneededforanewprojectisdonebycountingtheSLOCofexistingprojectsforwhichtotaleffortisknown,decidingwhichofthoseexistingprojectsthenewprojectmostresembles,andthenrunningacostestimationmodeltomaketheprediction.Ifthepersondoingtheestimatingiscorrectaboutwhichexistingproject(s)thenewprojectmostcloselyresembles,thispredictionmaybeaccurate.
Measuringprogrammerproductivitybycountinglinesofcodeassumesthatallprogrammerswriteequallyefficientcode.However,noviceprogrammersareoftenfarmoreverbosethanthosewithmoreexperience.Despitethefactthatnoviceswritemorecodetoproducelessfunction,bythismetric,theycanseem
“
moreproductive.
Whilethecostofmaintenanceisrelatedtothesizeofanapplication,thewayinwhichcodeisorganizedalsomatters.Itischeapertomaintainawell-designedapplicationthanitistomaintainapileofspaghetti-code.
SLOCnumbersreflectcodevolume,andwhileit’susefulforsomepurposes,knowingSLOCaloneisnotenoughtopredictcodequality.
CyclomaticComplexityIn1976,ThomasJ.McCabe,Sr.publishedAComplexityMeasure,inwhichheasserted:
What is needed is a mathematical technique that willprovideaquantitativebasisformodularizationandallowustoidentifysoftwaremodulesthatwillbedifficulttotestormaintain.
A"mathematicaltechnique"toidentifycodethatis"difficulttotestormaintain"--thiscouldbetheperfecttoolforassessingcode.Inhispaper,McCabedescribeshisCyclomaticComplexitymetric,analgorithmthatcountsthenumberofuniqueexecutionpathsthroughabodyofsourcecode.Thinkofthisalgorithmasalittlemachinethatpondersyourcodeandthenmapsoutallthepossibleroutesthrougheverycombinationofeverybranchofeveryconditional.Amethodwithmanydeeplynestedconditionalswouldscoreveryhigh,whileamethodwithnoconditionalsatallwouldscore0.
Cyclomaticcomplexitydoesnotpredictapplicationdevelopmenttimenordoesitmeasureprogrammerproductivity.Itsdesiretoidentifycodethatisdifficulttotestormaintainaimsitdirectlyatcodequality.
Cyclomaticcomplexitycanbeusedinseveralways.First,youcanuseittocomparecode.Ifyouhavetwovariantsofthesamemethod,youcanchoosebetweenthembasedontheircyclomaticcomplexity.Lowerscoresarebetterandsobyextensionthecodewiththelowestscoreisthebest.
Next,youcanuseittolimitoverallcomplexity.Youcansetstandardsforhowhighascoreyou’rewillingtoaccept,andrequireexplicitdispensationbeforeallowingcodetoexceedthismaximum.
Finally,youcanuseittodetermineifyou’vewrittenenoughtests.Cyclomaticcomplexitytellsyoutheminimumnumberoftestsneededtocoverallofthelogicinthecode.Ifyouhavefewerteststhancyclomaticcomplexityrecommends,youdon’thavecompletetestcoverage.
Cyclomaticcomplexitysoundsgreat,andit’seasytoseethatitcouldbeuseful,butitviewstheworldofcodethroughanarrowlens.
Assignments,BranchesandConditions(ABC)MetricTheproblemwithcyclomaticcomplexityisthatitdoesn’ttakeeverythingintoaccount.Codedoesmorethanjustevaluateconditions;italsoassignsvaluestovariablesandsendsmessages.Thesethingsaddup,andasyoudomoreandmoreofeach,yourcodebecomesincreasinglydifficulttounderstand.
In1997,twenty-oneyearsaftertheunveilingofcyclomaticcomplexity,JerryFitzpatrickpublishedApplyingtheABCMetrictoC,C++,andJava,inwhichhedescribesametricthatdoesconsidermorethanconditionals.HisABCstandsforassignments,branchesandconditions,where:
Assignmentsisacountofvariableassignments.
Branchescountsnotbranchesofanifstatement(asonecouldforgivablyinfer)butbranchesofcontrol,i.e.,itcountsfunctioncallsormessagesends.
Conditionscountsconditionallogic.
FitzpatrickdescribestheABCmetricasameasureofsize,asifABCisamoresophisticatedversionofSLOC.Thisishismetricsohecertainlygetstosaywhatitrepresents,butyouwillnotgowrongifyouthinkofABCscoresasreflectingcognitiveasopposedtophysicalsize.HighABCnumbersindicatecodethattakesupalotofmentalspace.Inthissense,ABCisameasureofcomplexity.Highlycomplexcodeisdifficulttounderstandandchange,thereforeABCscoresareaproxyforcodequality.
ThemostpopulartoolforgeneratingABCscoresforRubycodeisRyanDavis’sFlog.FlogismoreABC-ishthanstrictlyABC.DavishasspecificallytunedittoreflecthisconsideredopinionaboutwhatmakesforgoodRubycode.Ifyou’reinterestedinthewaysinwhichFlogdiffersfromclassicABC,youcanfindoutbysimplybrowsingthesourcecode,butyoudon’thavetodelveintothegorydetailstobenefitfromrunningthismetricagainstyourowncode.
Flogscoresprovideanindependentperspectivethatmaychallengeyourideasaboutcomplexityanddesign.Highscoressuggestthatcodewillbehardtotestandexpensivetomaintain.IfyoubelieveyourcodetobesimplebutFlogsaysotherwise,youshouldthinkagain.
EveryexampleinthisbookwilleventuallyberunthroughFlogandtherelativescoreswillbecomparedanddiscussed.AlthoughFlogscoresaren’teverything,theyareverydefinitelyausefulsomething.
Metricsarefalliblebuthumanopinionisnomoreprecise.Checkingmetricsregularlywillkeepyouhumbleandimprove
yourcode.
1.2.3.ComparingSolutionsNowthatyouhavesomeinsightintocodemetrics,it’stimetoexaminesomescoresforthecodeexamplesshowninthischapter.
Thefollowingtableshowseachsolution’stotallinesofcode(SLOC),totalFlogscore,andworstscoring"bit."
Table1.1:FlogScores
Solution SLOC FlogTotal
FlogWorstBit
Listing1.1:IncomprehensiblyConcise
19 42.5 #verse 36.2
Listing1.2:SpeculativelyGeneral
63 50.6 lambdas 26.5
Listing1.3:ConcretelyAbstract
92 61.9 #challenge 14.4
Listing1.4:ShamelessGreen
34 25.6 #verse 19.3
Inmostcases,theworstscoringbitisamethod,butinthecaseofListing1.2:SpeculativelyGeneral,theworstscoreisearnedbythegroupoflambdasthataredefinedasconstants.
Thefollowingchartmakesthenumberseasiertocompare.AlthoughSLOCisnotrelatedtoFlogscore,thevaluesarein
similarrangessoit’sconvenienttodisplayeverythingonthesamechart.
Figure1.1:FlogScoreChart
Thisgraphexposesanumberofinterestingpatterns.
First,itisunsurprisingthatsolutionswithmorelinesofcodetendtoFlogtohigherscores,i.e.,thattotalFlogscoregenerallyrisesintandemwithSLOC.ShamelessGreenisthenotableexception—itissecondlowestinSLOCbutlowestintotalFlogscorebyaconsiderablemargin.
Next,ConcretelyAbstractscoresattheextremeofeverydimension.Itcontainsthemostcode,Flogstothehighesttotal,andhasthebest,ifyouwill,"WorstBit"Flogscore.ThetotalFlogscoreisreasonableinlightofthetotallinesofcode,andthelowWorstBitscoreindicatesthatthemethodsaresmallandfocused.
ThesemetricssuggestthatConcretelyAbstractcontainsgood
code,butasyoumayrecall,itdoesnot.Metricsclearlydon’ttellthewholestory.Theproblemhereisthatalthoughthecodeisnicelyarranged,itcontainsnamesthatareatthewronglevelofabstraction.ThesenamesmakeConcretelyAbstractexpensivetochangedespiteitsorderlyarrangement.ThemetricsoverstatethequalityofConcretelyAbstractbecausetheyapproveofthecodestructure,buttheyareunabletorecognizethepoornames.
SpeculativelyGeneralisthesecondlongestsolution,andhasthesecondhighestFlogandWorstBitscores.Itslengthandcomplexityreflectanattempttoarrangethecodesuchthatcertainimaginedchangeswillbeeasy,i.e.toguessthefuture.Theseguessesareunlikelytopayoffandrelativetotheothersolutions,SpeculativelyGeneralisbothlongerandmorecomplexthannecessary.
Thefinaltwoexamples,IncomprehensiblyConciseandShamelessGreen,aresimilarinthatmostoftheircomplexityiscontainedinasinglemethod.Ineachcase,thescoreoftheirworstbitisveryneartotheirtotalscore.Thisreflectsthefactthatbotharebasicallyproceduresandthatneitherhasattemptedtoidentifyabstractions.
Despitethissimilarity,ifyoucomparetheirSLOCscorestotheirtotalFlogscores,you’llseethattheyarealsoverydifferent.IncomprehensiblyConcisehasahighFlogscorerelativetoSLOC,ShamelessGreenhastheopposite.IncomprehensiblyConcisepacksalotofcomplexityintoafewlinesofcode.ShamelessGreenisbiasedintheotherdirection;ithasmorecodebutismuchsimpler.
Overall,ShamelessGreenhasthelowesttotalFlogscore,thesecondlowestSLOC,andthesecondlowestWorstBitscore.Ifyourgoalistowritestraightforwardcode,thesemetricspointyoutowardShamelessGreen.
1.3.SummaryAsprogrammersgrow,theygetbetteratsolvingchallengingproblems,andbecomecomfortablewithcomplexity.Thishigherlevelofcomfortsometimesleadstothebeliefthatcomplexityisinevitable,asifit’sthenatural,inescapablestateofallfinishedcode.However,there’ssomethingbeyondcomplexity—ahigherlevelofsimplicity.Infinitelyexperiencedprogrammersdonotwriteinfinitelycomplexcode;theywritecodethat’sblindinglysimple.
Thischapterexaminedfourpossiblesolutionstothe99Bottlesproblemasapreludetodefiningwhatitmeanstowritesimplecode.Itusedmetricsasastartingpoint,injectedabitofcommonsense,andlandedonShamelessGreen.
ShamelessGreenisdefinedasthesolutionwhichquicklyreachesgreenwhileprioritizingunderstandabilityoverchangeability.Itusesteststodrivecomprehension,andpatientlyaccumulatesconcreteexampleswhileawaitinginsightintounderlyingabstractions.Itdoesn’tdisputethatDRYisgood,ratheritbelievesthatitischeapertomanagetemporaryduplicationthantorecoverfromincorrectabstractions.
WritingShamelessGreenisfast,andtheresultingcodemightbe"goodenough."Mostprogrammersfinditembarrassinglyduplicative,andthecodeiscertainlynotveryobject-oriented.However,ifnothingeverchanges,themostcost-effectivestrategyistodeploythiscodeandwalkaway.
Thechallengecomeswhenachangerequestarrives.Codethat’sgoodenoughwhennothingeverchangesmaywellbecodethat’snotgoodenoughwhenthingsdo.Chapter3introducesjustsuchachange,andinthatchapteryou’llbeginimprovingthecode.Beforemovingon,however,it’stimetotakeastepback,andlearnhowtotest-driveShamelessGreen.
2.TestDrivingShamelessGreenThepreviouschapterexaminedfoursolutionstothe99Bottlesproblem,andassertedthattheoneknownasShamelessGreenisbest.TheShamelessGreensolutionconsistsofintention-revealing,workingsoftware,andistheresultofwritingsimplecodetopassaseriesofpre-suppliedtests.
TheprovenanceofthecodethatwaswritteninChapter1isobvious,butthetestsappearedwithoutexplanation.Itisnowtimetotakeastepback,andinvestigatehowtocreateteststhatleadtoShamelessGreen.
2.1.UnderstandingTestingAgenerationago,ahandfulofextremeprogramming(XP)practitionersbeganwritingautomatedtestsusingatechniquetheycalled"testfirstdevelopment."Theirideasweresoinfluentialthatautomatedtestsarenowthenorm,andthesetestsareoftenwrittenfirst,inpreludetowritingcode.
Thepracticeofwritingtestsbeforewritingcodebecomeknownastest-drivendevelopment(TDD).Initssimplestform,TDDworkslikethis:
1. Writeatest.Becausethecodedoesnotyetexist,thistestfails.Testrunnersusuallydisplayfailingtestsinred.
2. Makeitrun.Writethecodetomakethetestpass.Testrunnerscommonlydisplaypassingtestsingreen.
3. Makeitright.Eachtimeyoureturntogreen,youcanrefactoranycodeinto
“
abettershape,confidentthatitremainscorrectifthetestscontinuetopass.
InTest-DrivenDevelopmentbyExample,KentBeckdescribesthisastheRed/Green/Refactorcycleandcallsit"theTDDmantra."
Theideasoftesting,andoftestingfirst,havewontheheartsandmindsofprogrammers.However,acommitmenttowritingtestsdoesn’tmakethiseasy.TDDpresentsanever-endingchallenge.Youmustrepeatedlydecidewhichtesttowritenext,howtoarrangecodesothatthetestpasses,andhowmuchrefactoringtodoonceitdoes.Eachdecisionrequiresjudgmentandhasconsequences.
IfyourTDDjudgmentisnotyetfullydeveloped,it’sreasonabletotemporarilyadoptthatofamaster.Here’sanexcellentguidingprinciple:
Quickgreenexcusesallsins.—KentBeck
Test-DrivenDevelopmentbyExample
Greenmeanssafety.Greenindicatesthat,atleastasevidencedbythetestsathand,youunderstandtheproblem.Greenisthewallatyourbackthatletsyoumoveforwardwithconfidence.Gettingtogreenquicklysimplifiesallthatfollows.
Thischapterillustrateshowtoincrementallycreatetestsandthenusetheseteststodrivethedevelopmentofcode.TheexamplesobedientlyfollowtheRed/Green/Refactorcycle,butarefairlyconservative.Becausetheinitialgoalismoreaboutreachinggreenthanwritingperfectcode,therefactoringstepsometimesremovesduplicationandothertimesretainsit.
Theplanistocreateteststhatthoroughlydescribethe99Bottlesproblem,andthentosolvetheproblemwiththeimplementation
knownasShamelessGreen.TheShamelessGreensolutionstrivesformaximumunderstandabilitybutisgenerallyunconcernedwithchangeability.ShamelessGreendoesnotassertthatchangeabilityisn’timportant;itmerelyrecognizesthatgettingtogreenquicklyisoftenatoddswithwritingperfectlychangeablecode.Thischapterconcentratesoncreatingthetestsandwritingsimplecodetopassthem.Futurechaptersrefactortheresultingcodetoimprovethedesign.
2.2.WritingtheFirstTestThefirsttestisoftenthemostdifficulttowrite.Atthispoint,youknowtheleastaboutwhateveritisyouintendtodo.Yourproblemisabig,fuzzy,amorphousblob,andit’schallengingtoreachinandcarveoffasinglepiece.Itfeelsimportanttochoosewell,becausewhereyoustartinformshowyou’llproceed,andultimatelydetermineswhereyou’llend.Thefirsttestcanthereforeseemfraughtwithperil.
Despiteitsapparentimport,thechoiceyoumakeherehardlymatters.Inthebeginning,youhaveideasabouttheproblembutactuallyknowverylittle.Yourideasmayturnouttobecorrect,butit’sjustaspossiblethattimewillprovethemwrong.Youcan’tfigureoutwhat’srightuntilyouwritesometests,atwhichtimeyoumayrealizethatthebestcourseofactionistothroweverythingawayandstartover.Therefore,thepurposeofsomeofyourtestsmightverywellbetoprovethattheyrepresentbadideas.Learningwhichideaswon’tworkisforwardprogress,howeverdisappointingitmaybeinthemoment.
So,whileitisimportanttoconsidertheproblemandtosketchoutanoverallplanbeforewritingthefirsttest,don’toverthinkit.Findastartingplaceandgetgoing,infaiththatasyouproceed,thefogwillclear.
IfyouweretosketchoutapublicApplicationProgrammingInterface(API)for99Bottles,itmightlooklikethis:
verse(n)Returnthelyricsfortheversenumbern
verses(a,b)Returnthelyricsforversesnumberedathroughb
songReturnthelyricsfortheentiresong
ThisAPIallowsotherstorequestasingleverse,arangeofverses,ortheentiresong.
NowthatyouhaveaplanfortheAPI,thereareanumberofpossibilitiesforthefirsttest.Youcouldwriteatestfortheentiresong,foraseriesofcontiguousverses,orforanysingleverse.Becausetheeasiestwaytogetstartedistotacklesomethingthatyouthoroughlyunderstand,itmakessensetobeginbytestingasingleverse,andthemostlogicalfirstversetotestisthefirstversetobesung.Here’sthattest,writteninMinitest:
Listing2.1:Verse99Test
1 classBottlesTest<Minitest::Test2 deftest_the_first_verse3 expected="99bottlesofbeeronthewall,"+4 "99bottlesofbeer.\n"+5 "Takeonedownandpassitaround,"+6 "98bottlesofbeeronthewall.\n"7 assert_equalexpected,Bottles.new.verse(99)8 end9 end
Thetestaboveisassimpleascanbe,butnoticethatwritingitrequiredmakingmanydecisions.Itcontainsbothaclassname( Bottles )andamethodname( verse(n) ).Thistestassumesthat
the Bottles classdefinesa verse methodthattakesanumberasanargument,anditassertsthatinvokingthatmethodwithanargumentof 99 returnsthelyricsforthe99thverse.
Thistest,likealltests,containsthreeparts:
SetupCreatethespecificenvironmentrequiredforthetest.
DoPerformtheactiontobetested.
VerifyConfirmtheresultisasexpected.
Lines3-6abovedefinetheexpectedresultandarethuspartofthesetup.Setupcontinuesonline7,whereanewbottleiscreatedvia Bottles.new .Line7alsosends verse(99) ,whichistheaction,andthenverifiestheresultwith assert_equal .
Runningthattestproducesthiserror:
TDDtellsyoutowritethesimplestcodethatwillpassthistest.Inthiscase,yourgoalistowriteonlyenoughcodetochangetheerrormessage.Theaboveerrorstatesthatthe Bottles classdoesnotyetexistsothefirststepistodefineit,asfollows:
Ifyou’renewtoTDD,thismayseemlikearidiculouslysmallstep.Becauseyouwrotethetest,youcanconfidentlypredictthatrunningitasecondtimewillnowproducethefollowingerror:
1)Error:BottlesTest#test_the_first_verse:NameError:uninitializedconstantBottlesTest::Bottlestest/bottles_test.rb:16:in`test_the_first_verse'
classBottlesend
Youcanchangethiserrormessagebyaddinga verse method.
Runningthetestnowproducesthiserror:
The verse methodrequiresanargument.Noticethatnothingaboutthismessagerequiresyoutoaddcodetothe verse method,somerelyaddinganargumentwillsufficetochangetheerror.
Rubyprogrammersbyconventionuse _ forthenameofanunusedargument.Thisargumentisunused,atleastatthismoment,so _ isareasonablenamefornow.
Runningthetestagainproducesthefollowingerror:
1)Error:BottlesTest#test_the_first_verse:NoMethodError:undefinedmethod`verse'for#<Bottles:0x007fde360741f0>test/bottles_test.rb:16:in`test_the_first_verse'
classBottlesdefverseendend
1)Error:BottlesTest#test_the_first_verse:ArgumentError:wrongnumberofarguments(1for0)Usersskm/Projects/books/99bottles/lib/bottles.rb:6:in`verse'test/bottles_test.rb:16:in`test_the_first_verse'
classBottlesdefverse(_)endend
1)Failure:
There’sfinallysufficientcodesothatthetestfailsbecausetheoutputisnotasexpectedinsteadofdyingbecauseofanexception.
Minitestshowsthedifferencebetweenexpectedandactualoutputbyprefixingtheexpectedwith'-'andtheactualwith'+'.Therefore,youcaninterprettheabovefailureasindicatingthatMinitestexpected
"99bottlesofbeeronthewall,99bottlesofbeer."followedbyanewline,followedby
"Takeonedownandpassitaround,98bottlesofbeeronthewall."followedbyanothernewline
butinsteadgot nil .
Payparticularattentiontohownewlinesarerepresentedabove.Theexpectedoutputstringcontainstwonewlines,specifiedas \ninthetestandshownaslinebreaksabove.Thefinalexpectedline, -" ,representsthesecondnewline.
Onceyoureachthispoint,it’seasytomakethetestpass;justcopytheexpectedoutputintothe verse method:
Listing2.2:Verse99Code
1 classBottles
BottlesTest#test_the_first_verse[test/bottles_test.rb:16]:---expected+++actual@@-1,3+1@@-"99bottlesofbeeronthewall,99bottlesofbeer.-Takeonedownandpassitaround,98bottlesofbeeronthewall.-"+nil
2 defverse(_)3 "99bottlesofbeeronthewall,"+4 "99bottlesofbeer.\n"+5 "Takeonedownandpassitaround,"+6 "98bottlesofbeeronthewall.\n"7 end8 end
TheAPIsaysthat verse takesanargument,butyoucanmakethisfirsttestpasswithoutactuallyusingit.Therefore,theargumentcontinuestobenamed _ inline2above.
Althoughthiscodepassesthetest,itclearlydoesn’tsolvetheentireproblem.Asamatteroffact,writingasecondtestwillbreakit.Whileitmayseempointlesstowriteanobviouslytemporaryandtransitionalbitofcode,thisistheessenceofTDD.
Youasthewriteroftestsknowthatthe verse methodmusteventuallytakethevalueofitsargumentintoaccount,butyouasthewriterofcodemustactinignoranceofthisfact.WhendoingTDD,youtogglebetweenwearingtwohats.Whileinthe"writingtests"hat,youkeepyoureyeonthebigpictureandworkyourwayforwardwiththeoverallplaninmind.Wheninthe"writingcode"hat,youpretendtoknownothingotherthantherequirementsspecifiedbythetestsathand.Thus,althougheachindividualtestiscorrect,untilareallwritten,thecodeisincomplete.
2.3.RemovingDuplicationNowthatthefirsttestpasses,youmustdecidewhatbesttotestnext.Thisnexttestshoulddothesimplest,mostusefulthingthatprovesyourexistingcodetobeincorrect.Whileitmayhavebeendifficulttoconceiveofthefirsttestbecausethepossibilitiesseeminfinite,thisnexttestisofteneasierbecauseitcheckssomething
relativetothefirst.
Verses99through3arenearlyidentical—theydifferonlyinthatthenumberchangeswithineachverse.Thetestabovealreadychecksthehighendofthisrange,andthereforeitnowmakessensetotestthelow.
Thefollowingtestforverse3exposesthecurrent verse methodtobeinsufficient:
Listing2.3:Verse3Test
1 deftest_another_verse2 expected="3bottlesofbeeronthewall,"+3 "3bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "2bottlesofbeeronthewall.\n"6 assert_equalexpected,Bottles.new.verse(3)7 end
TDDrequiresthatyoupasstestsbywritingsimplecode.However,mostprogrammingproblemshavemanysolutions,andit’snotalwaysclearwhichoneissimplest.Forexample,thefollowingcodepassesthesetestsbyaddingaconditionalthatchecksthevalueof number andreturnsthecorrectstring:
Listing2.4:Conditional
1 defverse(number)2 ifnumber==993 "99bottlesofbeeronthewall,"+4 "99bottlesofbeer.\n"+5 "Takeonedownandpassitaround,"+
6 "98bottlesofbeeronthewall.\n"7 else8 "3bottlesofbeeronthewall,"+9 "3bottlesofbeer.\n"+10 "Takeonedownandpassitaround,"+11 "2bottlesofbeeronthewall.\n"12 end13 end
Atfirstglance,thiscodeappearstohaveachievedtheultimateinsimplicity.Itcanproduceonlythelyricsforverses99and3,andsodoestheabsoluteminimumneededtopassthetests.
Butconsiderthatitnowcontainsaconditionalwherenoneexistedbefore.ThismaycauseyoutorecallthediscussiononCyclomaticComplexityinChapter1.Thisconditionaladdsanewexecutionpaththroughthecode,andadditionalexecutionpathsincreasecomplexity.Thiscodeissimpleinthesensethatitcan’tdomuch,butitdoesthatonesmallthinginanoverlycomplexway.
Partoftheproblemisthatalthoughthe if statementswitchesonnumber ,thetrueandfalsebranchescontainmanythingsthatdon’tvarybasedon number .Thebranchesdodifferinthatonesays99/98,andtheother3/2,buttheyarethesameforalloftheotherlyrics.Thiscodeconflatesthingsthatchangewiththingsthatremainthesame,andsoforcesyoutoparsestringswithyoureyestofigureouthow number matters.
Ifyouweretoalterthe if statementtoreturnonlythethingsthatchange,thecodewouldlooklikethis:
Listing2.5:SparseConditional
1 defverse(number)
2 ifnumber==993 n=994 else5 n=36 end7 8 "#{n}bottlesofbeeronthewall,"+9 "#{n}bottlesofbeer.\n"+10 "Takeonedownandpassitaround,"+11 "#{n-1}bottlesofbeeronthewall.\n"12 end13 end
Thiscodeisstillveryspecifictothetwoexistingtests—itcanproducethelyricsforverses99and3,andnoother.Notice,however,thatitnowhastwoparts.Thefirstpart(lines2-6)containstheconditional,andthesecond(lines8-11)containsatemplatethatcouldcorrectlygeneratemanyverses.Lines2-6arestillspecifictotheexistingtests,butnowthatyou’veseparatedthethingsthatchangefromthethingsthatremainthesame,lines8-11aregeneralizabletoeveryversebetween99and3.
Ifyouweretocontinuedownthe"specific"path,youwouldprogressivelyaddtestsfortheversesbetween97and4,eachtimealteringthe if statementtoaddaconditiontocheckforthatnumber.Followingthisstrategywouldultimatelyresultin97nearlyidenticaltestsand97nearlyidenticalverses;eachwoulddifferonlyinthevaluesofthenumbers.
Theobviousalternativeistoinsteadmakethecodemoregeneral.Becausetheexistingtemplatealreadyworksforeveryversebetween99and3,youcouldchangethiscodetoproducethoseversesbydeletingthe if statementandalteringthetemplatetoreferto number ,asshownhere:
Listing2.6:Interpolation
1 defverse(number)2 "#{number}bottlesofbeeronthewall,"+3 "#{number}bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "#{number-1}bottlesofbeeronthewall.\n"6 end
Lefttoyourowndevices,yourinstinctwouldlikelyhavebeentowritethecodeabovewithoutbotheringwiththeintermediatestepsshowninListing2.4:ConditionalandListing2.5:SparseConditional.However,evenifyouwouldnaturallyhavestartedwiththismoregeneralversion,it’simportanttounderstandandbeabletoarticulatetheimplicationsoftheotherimplementation.
Thedifferencebetweenthesolutionthataddsaconditionalandthesolutionthatinterpolatesavariableintoastringisthatinthefirst,asthetestsgetmorespecific,thecodestaysequallyspecific.Everyversehasitsownpersonaltestanditsownindividualcode;therewillneverbeatimewhenthecodecandoanythingwhichisnotexplicitlytested.
However,inListing2.6:Interpolation,asthetestsgetmorespecific,thecodegetsmoregeneric.[4]Oncethetestofverse3iswritten,thecodeisthengeneralizedtoproducelyricsforallverseswithinthe3-99range.
RememberthatthepurposeofthischapteristoquicklygettoShamelessGreen.Withthatgoalinmind,considertheabovesolutionsandanswerthisquestion:Whichissimplest?
Aspreviouslynoted,metricsaren’teverything,buttheycan
certainlybeausefulsomething.Inhopesthatdatawillhelpanswerthisquestion,thefollowingchartshowsSourceLinesOfCode,FlogscoreandCyclomaticComplexityforthevariants.
Table2.1:MetricsforCodeVariantsAfterTestsofVerse97and3
Solution SLOC FlogTotal CyclomaticComplexity
Listing2.4:Conditional
15 9.2 2
Listing2.5:SparseConditional
14 7.4 2
Listing2.6:Interpolation
7 5.1 1
Asyoucansee,astheexamplesprogress,theygetshorter,Flogtolowerscores,anddecreaseinCyclomaticComplexity.Thesemetricsindicatethateachsubsequentexampleisbetterthantheprevious,andthatthegeneralsolution,Listing2.6:Interpolation,isbestofall.Youmust,ofcourse,takemetricswithagrainofsalt,butheretheycastaclearvoteforListing2.6:Interpolation.
2.4.UnderstandingTransformationsTheprogressionshowninthemetricsabovelikelymapsnicelytoyourintuitivesenseofcorrectness.Intuition,however,ismerelyanunconsciousproddingtofollowanunarticulatedrule,anditturnsoutthatRobertMartinhasdraggedasetoftheseunconsciousrulesintothebrightlightofday.
IntheTransformationPriorityPremise,MartindefinesTransformationsas"simpleoperationsthatchangethebehaviorofcode."Notonlydoeshedescribeasetoftransformationsthat
movecodefrommorespecifictomoregeneric,hearrangesthesetransformationsin"priority"order,fromsimplertomorecomplex.Heassertsthatwhenaproblemcanbesolvedwithanyoneofseveraltransformations,thetransformationwiththehighestpriorityissimplestandthereforebest.
Intheexamplesabove,Listing2.6:Interpolationtransformsthecodebyinterpolatingavariableintoastring.InMartin’sterminology,thistransformationisanexampleofconstant→scalar("replacingaconstantwithavariableoranargument"),whichisfourthinpriorityonhislist.Listing2.4:ConditionalandListing2.5:SparseConditionalbothtransformcodebyaddingaconditionalwherenonepreviouslyexisted.Martincallsthisunconditional→if("splittingtheexecutionpath")andplacesitsixthinpriority.
Becauselowernumberedtransformationhavepriorityoverhighernumberedones,theTransformationPriorityPremisealsocastsavoteforListing2.6:Interpolationasthesimplersolution.Interpolatingavariableintoastringissimplerthanaddinganewconditional.
Metrics,theTransformationPriorityPremise,andcommonsenseconvergeonListing2.6:Interpolationasthebestsolution.Thissolutiongeneralizesthecode,whichcreatesanabstraction.Theabstractionexpressesaconcept,atruthifyouwill,aboutthe99Bottlesdomain,i.e.thatverses99through3arealikeinthattheirnumberschangeinacommonandpredictableway.
Noticethatthisgeneralizationisnotonebitspeculative.Youhaven’toverreached,ormadeguessesabouttheuncertainfuture.Here,inthismoment,thereare97exampleswhichfollowthesamestraightforwardrule.There’splentyofevidencetosupportreplacingduplicationwithanabstraction,anddoingsoheresimplifiesthecode.
Thenextsectionexaminesanearlyidenticalsituationwherethe
choiceofwhattodoaboutduplicationisnotnearlysoclear-cut.
2.5.ToleratingDuplicationVerses2,1and0muststillbetested,andeachisunique.Havingestablishedapatternoftestingversesintheorderthattheyappear,itmakessensetonexttestverse2.
Verse2differsinonesmallwayfromtheprevious97.Thefinalphraseinallpreviousversesrefersto"nnbottles"onthewall,i.e.theword"bottles"isplural.Hereinverse2,however,thefinalphrasereads"1bottle."Therefore,inline5ofthefollowingtestofverse2,theword"bottle"issingularinsteadofplural.
Listing2.7:Verse2Test
1 deftest_verse_22 expected="2bottlesofbeeronthewall,"+3 "2bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "1bottleofbeeronthewall.\n"6 assert_equalexpected,Bottles.new.verse(2)7 end
Runningthattestproducesthefollowingfailure:
Thisfailureisperfect;thetestexpected 1bottle ,butgot 1bottles .
Aswastruewiththetestforverse3,therearetwofundamentallydifferentwaystopassthistest.Youcanaddanewconditional
-Takeonedownandpassitaround,1bottleofbeeronthewall.+Takeonedownandpassitaround,1bottlesofbeeronthewall.
aroundtheexistingcode,orusethevalueof number insomewaywithinit.
Thisnextexampleillustratesthefirstpossibilitybywrappingthecodeinanewconditional:
Listing2.8:StarkConditional
1 defverse(number)2 ifnumber==23 "2bottlesofbeeronthewall,"+4 "2bottlesofbeer.\n"+5 "Takeonedownandpassitaround,"+6 "1bottleofbeeronthewall.\n"7 else8 "#{number}bottlesofbeeronthewall,"+9 "#{number}bottlesofbeer.\n"+10 "Takeonedownandpassitaround,"+11 "#{number-1}bottlesofbeeronthewall.\n"12 end13 end
Incontrast,thefollowingalternativeembedsinterpolatedlogicintotheexistingversestring:
Listing2.9:InterpolatedConditional
1 defverse(number)2 "#{number}bottlesofbeeronthewall,"+3 "#{number}bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+
5 "#{number-1}bottle#{'s'unless(number-1)==1}ofbeer"+6 "onthewall.\n"7 end
Atfirstglance,thesetwosolutionslookalotlikethealternativespreviouslyexploredforverse3.Listing2.8:StarkConditionalwrapstheexistingcodeinanewconditional(asdidListing2.4:Conditional).Moreover,Listing2.9:InterpolatedConditionaladdsinterpolationtotheversestring(similartoListing2.6:Interpolation).
Thechoiceofthebestalternativeforverse3wasguidedbothbymetricsandthe"TransformationPriorityPremise,"andthosethingsmightagainbeusefulhere.Thefollowingtableshowsmetricsforthenewexamples:
Table2.2:MetricsforCodeVariantsAfterTestofVerse2
Solution SLOC FlogTotal
CyclomaticComplexity
Listing2.8:StarkConditional
15 10.8 2
Listing2.9:InterpolatedConditional
9 10.9 saikuroreports1
ThetableaboveshowsthatListing2.9:InterpolatedConditionalisnoticeablyshorterandonlyfractionallymorecomplexthanthealternative.Also,intheprevioussection,thetestofverse3presentedanapparentlyidenticalproblem,andinthatcase,theinterpolatedversionwasthebestsolution.Inthatcase,itwassimplertointerpolate number intothestringthantowrapthe
stringinanewconditional.
ThemetricsandrecenthistoryseemtobecastingvotesforListing2.9:InterpolatedConditional,butinthiscasetheyarebothmisleading.
Thereareseveralproblemshere.First,oneofthemetricsshownaboveisjustplainwrong.SaikuroreportsthatListing2.9:InterpolatedConditionalhasaCyclomaticComplexityof1.However,line5ofthatexamplestates:
The unless keyworddefinesaconditionalwhichusesthevalueof number todeterminewhethertoappendtheletter"s"to"bottle."SaikurofailedtonoticethiskeywordandsoreportedtheCyclomaticComplexitytobe1,whichisincorrect.BothexamplesactuallyhaveaCyclomaticComplexityof2.
Therefore,theCyclomaticComplexityscoresareidenticalandtheFlogscoresvirtuallyso.Theonlydifferencebetweentheexamples,asleastasfarasthemetricsareconcerned,isthatListing2.9:InterpolatedConditionalisshorter.Shorterisoftenbetter,but,unfortunately,notinthiscase.
Aswasstatedintheprevioussection,astestsgetmorespecific,codeshouldbecomemoregeneric.Codebecomesmoregenericbybecomingmoreabstract.OnewaytomakecodemoreabstractistoDRYitout,thatis,toextractduplicatebitsofcodeintoasinglemethod,togivethatmethodaname,andthentorefertothecodebythisnewname.DRYingoutcoderemovestheduplicationandthusreducesitsoverallsize.
InListing2.9:InterpolatedConditional,thecodehasdefinitelygottenshorter.Onewouldhopethishappenedbecausethecodegotmoreabstract,butsadly,thisisnotthecase.Examinethenew
"#{number-1}bottle#{'s'unless(number-1)==1}ofbeer"
conditional(repeatedbelowforconvenience):
Noticethat,evenifanabstractionlurkshere,itcertainlyhasnotbeennamed.Ifforcedtosuggestanameyoumightcalltheunderlyingconcept"pluralization,"assertingthatthenewconditionalhandlespluralizationbyaddingan"s"tothestring"bottle"when (number-1) isotherthan1.
Ifpluralizationisameaningfulabstractionfor99Bottles,perhapsyoushouldcreatea pluralize method,asfollows:
Unfortunately,thecodeabovejustconfusestheissue.Theconceptofpluralizationisaredherring.[5]Theneedforitappearedsuddenlyandsoitfeelslikeanimportant,meaningful,test-drivenidea,butonlybecauseyou’reworkingwithincompleteinformation.
ExamineListing2.9:InterpolatedConditionalandcountthenumberoftimestheword"bottle"occurs,regardlessofwhetherinsingularorpluralform.Thefactthat"bottle"isduplicatedmanytimessignalsthatthere’sanunderlyingconceptthathasnotyetbeenunearthed.Withinthedomainofthesong,"bottle/bottles"representssomethingimportant,andthatthingisnotpluralization.Thesewordsallhavesomethingincommon,andisolatingasingleoccurrencebehindpluralizationlogic
"#{number-1}bottle#{"s"unless(number-1)==1}ofbeer"
defverse#..."#{number-1}#{pluralize(number)}ofbeer"#...end
defpluralize(number)"bottle#{'s'unless(number-1)==1}"end
obscuresthiscommonality.Makingonelookdifferentwillultimatelymakeithardertoseehowallarethesame.
Codelikethis pluralize methodgetswrittenwhenprogrammerstaketheDRYprincipletoextremes,asifthey’reallergictoduplication.DRYisimportantbutifappliedtooearly,andwithtoomuchvigor,itcandomoreharmthangood.Whenfacedwithasituationlikethis,askthesequestions:
DoesthechangeI’mcontemplatingmakethecodehardertounderstand?Whenabstractionsarecorrect,codeiseasytounderstand.Besuspiciousofanychangethatmuddiesthewaters;thissuggestsaninsufficientunderstandingoftheproblem.
Whatisthefuturecostofdoingnothingnow?Somechangescostthesameregardlessofwhetheryoumakethemnowordelaythemuntillater.Ifitdoesn’tincreaseyourcosts,delaymakingchanges.Thedaymaynevercomewhenyou’reforcedtomakethechange,ortimemayprovidebetterinformationaboutwhatthechangeshouldbe.Eitherway,waitingsavesyoumoney.
Whenwillthefuturearrive,i.e.howsoonwillIgetmoreinformation?Ifyou’reinthemiddleofwritingatestsuite,betterinformationisascloseasthenexttest.Squeezingallduplicationoutattheendofeverytestisnotnecessary.It’sperfectlyreasonabletotolerateabitofduplicationacrossseveraltests,hopingthatcodingupanumberofslightlyduplicativeexampleswillrevealthecorrectabstraction.It’sbettertotolerateduplicationthantoanticipatethewrongabstraction.
BothListing2.8:StarkConditionalandListing2.9:InterpolatedConditionalusethesametransformation(unconditional→if)andhavenearlyidenticalFlogandCyclomaticComplexityscores.
Fromthemetricspointofview,theonlymeasurabledifferencebetweentheexamplesisthatListing2.9:InterpolatedConditionalisshorter.Unfortunately,itisn’tshorterbecauseitcontainsanabstraction;it’sshorterbecauseitcramslackofunderstandingintoaverysmallspace.Thisbrevitymakesthecodehardertounderstand,andobscurestheconceptthatunderlies"bottles."
WritingShamelessGreenmeansoptimizingforunderstandability,notchangeability,andpatientlytoleratingduplicationifdoingsowillhelprevealtheunderlyingabstraction.Subsequenttests,orfuturerequirements,willprovidetheexactinformationnecessarytoimprovethecode.
AlthoughListing2.8:StarkConditionalretainssomeduplication,itresistscreatinganabstractioninadvanceofallavailableinformation,andsoisthebetterofthesetwosolutions.
2.6.HewingtothePlanAsyou’veseen,whenworkingtowardsShamelessGreen,itmakessensesometimestoeliminateduplicationandothertimestoretainit.TheShamelessGreensolutionisoptimizedtobestraightforwardandintention-revealing,anditdoesn’tmuchconcernitselfwithchangeabilityorfuturemaintenance.Thegoalistousegreentomaximizeyourunderstandingoftheproblemandtounearthallavailableinformationbeforecommittingtoabstractions.
Atsomepoint(actually,bytheendofthischapter)youwillhavewrittenafulltestsuitefor99Bottles,andacompleteShamelessGreensolution.Oncethat’sdone,you’llhavetwochoices.Youcoulddeploytheshamelesscodetoproductionandwalkaway,oryoucouldrefactoritintoamorechangeablearrangementbyDRYingoutduplicationandextractingabstractions.
WithinShamelessGreen,itisperfectlyacceptabletocreateabstractionsofideasforwhichyouhavemanyunambiguous
examples.Forexample,Listing2.6:Interpolationreduced97versestoasingleabstraction.Having97examplesgivesyouconfidencethatyouareseeingthecorrectabstraction,andcreatingitmakesthecodeeasiertounderstand.
WhenwritingShamelessGreen,youshouldexpresstheunambiguousabstractionsbutavoidgraspingforthenot-quitevisibleones.Listing2.9:InterpolatedConditionaljammedaconditionalinsidetheversestringtoavoidhavingtowriteaseparate,mostlyduplicate,copyofverse2.Inthiscasethenewcodewasconfusingandtherewereonlytwoexamples,sohereit’sbettertotakeadeepbreathandwritedownallofverse2whileawaitingmoreinformation.
ThinkofthepathtoShamelessGreenasrunningonahorizontalaxis.Somechangespropelyouforwardalongthispathandhelpyouquicklyreachgreen,whileothersarespeculativeandpossiblydistractingtangentsinaverticaldirection.Youshouldcompletetheentirehorizontalpathbeforeindulginginanyverticaldigressions.
Nowthatyouhavecodeforverses99-2,itmakessensetocontinuealongthehorizontalpathandwriteatestforverse1,asfollows:
Listing2.10:Verse1Test
1 deftest_verse_12 expected="1bottleofbeeronthewall,"+3 "1bottleofbeer.\n"+4 "Takeitdownandpassitaround,"+5 "nomorebottlesofbeeronthewall.\n"6 assert_equalexpected,Bottles.new.verse(1)7 end
Verse1isdifferentfromtheothersinanumberofways:
Itbeginswith"1bottle"insteadof"1bottles"
Itsays"Takeitdown"insteadof"Takeonedown"
Itendswith"nomorebottles"insteadof"0bottles"
Whileit’spossibletopassthistestbyaddinginterpolatedlogictotheversestring,yourexperiencewiththepriorexampleshoulddissuadeyoufromchoosingtodoso.Verse1isevenmorespecialthanwasverse2,andhavingdecidedthatverse2wasdifferentenoughtojustifyaddingacondition,thepatientpathtoShamelessGreenrequiresthatyoumakethesamedecisioninthecaseofverse1.
Thefollowingexampleaddsthecodeforverse1.Whiledoingsoitconvertstheexisting if statementtoa case statement:
Listing2.11:Verse1Code
1 defverse(number)2 casenumber3 when14 "1bottleofbeeronthewall,"+5 "1bottleofbeer.\n"+6 "Takeitdownandpassitaround,"+7 "nomorebottlesofbeeronthewall.\n"8 when29 "2bottlesofbeeronthewall,"+10 "2bottlesofbeer.\n"+11 "Takeonedownandpassitaround,"+12 "1bottleofbeeronthewall.\n"13 else
14 "#{number}bottlesofbeeronthewall,"+15 "#{number}bottlesofbeer.\n"+16 "Takeonedownandpassitaround,"+17 "#{number-1}bottlesofbeeronthewall.\n"18 end19 end
Giventhepriordiscussion,itmakessensetoaddanewbranchtotheconditionalforverse1,butthisexamplealsoswitchedfromif to case .Thesekeywordstelladifferentstory.
Lookatthefollowingpseudocodeandpondertheinferencesafuturereadermightdraw.Putyourselfintheirplace;imaginethatyoudidn’twritethecodeandthatyoudon’tcompletelyunderstandit.Whatdoesitmeantowrite if ratherthan case?
Useof if / elsif impliesthateachsubsequentconditionvariesin
ifnumber==1#somethingelsifnumber==2#somethingelseelse#defaultend
casenumberwhen1#somethingwhen2#somethingelseelse#defaultend
ameaningfulway.Because elsif 'softentestwildlydifferentconditions,futurereaderswillfeelobligedtocloselyexamineeachone.
Incontrast,useof case impliesthateveryconditionchecksforequalityagainstanexplicitvalue.Whileit’struethatthe whenclausesupportsmorecomplicatedoperations,theformaboveismostcommonandistheoneyourreaderswillexpect.Readersofcase statementsexpectconditionstobefundamentallythesame.
Inthe99Bottlescaseabove,theconditionsarefundamentallythesame.Switchingfrom if to case whenyouaddthecodeforverse1impliesthissameness,andsoisanactofkindnesstowardsyourreader.Intention-revealingcodeisbuiltfromtheaccumulationofsuchthoughtfulacts.
The verse methodisaccumulatinglotsofduplication,andthismayfeeltroubling.However,youareveryclosetohavingcodetoproduceeveryverse.WhileitmaybetemptingtoveerontotheverticalpathandbeginDRYingoutduplication,it’sbesttopushforwardhorizontally.
Withtheendinsight,thecostoffinishingthehorizontalpathislow.Onceit’scomplete,you’llhaveanexampleofeverydifferentkindofverse,andthereforemaximalinformationabouttheproblem.Whenthecurrentcodeiseasytounderstand,andmoreinformationisimminent,beshamelessandscrambletowardsgreen.
Proceedinghorizontally,then,here’sthetestforverse0:
Listing2.12:Verse0Test
1 deftest_verse_02 expected="Nomorebottlesofbeeronthewall,"+
3 "nomorebottlesofbeer.\n"+4 "Gotothestoreandbuysomemore,"+5 "99bottlesofbeeronthewall.\n"6 assert_equalexpected,Bottles.new.verse(0)7 end
Verse0isuniqueinthefollowingways:
Itsays"No/nomorebottles"insteadof"0bottles"
Itsays"Gotothestoreandbuysomemore"insteadof"Takeit/onedownandpassitaround"
Itendswith"99bottles"
Atthispointyouwilllikelybeunsurprisedtofindthatverse0getsitsownbranchintheconditional,asshownhere:
Listing2.13:Verse0Code
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 "nomorebottlesofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 when19 "1bottleofbeeronthewall,"+10 "1bottleofbeer.\n"+11 "Takeitdownandpassitaround,"+12 "nomorebottlesofbeeronthewall.\n"
13 when214 "2bottlesofbeeronthewall,"+15 "2bottlesofbeer.\n"+16 "Takeonedownandpassitaround,"+17 "1bottleofbeeronthewall.\n"18 else19 "#{number}bottlesofbeeronthewall,"+20 "#{number}bottlesofbeer.\n"+21 "Takeonedownandpassitaround,"+22 "#{number-1}bottlesofbeeronthewall.\n"23 end24 end
Thiscodecompletesthe verse method.Younowhavetestsforalltheversevariants,andcodetomakeeachtestpass.
Thisimplementationrevealssomeimportantconceptsinthedomain.It’seasy,forexample,toseethatthereare4basicversevariants:verse0,verse1,verse2andverses3-99.Also,verses3-99aresomuchalikethatitmadesensetoproducethemwiththesamebitofcode.
Theotherversesdiffer,notonlyfromthe3-99case,butalsofromeachother.Thecasestatementabovemakesitobviousthat0,1and2arespecial,althoughgranted,it’sdifficulttoseeinwhatway.Youhavetoreadthecodecarefullytoseehowtheversesareunique.
Thecodeiseasytounderstandbecausetherearen’tmanylevelsofindirection.Thislackofindirectionisadirectresultofthedearthofabstractions.Followingthehorizontalpathmeanswritingcodetoproduceeverykindofversebeforediverging
ontotangentstoDRYoutsmallbitsofcodethattheverseshaveincommon.Thegoalistoquicklymaximizethenumberofwholeexamplesbeforeextractingabstractionsfromtheirparts.
Nowthatyoucanproduceanysingleverse,it’stimetoturnyourattentiontoproducinggroupsofverses.
2.7.ExposingResponsibilitiesTheplanisforthe verses(a,b) methodtotaketwoarguments.Theseargumentsarenumbersthatspecifytherangeofversesforwhichthemethodshouldgeneratelyrics.Thehigh-levelAPIhasbeendefined,butbeforewritingthenexttest,youmustmakeseveralmoreprecisedecisions:
Inwhatorderdotheseargumentsappear?Doesthefirstargumentrepresentthefirstversetosing,suchthatitisalwaysgreaterthanthesecond,orviceversa?Inessence,whatexactlydo a and b represent,andhowshouldtheybenamed?
Dotheargumentsdenoteaninclusivelist,i.e.shouldyouproducelyricsfortheentirerangespecified?
Whatactualargumentvaluesdoesitmakemostsensetotest?
Groupsofversesgetsungfromahighertoalowernumber,soitmakessensetohavetheinitialargumentrepresentthefirstversetosing,andthusthehighernumber.Italsoseemsnaturaltospecifyaninclusivelistofversenumbers.Onceyoumakethesedecisions,you’vefinalizedthispartoftheAPIandcanbeginconsideringthetests.
Thefirst verses test,likethefirst verse test,shouldbethesimplestthingimaginable.Atthebeginningofthischapter,whenwritingtheinitial verse test,itmadesensetostartwiththefirstverseofthesong.Followingthatpattern,hereitmakessenseto
startinthesameplace,withverse99.However,sincethe versesmethodproducesasequenceofverses,itneedstwoarguments.Theshortestpossiblesequenceistwo,soit’sreasonableforthisfirsttesttobeforthesequencefrom99to98.
Here’sthetest:
Listing2.14:Verses9998Test
1 deftest_a_couple_verses2 expected="99bottlesofbeeronthewall,"+3 "99bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "98bottlesofbeeronthewall.\n"+6 "\n"+7 "98bottlesofbeeronthewall,"+8 "98bottlesofbeer.\n"+9 "Takeonedownandpassitaround,"+10 "97bottlesofbeeronthewall.\n"11 assert_equalexpected,Bottles.new.verses(99,98)12 end
Here’sonepossiblewaytopassthattest:
Listing2.15:Verses9998Literal
1 defverses(_,_)2 "99bottlesofbeeronthewall,"+3 "99bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "98bottlesofbeeronthewall.\n"+
6 "\n"+7 "98bottlesofbeeronthewall,"+8 "98bottlesofbeer.\n"+9 "Takeonedownandpassitaround,"+10 "97bottlesofbeeronthewall.\n"11 end
Althoughthecodeaboveclearlypassesthetest,manyprogrammerswillfinditobjectionable.Ifaskedtoarticulatetheflaw,youmightcomplainthatitduplicatescodefromthe versemethod.Thisiscertainlytrue.The verse methodalreadycontainsafairamountofduplication,andthisnew versesmethodrepeatssomeofthatexistingcode.
SomeduplicationistolerableduringthesearchforShamelessGreen.However,notallduplicationishelpful,andthere’ssomethingabouttheduplicationintroducedabovethatmeansitshouldnotbetolerated.Thisnewcodemuddiesratherthanclarifiesthewaters,andit’simportanttounderstandwhy.
Duplicationisusefulwhenitsuppliesindependent,specificexamplesofageneralconceptthatyoudon’tyetunderstand.Forexample,inthepriorsection,thecasestatementwithin verseevolvedtocontainfourdifferenttemplates.Thosetemplatesareconcreteexamplesofamoregenericverse.Eachsuppliesuniqueinformation,buttogethertheypointyoutowardstheunderlyingabstraction.
Theproblemwiththe verses implementationaboveisthatitdoesnotisolateanew,independentexample,butinstead,itduplicatesonethatyou’vealreadyidentified.Thecodetoproduceverses99and98alreadyexistsinthe else clauseofthecase statementof verse (repeatedbelow).
Listing2.16:VerseCaseStatementElseBranch
1 defverse(number)2 casenumber3 #...4 else5 "#{number}bottlesofbeeronthewall,"+6 "#{number}bottlesofbeer.\n"+7 "Takeonedownandpassitaround,"+8 "#{number-1}bottlesofbeeronthewall.\n"9 end10 end
NotethatListing2.15:Verses9998Literalisjustthenon-generalizedversionoftheabovepattern.Thus,thisnewcodeduplicatesanexamplethatalreadyexistsandsosuppliesnonewinformationabouttheproblem.Inaddition,duplicatingthisalready-existingcodemasksthetrueresponsibilityof verses .Thismethodwouldbemoreintention-revealingifthishiddenresponsibilitywereexposedinsteadofobscured.
The verses methodisresponsibleforunderstandingitsinputarguments,andforknowinghowtousetheseargumentstoproducethecorrectoutput.Itsjobisnottoknowtheexactlyricsforanyverse.Itsjobis,rather,torepeatedlyreferthisquestionontothe verse method,andtoaccumulatetheanswersintoamulti-versestring.
Codelongstobeasignorantaspossible.Whileitmakesperfectsenseforthe verse methodtoberesponsibleforknowingtheversetemplates,once verse assumesthisresponsibility,otherpartsofyourapplicationshouldnotusurpit.
Here’sanalternativeimplementationof verses thatknowsless
butrevealsmore:
Listing2.17:Verses9998Message
1 defverses(_,_)2 verse(99)+"\n"+verse(98)3 end
Thestorythiscodetellsisthat verses aremadeupof verse s(sorry),thatthere’sarelationshipbetweenasequenceofversesandanindividualverse.Listing2.15:Verses9998Literalhidthatrelationship,whilethisexamplebeginstoexposeit.
Thecodeaboveisthesimplestthingthatpassesthistest,butyou’reprobablychompingatthebittodomore.Youaresurelyawarethatthe verses methodmustultimatelyproducelyricsforall100verses.Yourecognizethatthecodeaboveisincompleteandthereforetemporary.Youknowthatthereal versesimplementationwillultimatelyloopfromstartingtoendingnumber,invoking verse foreachnumberandaccumulatingtheresponse.Followingthe"simplest-thing"ruleheremayfeeltediousandtime-consumingwhentherealsolutionissoobvious.
InChapter28ofTest-DrivenDevelopmentbyExample,KentBeckdescribesdifferentwaystomaketestspass.Threeofhis"GreenBarPatterns"are:
FakeIt("TilYouMakeIt")
ObviousImplementation
Triangulate
Theprevioustwoattemptsat verses (Listing2.15:Verses9998LiteralandListing2.17:Verses9998Message)areexamplesofFakeItbecausealthougheachimplementationpassesthecurrent
test,thetestsarenotyetcomplete.Thefirstexamplewasabandonedinfavorofthesecond,butbothareFakesbecauseneitherdoeseverythingthefinalspecwillrequire.
AnObviousImplementationsolutionis,well,obvious,andwhat’sobvioushereisthatthe verses shouldloopfrom99downto0,invoking verse foreachnumberandconcatenatingtheresults.Whentheobviousimplementationisevident,itmakessensetojumpstraighttoit.Ifyouareabsolutelycertainofthecorrectimplementation,there’snoneedtowearahairshirt[6]andrepetitivelyinchthroughaseriesoftinysteps.
Notice,however,thatattractivethoughthisideais,itisfraughtwithperil.ThesmallstepsofTDDacttoincrementallyrevealthecorrectimplementation.Ifyourabsolutecertaintyturnsouttobewrong,skippingtheseincrementalstepsmeansyoumisstheopportunityofbeingsetright.Anapparently"obvious"implementationthatisactuallyanincorrectguesswillcauseaworldofdownstreampain.
FakeItstyleTDDmayinitiallyseemawkwardandtedious,butwithpracticeitbecomesbothnaturalandspeedy.Developingthehabitofwritingjustenoughcodetopassthetestsforcesyoutowritebettertests.Italsoprovidesanantidoteforthehubrisofthinkingyouknowwhat’srightwhenyou’reactuallywrong.Althoughitsometimesmakessensetoskipthesmallstepsandjumpimmediatelytothefinalsolution,exercisecaution.It’sbesttosave"ObviousImplementation"forverysmallleaps.
ThenextGreenBarPatternisTriangulate,whichBeckdescribesasawayto"conservativelydriveabstractionwithtests."Triangulationrequireswritingseveraltestsatonce,whichmeansyou’llhavemultiplesimultaneousbrokentests.Theideaistowriteonebitofcodewhichmakesallofthetestspass.Triangulationismeanttoforceyoutoconvergeuponthecorrectabstractioninyourcode.
TriangulationissuchausefulideathatShamelessGreenexpandsitfromteststocode.Youcanexposeacommon,underlyingabstractionthroughtheaccumulationofmultipleconcreteexamples.Theseconcretecodeexamplesoftencontainsomeduplication,butthisduplicationisfineaslongaseachoverallexampleisindependentandunique.
Nowthatthe verses methodworksfor99and98,thenextstepistowriteatestthatassertsitcangenerateothersequences.Atthispoint,itmakessensetotesttheotherendoftherange.Here’satestfortheversesfrom2downto0:
Listing2.18:Verses2,1,0Test
1 deftest_a_few_verses2 expected="2bottlesofbeeronthewall,"+3 "2bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "1bottleofbeeronthewall.\n"+6 "\n"+7 "1bottleofbeeronthewall,"+8 "1bottleofbeer.\n"+9 "Takeitdownandpassitaround,"+10 "nomorebottlesofbeeronthewall.\n"+11 "\n"+12 "Nomorebottlesofbeeronthewall,"+13 "nomorebottlesofbeer.\n"+14 "Gotothestoreandbuysomemore,"+15 "99bottlesofbeeronthewall.\n"16 assert_equalexpected,
Bottles.new.verses(2,0)17 end
Onceagainyoumustchoosebetweenhard-codinganewspecialcaseorgeneralizingthecode.Forexample,youcouldmakethetestpassbyexplicitlyaddinganewconditionaltothe versesmethod,likeso:
Listing2.19:VersesSpecificRanges
1 defverses(starting,ending)2 ifstarting==993 verse(99)+"\n"+verse(98)4 else5 verse(2)+"\n"+verse(1)+"\n"+verse(0)6 end7 end
Alternatively,youcouldalterthecodetomakeitmoreabstract,asfollows:
Listing2.20:VersesWithinaRange
1 defverses(starting,ending)2 starting.downto(ending).collect{|i|verse(i)}.join("\n")3 end
Thischoicebetweena)addingaconditionalorb)makingthecodemoreabstractshouldremindyouofanearlierdiscussion.BackintheRemovingDuplicationsection,youfacedtheidenticalsituationwhenaltering verse topassthetestforverse3.
Inbothcases,therearemanyexistingexamplesoftheproblem(i.e.youknowallofthepossibleverseranges),andtheunderlyingabstractioniswellunderstood.Therefore,theargumentsmadeinRemovingDuplicationapplyherejustastheydidpreviously.
Relativetoitsalternative,Listing2.20:VersesWithinaRangeiseasiertounderstandandjustascheaptoimplement,andyouhavealltheinformationyouneedtofeelconfidentthatit’scorrect.Itisthebestsolutionnotonlybecauseitpassesthetest,butalsobecauseitclearlyexposestheresponsibilityof verses toproduceanyrangeofverses.Itgeneralizesthecode,whichisthebestchoicewhenyouareconfidentthatyouunderstandtheabstraction.
Nowthatyoucangenerateanysequenceofverses,thefinaltaskistoproducelyricsfortheentiresong.
2.8.ChoosingNamesAtthestartofthischapter,theplanwastocreatea Bottles classthatimplementedthefollowingAPI:
verse(n)
verses(starting,ending)#initiallyverses(a,b)
song
Thusfar,thisplanhasworkedswimmingly.The verse and versesmethodsarecomplete;it’stimetomoveonto song .
Thecodetoproducetheentiresongisquitestraightforward,asshownhere:
Listing2.21:SongCode
1 defsong2 verses(99,0)3 end
ThisisagoodtimetoreflectupontheAPIasawhole,andtoreconsiderthe song method.Thebodyof song isscarcelylongerthanitsname.Asthe verses methodisalreadyinthepublicAPI,usersof Bottles don’tneedthe song methodatall—theycouldsend verses(99,0) andgetbackthesameoutput.
Extraneouscodeaddscostswithoutprovidingbenefits,andatthispoint,it’squitereasonabletochallengetheneedfor song .Does song serveapurposeindependentof verses ,orisitredundantandthusacandidatefordeletion?
Answeringthisquestionrequiresthinkingabouttheproblemfromthemessagesender’spointofview.Whileit’struethatverses(99,0) and song returnthesameoutput,theydifferwidelyintheamountofknowledgetheyrequirefromthesender.Fromthesender’spointofview,itisonethingtoknowthatyouwantallofthelyricstothe99Bottlessongbutitisquiteanothertoknowhow Bottles producesthoselyrics.
Knowledgethatoneobjecthasaboutanothercreatesadependency.Dependenciestieobjectstogether,exacerbatingthecostofchange.Yourgoalasamessagesenderistoincuralimitednumberofdependencies,andyourobligationasamethodprovideristoinflictfew.
The song methodimposesasingledependency;touseit,youneedonlyknowitsname.
Usingthe verses methodtorequesttheentiresong,however,requiressignificantlymoreknowledge.Thesendermustknow:
“
thenameofthe verses method
thatthemethodrequirestwoarguments
thatthefirstargumentistheverseonwhichtostart
thatthesecondargumentistheverseonwhichtoend
thatthesongstartsonverse99
thatthesongendsonverse0
Thisisalotofknowledge.Therearemanywaysinwhichtheverses methodcouldchangethatwouldbreaksendersofthismessage.
2.9.RevealingIntentionsKentBeckexplainsthedifferencebetweenintentionandimplementation.
Thedistinctionbetweenintentionandimplementation[…]allows you to understand a computation first in essenceandlater,ifnecessary,indetail.
—KentBeckImplementationPatterns(p.69)
Here song istheintention,and verses(99,0) istheimplementation.There’sabigdifferencebetweenwantingthelyricsforarangeofverses,andwantingthelyricsfortheentiresong.The verses methodisinthepublicAPI,soitmustcontinuetoexist,butitsexistencedoesn’tobviatetheneedfor song .Sendersofthe song messagewantalloftheverses,andtheyoughtn’tbeforcedtotroublethemselveswithdetailsabouthowthishappens.
The song methodhavingdefendeditsworth,here’sthefullShamelessGreenfor99Bottles.
Listing2.22:ShamelessGreenInitial
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(starting,ending)7 starting.downto(ending).collect{|i|verse(i)}.join("\n")8 end9 10 defverse(number)11 casenumber12 when013 "Nomorebottlesofbeeronthewall,"+14 "nomorebottlesofbeer.\n"+15 "Gotothestoreandbuysomemore,"+16 "99bottlesofbeeronthewall.\n"17 when118 "1bottleofbeeronthewall,"+19 "1bottleofbeer.\n"+20 "Takeitdownandpassitaround,"+21 "nomorebottlesofbeeronthewall.\n"22 when223 "2bottlesofbeeronthewall,"+24 "2bottlesofbeer.\n"+25 "Takeonedownandpassitaround,"+
26 "1bottleofbeeronthewall.\n"27 else28 "#{number}bottlesofbeeronthewall,"+29 "#{number}bottlesofbeer.\n"+30 "Takeonedownandpassitaround,"+31 "#{number-1}bottlesofbeeronthewall.\n"32 end33 end34 end
Pleasingasthiscodemaybe,thealertreaderwillhavenoticedthatthe song methodwasintroducedwithoutfirstwritingatest.ThisisaclearviolationofTDD.
Indeed,thereareanumberofgapsinthetests.Forexample,thereisnocoverageforindividualverses4through97,andthere’snoguaranteethattheseversesappearinthecorrectorder.
Bottles nowproducesthatcorrectoutput,andit’stemptingtowalkawayatthispoint.However,doingsotransferstheburdenofkeepingthiscoderunningtosomepoordownstreamprogrammer,onewhohasfarlessunderstandingoftheproblemthanyoudorightnow.
Thenextsection,therefore,isconcernedwithtighteningupthetests.
2.10.WritingCost-E ectiveTestsTDDpromisesstraightforward,bug-freesoftwarethatcanbeconfidentlyandeasilychanged.TDDdoesnotclaimtobefree,
merelythatitsbenefitsoutweighitscosts.
BeliefinthevalueofTDDhasbecomemainstream,andthepressuretofollowthispracticeapproachesanunspokenmandate.Acceptanceofthismandateisillustratedbythefactthatit’scommonforfolkswhodon’ttesttosheepishlyapologizefornotdoingso.Eventhosewhodon’ttestseemtobelievetheyoughtto.
Despitethisgeneralagreement,thesadtruthisthatthepromiseofTDDhasnotbeenuniversallyfulfilled.Manyapplicationshaveteststhataredifficulttounderstand,challengingtochange,andprohibitivelytime-consumingtorun.Insteadofenablingchange,thesetestsactivelyimpedeit.Theworldislitteredwithtestsuitesthatareroundlyhatedbytheirmaintainers,sometimestothepointofabandonment.
Agreatdealofthispainoriginateswithteststhataretiedtoocloselytocode.Whenthisistrue,everyimprovementtothecodebreaksthetests,forcingthemtochangeinturn.Therefore,thefirststepinlearningtheartoftestingistounderstandhowtowriteteststhatconfirmwhatyourcodedoeswithoutanyknowledgeofhowyourcodedoesit.
Thissectionexplorestheproblemoftest-to-codecoupling.Asareminderofthecurrentstateofaffairs,herearethecurrenttests:
Listing2.23:NoSongTest
1 classBottlesTest<Minitest::Test2 deftest_the_first_verse3 expected="99bottlesofbeeronthewall,"+4 "99bottlesofbeer.\n"+5 "Takeonedownandpassitaround,"+
6 "98bottlesofbeeronthewall.\n"7 assert_equalexpected,Bottles.new.verse(99)8 end9 10 deftest_another_verse11 expected="3bottlesofbeeronthewall,"+12 "3bottlesofbeer.\n"+13 "Takeonedownandpassitaround,"+14 "2bottlesofbeeronthewall.\n"15 assert_equalexpected,Bottles.new.verse(3)16 end17 18 deftest_verse_219 expected="2bottlesofbeeronthewall,"+20 "2bottlesofbeer.\n"+21 "Takeonedownandpassitaround,"+22 "1bottleofbeeronthewall.\n"23 assert_equalexpected,Bottles.new.verse(2)24 end25 26 deftest_verse_127 expected="1bottleofbeeronthewall,"+28 "1bottleofbeer.\n"+29 "Takeitdownandpassitaround,"+30 "nomorebottlesofbeeronthewall.\n"
31 assert_equalexpected,Bottles.new.verse(1)32 end33 34 deftest_verse_035 expected="Nomorebottlesofbeeronthewall,"+36 "nomorebottlesofbeer.\n"+37 "Gotothestoreandbuysomemore,"+38 "99bottlesofbeeronthewall.\n"39 assert_equalexpected,Bottles.new.verse(0)40 end41 42 deftest_a_couple_verses43 expected="99bottlesofbeeronthewall,"+44 "99bottlesofbeer.\n"+45 "Takeonedownandpassitaround,"+46 "98bottlesofbeeronthewall.\n"+47 "\n"+48 "98bottlesofbeeronthewall,"+49 "98bottlesofbeer.\n"+50 "Takeonedownandpassitaround,"+51 "97bottlesofbeeronthewall.\n"52 assert_equalexpected,Bottles.new.verses(99,98)53 end54 55 deftest_a_few_verses56 expected="2bottlesofbeeronthewall,
"+57 "2bottlesofbeer.\n"+58 "Takeonedownandpassitaround,"+59 "1bottleofbeeronthewall.\n"+60 "\n"+61 "1bottleofbeeronthewall,"+62 "1bottleofbeer.\n"+63 "Takeitdownandpassitaround,"+64 "nomorebottlesofbeeronthewall.\n"+65 "\n"+66 "Nomorebottlesofbeeronthewall,"+67 "nomorebottlesofbeer.\n"+68 "Gotothestoreandbuysomemore,"+69 "99bottlesofbeeronthewall.\n"70 assert_equalexpected,Bottles.new.verses(2,0)71 end
2.11.AvoidingtheEcho-ChamberTheoutputof song isastringofonehundredverysimilarverses.Themethoddoesnotyethaveatest.Programmerswhowanttoremedythisomission,butwhoarehyper-alerttoduplication,maybetemptedtotest song likethis:
Listing2.24:WholeSongTestLogic
1 deftest_the_whole_song2 bottles=Bottles.new3 assert_equalbottles.verses(99,0),
bottles.song4 end
Thetestaboveassertsthat song returnsthesameoutputasdoesverses(99,0) .Onitsface,thisseemslikeagreatidea.Thetestisshort,itpasses,itwaseasytowrite,and(atleastforthemoment,whileyou’reimmersedintheproblem)it’seasytounderstand.However,thistesthasamajorflawthatcancauseittotogglefrom"shortandsweet"to"painfulandcostly"intheblinkofaneye.Thisflawliesdormantuntilsomethingchanges,sothebenefitsofwritingtestslikethisaccruetothewritertoday,whilethecostsarepaidbyanunfortunatemaintainerinthefuture.
Understandingthisflawrequiresbeingclearabout song 'sresponsibilities.Fromthemessagesender’spointofview, song isresponsibleforreturningthelyricsforall100verses.Imaginethatyouweretaskedtotestthismethodbutknewnothingabouthow Bottles wasimplemented.Youwouldbeunawareoftheexistenceofthe verses method,andwouldhavenochoiceotherthantotest song byassertingthatitsoutputmatchedthoselyrics.
Assertingthat song returnstheexpectedlyricsisverydifferentfromassertingthat song returnsthesamethingas verses .Inthefirstcase,the song testisindependentofimplementationdetailsandsotolerateschangestootherpartsoftheclasswithoutbreaking.Inthesecondcase,the song testiscoupledtothecurrent Bottles implementationsuchthatitwillbreakifthesignatureorbehaviorof verses changes,evenif song continuestoreturnthecorrectlyrics.
There’snothingmorefrustratingthanmakingachangethatpreservesthebehaviorofanapplicationbutbreaksapparentlyunrelatedtests.Ifyouchangeanimplementationdetailwhileretainingexistingbehaviorandarethenconfrontedwithaseaofred,youarerighttobeexasperated.Thisiscompletelyavoidable,andasignofteststhataretootightlycoupledtocode.Suchtests
impedechangeandincreasecosts.
Notonlyistheabove song testtootightly-coupledtothecurrentBottles implementation,itdoesn’tevenforceyoutowritetherightcode.Thefollowingbadly-broken Bottles classpassesthetestsuitewithoutactuallyproducingthecorrectsong.Noticethatthe verses methodbelowcanonlyreturnverses99-98,verses2-0,orthestring"ok."
Listing2.25:BadlyBrokenBottlesSong
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(starting,ending)7 ifstarting==99&&ending==988 "99bottlesofbeeronthewall,"+9 "99bottlesofbeer.\n"+10 "Takeonedownandpassitaround,"+11 "98bottlesofbeeronthewall.\n"+12 "\n"+13 "98bottlesofbeeronthewall,"+14 "98bottlesofbeer.\n"+15 "Takeonedownandpassitaround,"+16 "97bottlesofbeeronthewall.\n"17 elsifstarting==218 verse(2)+"\n"+verse(1)+"\n"+verse(0)19 else20 "ok"21 end
22 end23 24 defverse(number)25 casenumber26 when027 "Nomorebottlesofbeeronthewall,"+28 "nomorebottlesofbeer.\n"+29 "Gotothestoreandbuysomemore,"+30 "99bottlesofbeeronthewall.\n"31 when132 "1bottleofbeeronthewall,"+33 "1bottleofbeer.\n"+34 "Takeitdownandpassitaround,"+35 "nomorebottlesofbeeronthewall.\n"36 when237 "2bottlesofbeeronthewall,"+38 "2bottlesofbeer.\n"+39 "Takeonedownandpassitaround,"+40 "1bottleofbeeronthewall.\n"41 when342 "3bottlesofbeeronthewall,"+43 "3bottlesofbeer.\n"+44 "Takeonedownandpassitaround,"+45 "2bottlesofbeeronthewall.\n"46 else47 "99bottlesofbeeronthewall,"+48 "99bottlesofbeer.\n"+49 "Takeonedownandpassitaround,"+50 "98bottlesofbeeronthewall.\n"51 end
52 end53 end
Theabovecodeexploitsweaknessesinthetesttogettogreenwithoutactuallyproducingalloftheverses.Tocorrectthis,youmightbetemptedtochangethe song testasfollows:
Listing2.26:WholeSongTestLogicAgain
1 deftest_the_whole_song2 bottles=Bottles.new3 expected=99.downto(0).collect{|i|4 bottles.verse(i)5 }.join("\n")6 assert_equalexpected,bottles.song7 end
Thisnewtestsucceedsinforcing song toproduceeveryverse,butalteringthetestinthiswayjustdigsadeeperhole.Considerwhatjusthappened.Theoriginaltestassertsthatsending songproducesthesameresultasrunningthecodecurrentlycontainedin song .Inotherwords,itassertsthat
and
returnthesameoutput.
Thisnewtestassertsthat song producesthesameresultasrunningthecodecurrentlycontainedin verses .So
song
verses(99,0)
and
returnthesameoutput.
Noticethatalthoughthissecondvariantforcestheproductionofeveryverse,thetestcontinuestoechocodefrom Bottles .Now,insteadofassertingthattheoutputfrom song islikethecurrentimplementationof song ,itassertsthattheoutputof song islikethecurrentimplementationof verses .Thisdoesn’timprovethetest,butjusttightlycouplesthetesttocodethat’sonestepfartherbackinthestack.Ifthatmore-distantcodechanges,thistestmightbreak.
There’sanobvioussolutiontothistestingproblem,onealludedtoabove.The song testshouldknownothingabouthowtheBottles classproducesthesong.Theclearandunambiguousexpectationhereisthat song returnthecompletesetoflyrics,andthebestandeasiestwaytotest song istoexplicitlyassertthatitdoes.
Here’sthattest:
Listing2.27:SongTest
1 deftest_the_whole_song2 expected=<<-SONG3 99bottlesofbeeronthewall,99bottlesofbeer.4 Takeonedownandpassitaround,98bottlesofbeeronthewall.5
song
99.downto(0).collect{|i|bottles.verse(i)}.join("\n")
6 98bottlesofbeeronthewall,98bottlesofbeer.7 Takeonedownandpassitaround,97bottlesofbeeronthewall.8 9 97bottlesofbeeronthewall,97bottlesofbeer.10 Takeonedownandpassitaround,96bottlesofbeeronthewall.11 12 #...13 14 4bottlesofbeeronthewall,4bottlesofbeer.15 Takeonedownandpassitaround,3bottlesofbeeronthewall.16 17 3bottlesofbeeronthewall,3bottlesofbeer.18 Takeonedownandpassitaround,2bottlesofbeeronthewall.19 20 2bottlesofbeeronthewall,2bottlesofbeer.21 Takeonedownandpassitaround,1bottleofbeeronthewall.22 23 1bottleofbeeronthewall,1bottleofbeer.24 Takeitdownandpassitaround,nomorebottlesofbeeronthewall.
25 26 Nomorebottlesofbeeronthewall,nomorebottlesofbeer.27 Gotothestoreandbuysomemore,99bottlesofbeeronthewall.28 SONG29 assert_equalexpected,Bottles.new.song30 end
Inthelistingabove,the expected stringissolongthatverses96through5areelidedonline12.Inreallife,ofcourse,thelyricstoall100verseswouldbeexplicitlydetailedinthistest.
Thetextneededfor100versesisfairlylengthy,andyoumayresistwritingoutthefullstringbecauseofconcernsaboutduplication.
2.12.ConsideringOptionsIfyoufindtheduplicationdistressing,considerthealternatives.Yourchoicesare:
1. Assertthattheexpectedoutputmatchesthatofsomeothermethod.
Thefirsttwo song testvariantsdothis.Thosetestsarecoupledtothecurrent Bottles implementation,andsodependuponcharacteristicsofthatcode.
Thesedependenciesmeanthatchangestothe Bottles codemightbreakthe song test,evenifthereisnothingotherwisewrongwiththeapplication.
2. Assertthattheexpectedoutputmatchesadynamicallygeneratedstring.
Onceyouacceptthatthe song testshouldverifyspecificoutputratherthencoupletothecurrentimplementation,youmustdecidehowtocreatethatoutput.Because song returnsalong,duplicativestring,manyprogrammersfeeltempted,perhapsevenobligated,toreducethisduplicationbydynamicallycreatingtheverseswithinthetests.
However,reducingstringduplicationinsidethe song testwouldofnecessityrequirelogic.Thislogicalreadyexistsinthe Bottles class,sothetestwouldbeforcedtoinvoke,copy,orre-implementit.Regardlessofhowyoudoit,usinganylogicheremeansthatachangeto Bottles mightbreakthesong testinanunexpectedandconfusingway.
3. Assertthattheexpectedoutputmatchesahard-codedstring.
Inthiscase(asinListing2.27:SongTest)notonlyistheexpectedoutputclearlyandunambiguouslystated,butthetesthasnodependencies.Thesequalitiescombinetomakeiteasytounderstandandtotoleratechangesincode.
Ofthesethreechoices,onlythethirdisindependentofthecurrentimplementationandsoguaranteedtosurvivechangestoBottles .Itmaybedifficulttoreconcileyourselftowritingdowntheentirelyricsstring,butremember,DRYingoutthelyricsinthetestwouldforceyoutointroduceanabstraction.Testsarenottheplaceforabstractions—theyaretheplaceforconcretions.Abstractionsbelongincode.Ifyouinsistonreducingduplicationbyaddinglogictoyourtests,thislogicbynecessitymustmirrorthelogicinyourcode.Thisbindstheteststoimplementationdetailsandmakesthemvulnerabletobreakingeverytimeyouchangethecode.
DRYisaverygoodideaincode,butmuchlessusefulintests.Whentesting,thebestchoiceisveryoftenjusttowriteitdown.
Hereagainisthecomplete Bottles listing:
Listing2.28:ShamelessGreen
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(starting,ending)7 starting.downto(ending).collect{|i|verse(i)}.join("\n")8 end9 10 defverse(number)11 casenumber12 when013 "Nomorebottlesofbeeronthewall,"+14 "nomorebottlesofbeer.\n"+15 "Gotothestoreandbuysomemore,"+16 "99bottlesofbeeronthewall.\n"17 when118 "1bottleofbeeronthewall,"+19 "1bottleofbeer.\n"+20 "Takeitdownandpassitaround,"+21 "nomorebottlesofbeeronthewall.\n"22 when223 "2bottlesofbeeronthewall,"+24 "2bottlesofbeer.\n"+25 "Takeonedownandpassitaround,"+26 "1bottleofbeeronthewall.\n"27 else
28 "#{number}bottlesofbeeronthewall,"+29 "#{number}bottlesofbeer.\n"+30 "Takeonedownandpassitaround,"+31 "#{number-1}bottlesofbeeronthewall.\n"32 end33 end34 end
The Bottles testsandcodearenowcomplete.Thetestsarestraightforward,andthecodeiseasytounderstand.
2.13.SummaryTesting,donewell,speedsdevelopmentandlowerscosts.Unfortunatelyit’salsotruethatflawedtestsslowyoudownandcostyoumoney.
Itisworththeeffort,therefore,togetgoodattesting.TDDcanpreventcostlyguesses,butonlyifyoucommittowritingcodeinsmallsteps.Testscanmakeitsafeandeasytorefactor,butonlyiftheyarecarefullyde-coupledfromthecurrentcode.
Goodtestsnotonlytellastory,buttheylead,stepbystep,toawell-organizedsolution.Thetestswritteninthischaptergiverise(assumingproperrestraintonthepartoftheprogrammer)toShamelessGreen.
TheShamelessGreensolutionisneitherclevernorextensible.Itsvalueliesinthefactthatthecodeiseasytounderstand,andcheaptowrite.Ifnothingeverchanges,thissolutionisquitecertainlygoodenough.
Thingsgetmoreinterestingonlyifsomethingneedstochange.
So,ontoChapter3,whichintroducesanewrequirement,andforcesyoutomakesomeharddecisionsaboutthecode.
3.UnearthingConceptsTheShamelessGreensolutionvaluesunderstandability,straightforwardnessandefficiency,withlittleregardforchangeability.Itcontainsduplication,andisunapologeticaboutleaningintheproceduraldirection.It’sfast,andcheap,andmaybegoodenough,atleastuntilsomethingchanges.
However,intherealworld,requirementsdochange,andwhenthathappens,thestandardsforcoderise.
Thischapterdefinesanewrequirement,whichtriggersadeeperlookatthestructureofthecode.Itthenintroducesafewstraightforwardrulestoallowyoutosystematicallyandincrementallyimprovecode,withoutfearofgettinglostorintroducingbugs.Therulesaresimple,buttheyallowcomplexbehaviortoemerge.Bytheendofthischapter,you’llhavebeguntounearthconceptsthatarecurrentlyhiddeninthecode.
3.1.ListeningtoChangeCodeisexpensive.Writingitcoststimeormoney.Itthereforebehoovesyoutobeasefficientaspossible.Themostcost-effectivecodeisasgoodasnecessary,butnobetter.
However,programmingisanart,andprogrammersloveelegantcode.Theconundrumisthatonceaninitial,moreprosaic,solutionexists,theproblemissolved,andthechoiceofwhethertodeliveritasis,ortoimproveuponitatthismoment,mustbeweighedcarefully.
Iftheproblemissolved,andyouchoosetorefactornowratherthanlater,youpaytheopportunitycostofnotbeingabletoworkonotherproblems.Spendingtime"improving"codebasedpurelyonaestheticsmaynotbethebestuseofyourprecioustime.
Agoodwaytoknowthatyou’reusinglimitedtimewiselyistobedrivenbychangesinrequirements.Thearrivalofanewrequirementtellsyoutwothings,oneveryspecific,theothermoregeneral.
Specifically,anewrequirementtellsyouexactlyhowthecodeshouldchange.Waitingforthisrequirementavoidstheneedtospeculateaboutthefuture.Therequirementrevealsexactlyhowyoushouldhaveinitiallyarrangedthecode.
Moregenerally,theneedforchangeimposeshigherstandardsontheaffectedcode.Codethatneverchangesobviouslydoesn’tneedtobeverychangeable,butonceanewrequirementarrives,thebarisraised.Codethatneedstobechangedmustbechangeable.Thus,anewrequirementforthe99Bottlesproblemwilldriveyoutoimprovethecode.
Here’sthatnewrequirement:usershaverequestedthatyoualterthe99Bottlescodetooutput"1six-pack"ineachplacewhereitcurrentlysays"6bottles."
Here’sareminderofthecurrentstateofthecode.
Listing3.1:ShamelessGreen
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(starting,ending)7 starting.downto(ending).collect{|i|verse(i)}.join("\n")8 end9
10 defverse(number)11 casenumber12 when013 "Nomorebottlesofbeeronthewall,"+14 "nomorebottlesofbeer.\n"+15 "Gotothestoreandbuysomemore,"+16 "99bottlesofbeeronthewall.\n"17 when118 "1bottleofbeeronthewall,"+19 "1bottleofbeer.\n"+20 "Takeitdownandpassitaround,"+21 "nomorebottlesofbeeronthewall.\n"22 when223 "2bottlesofbeeronthewall,"+24 "2bottlesofbeer.\n"+25 "Takeonedownandpassitaround,"+26 "1bottleofbeeronthewall.\n"27 else28 "#{number}bottlesofbeeronthewall,"+29 "#{number}bottlesofbeer.\n"+30 "Takeonedownandpassitaround,"+31 "#{number-1}bottlesofbeeronthewall.\n"32 end33 end34 end
InthesamewaythatShamelessGreenmakesnoguessesaboutthefuture,youshouldrefrainfrommakinguprequirements.
Noticetherequestisnotto"replaceeverymultipleof6withnnsix-pack(s)"nordoesitmentionspecialhandlingfor"cases"ofbeer.Therequirementissimplytooutput"1six-pack"whereitcurrentlysays"6bottles."Knowledgeofthedomainmaypromptyoutoqueryyourcustomerabouttheseotherpossibilities,andpastexperiencemayoccasionallyleadyoutoinferarequirementotherthantheonespecified.Butgenerallyit’sbesttoclarifyrequirements,andthenwritetheminimumnecessarycode.
Despitethefactthatyoushouldrarelyinfernewrequirements,it’struethatthingsthatchange,do.Nowthatsomeonehasaskedforachange,youhavelicensetoimprovethiscode.ThecodearrangementthatwasacceptableforShamelessGreenisnotnecessarilybestforenablingchange.
ConditionalsarethebaneofOO.ShamelessGreencontainsacasestatement,andwithinitsbranches,muchduplication.Whilethiswasacceptableintheinitialsolution,considertheresultifyoucontinuedowntheconditionalpath.Thefollowingexampleillustratestheproblembyamendingtheexistingcodetomeetthe"six-pack"requirement.
Listing3.2:CompoundingConditionalSins
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 #...6 when17 "1bottleofbeeronthewall,"+8 #...9 when210 "2bottlesofbeeronthewall,"+
11 #...12 when613 "1six-packofbeeronthewall,"+14 "1six-packofbeer.\n"+15 "Takeonedownandpassitaround,"+16 "5bottlesofbeeronthewall.\n"17 when718 "7bottlesofbeeronthewall,"+19 "7bottlesofbeer.\n"+20 "Takeonedownandpassitaround,"+21 "1six-packofbeeronthewall.\n"22 else23 "#{number}bottlesofbeeronthewall,"+24 #...25 end26 end27 end
The verse casestatementinitiallycontainedfourbranches,andinthecodeabovethenumberofbrancheshasballoonedtosix.Thisisunacceptable.Conditionalsbreed,andnowthatthisonehasstartedreproducing,youmustdosomethingtostopit.
3.2.StartingWiththeOpen/ClosedPrincipleThedecisionaboutwhethertorefactorinthefirstplaceshouldbedeterminedbywhetheryourcodeisalready"open"tothenewrequirement.
"Open"isshortfor"Open/Closed,"whichinturnisshortfor"openforextensionandclosedformodification."The"O"inopensuppliesthe"O"intheacronym"SOLID"(seesidebar).Codeis
opentoanewrequirementwhenyoucanmeetthatnewrequirementwithoutchangingexistingcode.
SOLIDDesignPrinciplesTheSOLIDacronymwascoinedbyMichaelFeathersandpopularizedbyRobertMartin.Eachletterstandsforawell-knownprincipleinobject-orienteddesign.Here’saformal
definitionofeachone:
S-SingleResponsibility
Themethodsinaclassshouldbecohesivearoundasinglepurpose.
O-Open-Closed
Objectsshouldbeopenforextension,butclosedformodification.
L-LiskovSubstitution
Subclassesshouldbesubstitutablefortheirsuperclasses.
I-InterfaceSegregation
Objectsshouldnotbeforcedtodependonmethodstheydon’tuse.
D-DependencyInversion
Dependonabstractions,notonconcretions.
Ifyoufindtheabovedefinitionslessthenenlightening,don’tdespair.Asprinciplesarereferencedinthisbook,
plainlanguageexplanations(liketheonebelow)willfollow.
The"open"principlesaysthatyoushouldnotconflatetheprocessofmovingcodearound,ofrefactoring,withtheactofaddingnewfeatures.Youshouldinsteadseparatethesetwooperations.Whenfacedwithanewrequirement,firstrearrangetheexistingcodesuchthatit’sopentothenewfeature,andoncethat’scomplete,thenaddthenewcode.
Thecurrent Bottles classisnotopentothe"6-packs"requirementbecauseaddingnewversevariantsrequireseditingtheconditional.Therefore,whenfacedwiththisnewrequirement,yourfirsttaskistorefactortheexistingcodeintoashapesuchthatyoucanthenimplementthenewrequirementbymerelyaddingcode.Unfortunately,itisquitelikelythatyoudonotknowhowtodothis,andsoareatalossabouthowtoapproachtheproblem.
Fortunately,youdonothavetoknoweverythinginordertochoosetherightplacetostart.Whenfacedwiththissituation,beguidedbythefollowingflowchart.
Figure3.1:OpenClosedFlowchart
Aspertheaboveflowchart,firstaskyourselfiftheexistingcodeisalreadyopentothenewrequirement.Ifso,yourjobissimplytowritethenewcode.
Ifnot,nextaskifyouknowhowtoaltertheexistingcodetomakeitopentothenewrequirement.Thiscaseisalsostraightforward.Ifso,makethealteration,andthenwritethenewcode.
However,thesadtruthisthattheanswertobothofthosequestionsisoften"no."Theexistingcodeisn’topentothenew
requirement,andyouhavenoideahowtomakeitso.Atthispoint"codesmells"cometotherescue.Ifyoucanidentifysmellsincode,youisolateflawsandcorrectthemonebyone.
3.3.RecognizingCodeSmellsMostcodeisimperfect.Itsflawsaremany,andsothoroughlyentangledthatitisimpossibletocorrectallofthematonce.Ifyou’veevertackledabitofcode,makingchangeafterchangewithoutmanagingtocompletethetask,andeventuallyrollingeverythingback,youknowthisproblem.
Thetricktosuccessfullyimprovingcodethatcontainsmanyflawsistoisolateandcorrectthemoneatatime.InhisRefactoringbook,MartinFowleridentifiesandnamesmanycommonflaws,andprovidesrefactoringrecipestofixthem.Chapter3(whichwasco-writtenbyKentBeck,whocoinedtheterm)callstheflaws"codesmells."ThankstoFowler’sbook,ifyoucanidentifyasmellwithincode,youcanlookupthecurativerefactoring,andapplyittoremovetheflaw.
Ifyou’rewonderingifyouneedtogoreadFowler’sbookrightnow,theansweris,“notnecessarily.”Fowler’sprinciplesareintroducedanddemonstratedhere.However,thisbookexploresonlyafewofthemanyrefactoringrecipeswithwhichyouwouldbewell-servedtobefamiliar.Fowler’sbookisanexcellentinvestment.Also,ifyoupreferyourexamplesinRuby,youmaybeinterestedinJayFields'versionofthebook.
Ifaskedtolistafewcodesmells,youmightsuggest"duplication,"or"classesthataretoobig,"anditisindeedtruethatDuplicatedCodeandLargeClassaretwoofthesmellslistedinMartinFowler’sRefactoringbook.It’sfairlyobvioushowtoremovethesecommonsmells(abstractawaytheduplication,ordivideoneclassintoseveral),andsoitmayappearthatsmellsareageneral,hand-wavykindofthing.
However,therearemanyothercodesmellswithwhichyoumaynotbeasfamiliar.Youcanprobablyguessthedefinitionof"DivergentChange,"butcanyoudefine"FeatureEnvy?"Canyourecognizeandspecifythecurativerefactoringsfor"PrimitiveObsession,""InappropriateIntimacy,"or"ShotgunSurgery?"
Acompleteexplorationofeverycodesmellisbeyondthescopeofthisbook,especiallysinceMr.Fowlerhascoveredthetopicsothoroughly.However,therefactoringsundertakenherewillbedriven,andguided,bysmells,sothetaskathandistoidentifythesmellsinthecurrent Bottles class.Theeasiestwaytounearththesesmellsistomakealistofthethingsyoudislikeaboutthecode.
3.4.IdentifyingtheBestPointofAttackThecurrent99Bottlescodeisnot"open"tothesix-packrequirement.Ifyouareunclearabouthowtomakeitopen(whichisoftenthecase),thewayforwardistostartremovingcodesmells.Ifthesmellsaren’timmediatelyobvious,startbymakingalistofthethingsyoufindobjectionable.
Considerthe verse method(repeatedbelow).
Listing3.3:ShamelessVerse
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 "nomorebottlesofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 when1
9 "1bottleofbeeronthewall,"+10 "1bottleofbeer.\n"+11 "Takeitdownandpassitaround,"+12 "nomorebottlesofbeeronthewall.\n"13 when214 "2bottlesofbeeronthewall,"+15 "2bottlesofbeer.\n"+16 "Takeonedownandpassitaround,"+17 "1bottleofbeeronthewall.\n"18 else19 "#{number}bottlesofbeeronthewall,"+20 "#{number}bottlesofbeer.\n"+21 "Takeonedownandpassitaround,"+22 "#{number-1}bottlesofbeeronthewall.\n"23 end24 end
Thismethodcontainsa case statement(theSwitchStatementssmell)whosebranchescontainmanyduplicatedstrings(DuplicatedCode).Ofthesetwosmells,DuplicatedCodeisthemoststraightforwardandsowillbetackledfirst.
Therefore,thecurrenttaskistorefactorthe verse methodtoremovetheduplication,inhopeandexpectationthattheresultingcodewillbemoreopentothesix-packrequirement.
Beforeundertakingthisrefactoring,itmustbeadmittedthatthereisnodirectconnectionbetweenremovingtheduplication,andsucceedinginmakingthecodeopentothesix-packrequirement.That,however,isthebeautyofthistechnique.Youdon’thavetoknowhowtosolvethewholeprobleminadvance.
“
Theplanistonibbleaway,onecodesmellatatime,infaiththatthepathtoopennesswillberevealed.
3.5.RefactoringSystematicallyHavingbandiedthewordaroundrepeatedly,it’shightimeforaformaldefinitionof"refactoring."AccordingtoFowler:
Refactoringistheprocessofchangingasoftwaresysteminsuchawaythatitdoesnotaltertheexternalbehaviorofthecodeyetimprovesitsinternalstructure.
—MartinFowlerRefactoring
Inshort,refactoringaltersthearrangementofcodewithoutchangingitsbehavior.Recallthatnewrequirementsshouldbeimplementedintwosteps.First,yourearrangeexistingcodesothatitbecomesopentothenewrequirement.Next,youwritenewcodetomeetthatrequirement.Thefirstofthesestepsisrefactoring.
Notethatsaferefactoringreliesupontests.Ifyoutrulyarerearrangingcodewithoutchangingbehavior,ateverystepalongthewaytheexistingtestsshouldcontinuetopass.Testsareasafetyblanketthatjustifiesconfidenceinthenewarrangementofcode.Iftheybegintofail,oneoftwothingsmustbetrue.Eithera)you’veinadvertentlybrokenthecode,orb)theexistingtestsareflawed.
Iftestsfailbecauseyou’vebrokenthecode,thecureissimple.Undothelastchange,makeabetteroneandproceedmerrilyalongyourway.
However,ifyourearrangecodewithoutchangingbehaviorandtestsbegintofail,thentheteststhemselvesareflawed.Teststhatmakeassertionsabouthowthingsaredone,ratherthanwhat
actuallyhappens,aretheprimecontributorstothispredicament.Forexample,atestthatmakesassertionsabouthowamethodisimplementedwillobviouslybreakifyouchangethatmethod’simplementation,evenifitsoutputisunchanged.Wheninthissituation,there’snoalternativeotherthantoimprovethetestsbeforeembarkinguponarefactoring.
Testsarethewallatyourback.Successfulrefactoringsleanongreen.Therefore,youshouldneverchangetestsduringarefactoring.Ifyourtestsareflawedsuchthattheyinterferewithrefactoring,improvethemfirst,andthenrefactor.
3.6.FollowingtheFlockingRulesRecallthatthecurrenttaskistoremoveduplicationfromthecase statementofthe verse method.
The case statementhasfourbranches,eachofwhichcontainsaversetemplate.Thetemplatesrepresentdistinctversevariants.Thesevariantsobviouslydiffer,butinsomenot-yet-identified,more-abstractway,theyarealsoalike.
Consideredfromahigherviewpoint,eachvariantismerelyaverseinthesong;inthatsensetheyareallthesame.Underlyingeachconcretevariantisageneralizedverseabstraction.Ifyoucouldfindthisabstraction,youcoulduseittoreducethefour-branch case statementtoasinglelineofcode.
Thegoodnewsisthatyoudon’thavetobeabletoseetheabstractioninadvance.Youcanfinditbyiterativelyapplyingasmallsetofsimplerules.Theserulesareknownas"FlockingRules",andareasfollows:
FlockingRules
1. Selectthethingsthataremostalike.
2. Findthesmallestdifferencebetweenthem.
3. Makethesimplestchangethatwillremovethatdifference.
Changestocodecanbesubdividedintofourdistinctsteps:
1. parsethenewcode
2. parseandexecuteit
3. parse,executeanduseitsresult
4. deleteunusedcode
Makingsmallchangesmeansyougetverypreciseerrormessageswhensomethinggoeswrong,soit’susefultoknowhowtoworkatthislevelofgranularity.Asyougainexperience,you’llbegintotakelargersteps,butifyoutakeabigstepandencounteranerror,youshouldrevertthechangeandmakeasmallerone.
Asyou’refollowingtheflockingrules:
Fornow,changeonlyonelineatatime.
Runthetestsaftereverychange.
Ifthetestsfail,undoandmakeabetterchange.
Why"Flocking"?Birdsflock,fishschool,andinsectsswarm.Aflock’sbehaviorcanappearsosynchronizedandcomplexthatitgivestheimpressionofbeingcentrallycoordinated.Nothingcouldbefurtherfromthetruth.Thegroup’sbehavioristheresultofacontinuousseriesofsmall
decisionsbeingmadebyeachparticipatingindividual.Thesedecisionsareguidedbythreesimplerules.
1. Alignment-Steertowardstheaverageheadingofneighbors
2. Separation-Don’tgettooclosetoaneighbor
3. Cohesion-Steertowardstheaveragepositionoftheflock
Thus,complexbehavioremergesfromtherepeatedapplicationofsimplerules.Inthesamewaythattherulesinthissidebarallowbirdstoflock,the"FlockingRules"forcodeallowabstractionstoappear.
FlockofStarlingsActingAsASwarm,JohnHolmes,CCBY-SA2.0
Toseeabeautifulexampleofflockinginaction,watchStevenStrogatz’sTheScienceofSyncTEDtalk.
“
3.7.ConvergingonAbstractionsTheFlockingRulesaresoatomic,andsogeneral,thattheymaynotyetinspireconfidence.Theremainderofthischapterwillusethemtounearthabstractionsinthe verse method,afterwhichyoumayfindtheprocessmoreconvincing.
3.7.1.FocusingonDi erenceWhileit’struethatthereareproblemsforwhichthesolutionisobvious,thoseofanyinterestingsizearen’ttractabletoinstantunderstanding.They’retoobig,orhavetoomanyparts.
Whenexaminingcomplicatedproblems,theeyeisfirstdrawntowardssameness.However,despitethefactthatsamenessiseasiertoidentify,differenceismoreusefulbecauseithasmoremeaning.DRYingoutsamenesshassomevalue,butDRYingoutdifferencehasmore.
ErichGamma,RichardHelm,RalphJohnsonandJohnVlissidesarecommonlyreferredtoasthe"GangofFour,"inreferencetotheirjointauthorshipofDesignPatterns:ElementsofReusableObject-OrientedSoftware.Thisinfluentialbookdescribestwenty-threepatternsorsolutionstocommonOOprogrammingproblemsanditexplainsthisprocessthusly:
Thefocushereisencapsulatingtheconceptthatvaries,athemeofmanydesignpatterns.
Differenceholdsthekeytounderstanding.Iftwoconcreteexamplesrepresentthesameabstractionandtheycontainadifference,thatdifferencemustrepresentasmallerabstractionwithinthelargerone.Ifyoucannamethedifference,you’veidentifiedthatsmallerabstraction.
Thegoodnewsisthatasystematicapplicationoftherulesofrefactoringconvertsdifferencetosameness,decomposingaproblemintoitsconstituentparts.Theevenbetternewsisthatthishappensautomatically.Youdon’thavetoidentifytheunderlyingabstractionsinadvanceofrefactoring.Ifyoumerelywritethecodedictatedbytherules,theabstractionswillfollow.
Thehabitofbelievingthatyouunderstandtheabstraction,andofjumpingtoaninventedsolution,isdeeplyingrained.Programmersstudyaproblem,decideonasolution,andthenimplementit.Solutionsarecraftedbyintention.
Ifthisdescribesyourentirepastexperience,youmayfindthefollowingcodesurprising.Ittakesmanysmall,iterativesteps,andresultsinasolutionthatisdiscoveredbyrefactoring.
Toreducethe verse case statementtoasinglelineofcode,therulessaytofirstidentifythethingsthataremostalike.Thismeansthatyoushouldselectthetwobranchesthataremostalike,andfocusonmakingthemidentical.
Hereagainisareminderofthe case statement:
Listing3.4:VerseMethodConditional
1 casenumber2 when03 "Nomorebottlesofbeeronthewall,"+4 "nomorebottlesofbeer.\n"+5 "Gotothestoreandbuysomemore,"+6 "99bottlesofbeeronthewall.\n"7 when18 "1bottleofbeeronthewall,"+9 "1bottleofbeer.\n"+
10 "Takeitdownandpassitaround,"+11 "nomorebottlesofbeeronthewall.\n"12 when213 "2bottlesofbeeronthewall,"+14 "2bottlesofbeer.\n"+15 "Takeonedownandpassitaround,"+16 "1bottleofbeeronthewall.\n"17 else18 "#{number}bottlesofbeeronthewall,"+19 "#{number}bottlesofbeer.\n"+20 "Takeonedownandpassitaround,"+21 "#{number-1}bottlesofbeeronthewall.\n"22 end
Noticethatalthoughverse2containshardcodednumbersfor 2and 1 ,itcouldjustascorrectlysay number and number-1 ,asintheelse branch.Thispartlooksdifferent,butislogicallythesame.Itmayhelptorecallthatverse2hasbutonetest,whichassertsthatthefinallinesays“1bottle”insteadof“1bottles.”Theonlyrealdifferencebetweenthe2andelsecasesistheword“bottle”versustheword“bottles.”Therefore,thesearethelinesthataremostalike.
3.7.2.SimplifyingHardProblemsHavingfoundthestringsthataremostalike,thenexttaskistomakethemidentical.It’simportanttofocusonthisspecificgoalwithoutsuccumbingtothetemptationsoftangents.
Thinkoftheprocessofturningthesetwolinesintooneasbeingonahorizontalpath.[7]Whilewalkingthispath,ifsomethingcatchesyoureyeinanotherpartofthecode(perhapsinthe 0 or
1 cases),youmaybetemptedtoveeroffinaverticaldirection.However,ifyoubeginmakingchangestootherpartsofthecodebeforeyoucompletelycombinethe 2 and else cases,youstepoffawell-trodpathintoawoodssodarkandsinisterthatyoumightneverreturn.Whileitcanbeusefultointerleavehorizontalandverticalwork,it’sbesttofinishthecurrentjourneywhentheterminusofthehorizontalpathisinsight.
Havealookatthecodebelow,anddecidewhattodonext.
Listing3.5:2andElseCase
1 when22 "2bottlesofbeeronthewall,"+3 "2bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "1bottleofbeeronthewall.\n"6 else7 "#{number}bottlesofbeeronthewall,"+8 "#{number}bottlesofbeer.\n"+9 "Takeonedownandpassitaround,"+10 "#{number-1}bottlesofbeeronthewall.\n"11 end
Recallthattheselineswerechosenbecausetheonlyrealdifferencebetweenthemisusing"bottle"versus"bottles"inthefinalphrase.Theotherapparentdifferencesareactuallysimilarities.The 2 and 1 inthe 2 casecanbereplacedby #{number} and #{number-1} respectively,whichmeansthatthesepartsarelogicallyidentical.
Thechangeneededtoresolvethedifferencesbetweenthe
numbersisobvious.Thatpartoftheproblemfeelssolved.It’sboring.The"bottle/bottles"difference,however,ismuchmoreinteresting.Itrequiresmorethought.
Programmerslovehardproblems.Notonlythat,butmanytimesthemostdifficultbitofalargerproblemistheriskiest,andrequiresthemostthought.It’snowonderthatmanyprogrammersgravitatetowardsstartingaproblematitsmostconfusingpart.Itjustsohappensthatsolvingeasyproblems,throughamagicalalchemyofcode,sometimestransmuteshardproblemsintoeasyones.Itiscommontofindthathardproblemsarehardonlybecausetheeasyoneshavenotyetbeensolved.
Therefore,don’tdiscountthevalueofsolvingeasyproblems.Withthatinmind,thefirststeptowardsmakingtheselinesidenticalistoresolvetheveryfirstdifference.Scanninglefttoright,theveryfirstcharacterofthe 2 casecouldbereplacedby #{number} .Proceedingrightwards,thenext 2 cansimilarlybereplaced.Scanningfurtherstill,the 1 canbecome #{number-1} .Theresultisshownbelow:
Listing3.6:ReplaceHardCodedNumber
1 when22 "#{number}bottlesofbeeronthewall,"+3 "#{number}bottlesofbeer.\n"+4 "Takeonedownandpassitaround,"+5 "#{number-1}bottleofbeeronthewall.\n"6 else7 "#{number}bottlesofbeeronthewall,"+8 "#{number}bottlesofbeer.\n"+9 "Takeonedownandpassitaround,"+
10 "#{number-1}bottlesofbeeronthewall.\n"11 end
Aftermakingtheabovechange(andrunningthetestsbetweeneach,ofcourse),theremainingdifferenceis"bottle/bottles"onthelastline:
Listing3.7:OneDifferenceRemains
1 when22 #...3 "#{number-1}bottleofbeeronthewall.\n"4 else5 #...6 "#{number-1}bottlesofbeeronthewall.\n"7 end
Thisisthefirstinterestingdifference.Nowyoumustdecidewhatthisdifferencemeans.
3.7.3.NamingConceptsPrevioussectionsstatethatifallversesarethesameinsomefundamentalway,thenanunderlyingverseabstractionmustexist.Thegoalofthecurrentrefactoringistofindawaytoexpressthatmoreabstractverse.
Ifanunderlyingverseabstractionexists,thenthissmalldifferencebetweenverse2andverses3-99mustrepresentasmallerabstractionwithinthatlargerone.Tomakethesetwolinesthesame,youmustnamethisconcept,createamethodnamedaftertheconcept,andreplacethetwodifferenceswitha
commonmessagesend.Therefore,it’stimetodecidewhatthewords"bottle"and"bottles"representinthecontextofthesong.
YoumayrecallfromtheConcretelyAbstractsectionofChapter1that"bottle"isnotunderlyingtheconcept.Ifyoucallthemethod"bottle"youarenamingitafteritscurrentimplementation,andyou’vealreadyseenhowthatcangobadlywrong.
Also,despitethatfactthatthesetwowordsdifferinthatoneissingularandoneisplural,theunderlyingconceptisnot"pluralization."Withinthecontextofthesong,"bottle/bottles"doesnotrepresentpluralization.
Therearetwopiecesofinformationthatcanhelpinthestruggleforaname.Oneisageneralruleandtheotheristhenewrequirement.
First,thenewrequirement.Recallthattheimpetusforthisrefactoringwastheneedtosay"six-pack"insteadof"bottle/bottles"whenthereare6bottles.Thestring"six-pack"isonemoreconcreteexampleoftheunderlyingabstraction.Thissuggeststhatifyounamethemethod"bottle,"youwillregretthisdecisioninshortorder.
Thegeneralruleisthatthenameofathingshouldbeonelevelofabstractionhigherthanthethingitself.Thestrings"bottle/bottles/six-pack"areinstancesofsomecategory,andthetaskistonamethatcategoryandtodosousinglanguageofthedomain.
Onewaytoidentifythecategoryistoimaginetheconcreteexamplesasrowsandcolumnsinaspreadsheet.[8]Thefollowingtableillustratesthisidea.Thistablecontainsthreerows,oneforeachconcreteexample.Eachrowhastwocolumns.Thefirstcolumncontainsanumberofbottles;thenext,thewordusedwiththatnumberinthesong
Table3.1:BottlesColumnHeader
Number xxx?
1 bottle
6 six-pack
n bottles
Column1abovecontainsnumbers,so"Number"makessenseasacolumnheader.Theheader"Number"isalevelofabstractionhigherthantheconcreteexamples."1,""6,"and"n"arenumbers.
Thesecondcolumnhasentriesforbottle,six-pack,andbottles.Bottleisanentityinthisas-yetunnamedcategory,ratherthanthecategoryitself.
Itmightseemasif"Unit"wouldbeagoodheader.Althoughit’struethateveryexampleissomekindofunit,therearetwoproblemswiththisname.First,it’stooabstract.Unitisnotonelevelofabstractionhigherthantheexamples,it’smany.Thereareplentyofgoodnamingalternativesonthecontinuumbetween"bottle"and"unit."Next,unitisnotinthelanguageofthedomain.Thenameyouchoosewillbethenameyouuseinconversationswithyourcustomers.Namingthingsafterdomainconceptsimprovescommunicationbetweenyouandthefolkswhopaythebills.Onlygoodcancomeofthis.
Whenyou’restrugglingtofindagoodnamebuthaveonlyafewconcreteinstancestoguideyou,itcanbeilluminatingtoimagineotherthingsthatwouldalsobeinthesamecategory.[9]Forexample,ifthesongwereaboutwine,thewinemightcomeinacarafe.Juicesometimescomesinsmallboxes.Softdrinkscomeincans.
Ifyouweretoaskyourusers,"Whatkindofthingisabottle?,"theywouldn’treply"It’saunit."Insteadtheymightcallitthecontainer.Inthecontextof99Bottles,containerisagoodnameforthisconcept.Containerismeaningful,understandable,andunambiguous.
Havingnamedtheconcept,it’stimetowritecodetoremovethedifference.
3.7.4.MakingMethodicalTransformationsNowthatyou’vedecidedtocreatea container method,it’stimetoalterthecode.It’stemptingtomakeallofthenecessarychangesinonefellswoop.Doingsorequiresaddinganewmethodandinvokingitintwoplaces.Here’sthenewmethod:
Listing3.8:GuessEntireContainer
1 defcontainer(number)2 ifnumber==13 "bottle"4 else5 "bottles"6 end7 end
Thismethodmustbeinvokedfrombothbranchesofthe versecase statement.Herethecode:
Butwait.Noticethattheabovechangeaddssevennewlinesofcode,changestwoexistingones,andalterscodeinthreeseparateplaces.Anyofthesechangescouldintroduceerrors,whichyouwouldthenbeobligedtounderstandandcorrect.Thissmallexamplestandsinforthemuchbiggerreal-lifeproblemwhere,
"#{number-1}#{container(number-1)}ofbeeronthewall.\n"
intheprocessofimplementinganewfeature,youaddmanylinesofcode,changemanyothers,andthenrunthetests,onlytobeconfrontedwithaoceanofred.
Realworldproblemsarebig.Realcodehasbugs.Realtestsareoftentightlycoupledtocurrentimplementations.Ifyousimultaneouslychangemanythingsandsomethingbreaks,you’reforcedtounderstandeverythinginordertofixanything.Youcouldendupchasingafterred,withincreasingdesperation,beforeeventuallydiscardingallofthechangesandbeginninganew.
Makingaslewofsimultaneouschangesisnotrefactoring—it’srehacktoring.Itwouldbemuchbettertomakeaseriesoftinychangesandrunthetestsaftereach.Ifthetestsfail,youknowtheexactchangethatcausedthefailure,andcanundobacktogreenandmakeabetterchange.Ifthetestspass,youknowthatthecurrentcodeworks,eveniftherefactoringisonlypartiallycomplete.
Formalrefactoringconferstwoadditionalbenefits.First,becausenochangebreaksthetests,thecodecanbedeployedtoproductionatanyintermediatepoint.Thisallowsyoutoavoidaccumulatingalargesetofchangesandsufferingthroughapainfulmerge.Next,codethatrunsproperlyeveninthemidstofalongrefactoringincreasesthebusfactor.Thiscontributestoahigherlikelihoodofprojectsuccessevenifyou,personally,weretomeetanuntimelyend.
Addingthe container methodbyrefactoringmeanstakingaseriesofsmallsteps.Asareminder,hereagainaretheFlockingRulesandcorollaries:
FlockingRules
1. Selectthethingsthataremostalike.
2. Findthesmallestdifferencebetweenthem.
3. Makethesimplestchangetoremovethatdifference:
a. parsethenewcode
b. parseandexecuteit
c. parse,executeanduseitsresult
d. deleteunusedcode
Asyou’refollowingtherules:
Ingeneral,changeonlyonelineatatime.
Runthetestsaftereverychange.
Ifyougored,undoandmakeabetterchange.
You’vealreadyfollowedrule1(youchosethe 2 and else cases)andrule2(you’veworkedyourwayacrosstothe"bottle/bottles"difference).Nowyou’reonrule3,readytoremovethisdifference.Asyouintendtochangeonlyonelineatatime,you’llofnecessityhavetomakesmallchangesiteratively.
Thefirststepistocreateanempty container method.
Listing3.9:EmptyContainerMethod
1 defcontainer2 end
Nowrunthetests.
Ifthisadmonitioncomesasasurprise,considerthathavinggreentestsatthispointprovidesaveryusefulpieceoffeedback.
Eventhoughthe container methodisnotyetbeinginvoked,greentestsatthispointprovethatthecodeyoujustwroteissyntacticallycorrect.Thismeansyouarefollowingrule3a,whichcallsforseparatingparsefromexecute.
Nowthatyouhavewrittenthisadmittedlynotveryexcitingcontainer method,thenextstepistomakethesmallestchangethatwilladvancethecodeintheintendeddirection.Here’sareminderofthetargetline:
Listing3.10:OneDifferenceRemainsRedux
1 when22 #...3 "#{number-1}bottleofbeeronthewall.\n"4 else5 #...6 "#{number-1}bottlesofbeeronthewall.\n"7 end
Thecurrent container methodreturns nil .Itwilleventuallybecalledfromtwoplaces.The 2 casewantsthereturntobe"bottle,"andthe else case,"bottles."Thenextincrementalchangeistoalterthemethodtomakeitusableforjustoneofthosecallers.Therefore,youmustnowchoosewhichvaluetoreturnfirst.
Thedefaultcaseisoftenagoodplacetostart,andthere’snoreasonnottodosohere.Inthatspirit,change container toreturnbottles ,likeso:
Listing3.11:SparseContainerMethod
1 defcontainer2 "bottles"3 end
Fromnowon,itgoeswithoutsayingthatyoushouldrunthetestsaftereverychange.
Nowthat container returnsausablevalue,alterthe else branchtosendthemessageinplaceoftheword"bottles,"asonline8below:
Listing3.12:SparseContainerUsedinElseBranch
1 defverse(number)2 #...3 when24 #...5 "#{number-1}bottleofbeeronthewall.\n"6 else7 #...8 "#{number-1}#{container}ofbeeronthewall.\n"9 end10 end11 12 defcontainer13 "bottles"14 end
Sofar,sogood,butconsiderthenextstep.Tobeusableinboththe 2 and else cases, container musteventuallyreturnthe
correctchoicebetween bottle or bottles .Thedecisionbetweenthemisbasedonthevalueof number ,which container doesnotyetknow.Therefore, container mustbechangedtotakeanargument.
Justas container doesn’tcurrentlytakeanargument(line12above),itsinvokerdoesn’tcurrentlysendone(line8above).Nowyoufaceaconundrum.Thegoalistomakechangesononelineatatime,butthissituationseemstorequirethatyouchangeboththesenderandthereceiversimultaneously.
Toillustratetheproblem,considerwhathappensifyoumakeeitherofthesechangeswithouttheother.Youcouldaddtheargumenttothemethoddefinitionfirst,likeso:
Inthiscase,themessagesendfailsbecauseitdoesn’tyetsendtheargument:
Ifyoureversetheorderofthechanges,andsendtheargumentfirst,asso:
Thentheoppositefailureoccurs,i.e.anargumentispassedwherenoneisexpected.
Thisproblem,needingtoaddarequiredargument,arisesregularlyintherealworld.Butinsteadofonesenderandonereceiver,asinthiscase,realapplicationsmighthave10or100or1000senders.Itmightbeimpossibletofixeverythingatonce,so
defcontainer(number)
ArgumentError:wrongnumberofarguments(0for1)
"#{number-1}#{container(number-1)}ofbeeronthewall.\n"
ArgumentError:wrongnumberofarguments(1for0)
it’shandytoknowthetechniqueforworkingaroundthisprobleminanincrementalmanner.
3.7.5.RefactoringGraduallyInhisbookRefactoringtoPatterns,JoshuaKerievskytalksabout"GradualCutoverRefactoring,"astrategyforkeepingthecodeinareleasablestate,graduallyswitchingoverasmallnumberofpiecesatatime.Thistypeofrefactoringcanbedonealongsideotherdevelopmentworksoasnottoaffectthereleaseschedule.
Manyofyourcolleagues,includingyourcustomers,willapplaudthisstrategyandyourwillingnesstokeepthecodereleasable.Butwhenyouarepreventedfromfixingallofthesendersatonce,youmustdosomethingtoallowsometopassthenewargumentwhileothersremainunchanged.Thetrickhere,asyoumayalreadyhaveguessed,istobeginbyaddinganoptionalargumentthatsuppliesitsowndefault,asshownbelow:
Listing3.13:ContainerWithDefaultedArgument
1 defcontainer(number=:FIXME)2 "bottles"3 end
Theabovecodetakesanargumentnamed number ,whichitdefaultstothesymbol :FIXME .Youmayhaveexpectedthedefaulttobe nil ,orattheveryleast,anumericvalue,butinthiscaseitmakessensetosetittosomethingthat’susefullywrong.Thisdefaultisatemporaryshimwhosepurposeistoenableastep-by-steprefactoring.Oncetherefactoriscomplete,thedefaultshouldberemoved.Settingittoavaluelike:FIXMEwillhelpyouremembertodothisclean-up.
Nowthatthe container methodacceptsanargument,considerthenextstep.Youcouldeither:
alter container tocheckthevalueof number andreturn"bottle"or"bottles,"i.e.changethereceiver,or
alterthe else branchtoaddthe number argumentto containermessage,i.e.changethesender.
Therefactoringrulesprohibityoufrommakingbothofthesechangesatonce,soyoumustchooseoneortheother.
Becausethe container methoddoesnotyetreference number ,changingthe else branchtopassthisargumentchangesalmostnothingaboutthecode.Insteadofpassingtheargument,thebetterchoiceistoexpandthecodein container touse number todecidewhichof"bottle"or"bottles"toreturn,asfollows:
Listing3.14:ContainerWithConditional
1 defcontainer(number=:FIXME)2 ifnumber==13 "bottle"4 else5 "bottles"6 end7 end
Thereareseveralthingstonoteabouttheabovestrategy.
First,noticethataddingtheconditionalwasveryclearlyamultilinechange.Thismayappeartobreakthe"makechangesononlyoneline"rule,butinthiscase,thechangeisobeyingthespiritofthelawwhileslightlyignoringitsletter.Thisconditionalcouldhavebeenexpressedinternaryform,as:
number==1?"bottle":"bottles"
whichwouldcertainlyhavebeenaone-linechange.Themultiline if formaboveispreferredinthisrefactoringforreasonsthatwillbecomeclearinlaterchapters.Fornow,justthinkofthesetwoformsasbothobeyingthe"oneline"rule.
Next,rememberthatthismethodisbeinginvokedfromonlyoneplace(the else branchofthe case statementin verse ),andthatasyetnoargumentisbeingpassed.Thismeansthatthe numberargumentin container getssetto :FIXME ,whichroutesexecutiontothe false branch.Thenewcodeinthetruebranchisnotyetbeingexecuted,althoughitgetsparsedwhenthetestsrun.
Theactofaddinganewbranchtotheconditionalwhileexecutingonlythepreviouslyexistingcodeisamini-exampleoftheOpen/ClosedPrinciple.Youcanthinkofthischangeasmakingthe container methodopentoanewrequirement—enablingittooccasionallyreturntheword"bottle."Thissplitsthechangeintoseveralsmallsteps,whichmakesiteasiertodebuganyerrors.
Thenexttinystepistochangethesendertoactuallypassthenewargument.Because container isbeinginvokedfromthefourthphraseofthesong,thevalueoftheargumentis number-1 ,asshownonline8below:
Listing3.15:PassinganArgumenttoContainer
1 defverse(number)2 #...3 when24 #...5 "#{number-1}bottleofbeeronthewall.\n"6 else7 #...
8 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"9 end10 end11 12 defcontainer(number=:FIXME)13 ifnumber==114 "bottle"15 else16 "bottles"17 end18 end
Theabovestepmightseemsotinyastoseempointlesstoisolate,butthere’sarealdifferencebetweenexecutingthe false branchbecauseofthe :FIXME default,andbeingroutedtherebecauseofthevalueofthe number argument.Inthefirstcase,youknowthatifyougotothe false branchthetestspass,andinthesecond,youknowthattheargumentbeingpassedtakesyoutothe falsebranch.Bothofthesethingsmustworkorthetestswillbreak.Changingcodeatthislevelofgranularitymakesiteasiertohandleunexpectedfailures.
Thenextstepistochangethe2branchsothatitalsoinvokesthecontainermethod,asshownbelow:
Listing3.16:2andElseCasesIdentical,NumberDefaultExists
1 defverse(number)2 #...3 when24 "#{number}bottlesofbeeronthewall,"+
5 "#{number}bottlesofbeer.\n"+6 "Takeonedownandpassitaround,"+7 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"8 else9 "#{number}bottlesofbeeronthewall,"+10 "#{number}bottlesofbeer.\n"+11 "Takeonedownandpassitaround,"+12 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"13 end14 end15 16 defcontainer(number=:FIXME)17 ifnumber==118 "bottle"19 else20 "bottles"21 end22 end
Theabovechangehastwoconsequences.First,allofthecodeincontainer isnowbeingexecuted.Next,thecodeinthe 2 and elsebranchesofthe verse casestatementarenowidentical.
Twotasksremaintocompletethisentirehorizontalrefactoring.First,asallsendersof container nowpass number ,the :FIXMEdefaulthasserveditspurposeandcanberemoved.Next,the 2caseisnowobsolete,andsoitalsocanbedeleted.Thefollowingexampleshowstheresulting,completecode:
Listing3.17:2SubsumedIntoElseCase,NumberDefaultRemoved
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 "nomorebottlesofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 when19 "1bottleofbeeronthewall,"+10 "1bottleofbeer.\n"+11 "Takeitdownandpassitaround,"+12 "nomorebottlesofbeeronthewall.\n"13 else14 "#{number}bottlesofbeeronthewall,"+15 "#{number}bottlesofbeer.\n"+16 "Takeonedownandpassitaround,"+17 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"18 end19 end20 21 defcontainer(number)22 ifnumber==123 "bottle"24 else25 "bottles"
26 end27 end28 end
Thathorizontalrefactoringrequiredafairamountofexplanation.Here’sareminderofthekeyactions:
1. identified verse 2 and else asthemostsimilarcases
2. workedfromlefttoright
3. changed verse 2 casetoreplacehardcoded 2 with #{number}(twice)
4. changed verse 2 casetoreplacehardcoded 1 with #{number-1}
5. identified"bottle"and"bottles"asthenextdifference
6. chosecontainerforthenameoftheconceptrepresentedbythisdifference
7. createdempty container method
8. changed container toreturn"bottles"
9. changed verse else casetosend container inplaceof"bottles"
10. changed container totake number argumentwithdefault:FIXME
11. addedconditionallogicto container toreturn"bottle"or"bottles"basedon number
12. changed verse else casetopass number-1 to container
13. changed verse 2 casetosend container(number-1) inplaceof
"bottle"
14. deleted verse 2 case
15. deleted container :FIXME number argumentdefault
Ofthese15steps,12involvechangestocode.Thetestsrunaftereverychange,soitistrivialtofixnewly-introducedflaws.
Thelengthydescriptionabovemayhaveledyoutofearthatworkinginthisfashionwouldbeunbearablyslow.Takeanotherlook.Asyoucansee,there’snotmuchcode,andwithpractice,writingitbecomesveryfast.Thesmallamountoftimelosttomakingincrementalchangesismorethanrecoupedbyavoidinglengthyandfrustratingdebuggingsessions.Thisstyleofcodingisnotonlyfast,it’salsostress-free.
Thisfirstrefactoringwasdeliberatelyperformedusingthesmallestpossiblesteps.Onceyoulearntoworkatthislevelofgranularity,youcanlatercombinestepsifcircumstancesallow.Letredbeyourguide.Ifyoutakeagiantstepandthetestsbegintofail,undoandfallbacktomakingsmallerchanges.
Thereareplentyofhardproblemsinprogramming,butthisisn’toneofthem.Realrefactoringiscomfortinglypredictable,andsavesbrainpowerforpeskierchallenges.
3.8.SummaryWhenfacedwiththeneedtochangecode,veryoftenthehardestdecisioniswheretostart.ThischaptersuggestedthatyoubeguidedbytheOpen-ClosedPrinciple,andsoseparatemostchangesintotwobroadsteps.First,refactortheexistingcodetobeopentothenewrequirement,next,addthenewcode.
Sometimesthefirststep,refactoringtoopenness,requiressuchalargeleapthatitisnotobvioushowtoachieveit.Inthatcase,be
guidedbycodesmells.Improvecodebyidentifyingandremovingsmellsandhavefaiththatasthecodeimproves,apathtoopennesswillappear.
Makingexistingcodeopentoanewrequirementoftenrequiresidentifyingandnamingabstractions.TheFlockingRulesconcentrateonturningdifferenceintosameness,andthusareusefultoolsforunearthingabstractions.
Thischapterintroducedthesix-packrequirement,andinthesearchforopenness,identifiedtheduplicationofcodeintheverse methodasthefirstpointofattack.Itthendedicatedagoodportionofthechaptertothetaskofmakingthe else and 2 casesidentical.However,nowthatyou’velearnedhowtousetheflockingrulestoidentifyabstractions,resolvingthedifferencesinthe 1 and 0 caseswillgomuchfaster.So,ontoChapter4,andmoreextractingofabstractions.
4.PracticingHorizontalRefactoringThepreviouschapterintroducedtheFlockingRules,whichitusedtoremovethespecialcaseforverse2.Thechaptercontainedplentyofexplanationabouthowtoapplytherules,butnotmuchnewcode.Fortunately,therefactoringsdictatedbytheFlockingRulesareeasierdonethansaid,andhavingreadthepriorchapter,youarenowequippedtomovebrisklythroughtheotherspecialcases.
ThischapteriterativelyappliestheFlockingRulestotheremainingspecialverses,andresultsinasingle,moreabstract,templatethatproduceseverypossibleverse.
4.1.ReplacingDi erenceWithSamenessTherefactoringrulessaytostartbychoosingthecasesthataremostalike.Nowthatverse2isbeingproducedbythe elsebranch,onlythreedifferentversetemplatesremain.Havealookatthecodebelow,andselectthetwocasesonwhichtoconcentratenext.
Listing4.1:3BranchCaseStatement
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 "nomorebottlesofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 when1
9 "1bottleofbeeronthewall,"+10 "1bottleofbeer.\n"+11 "Takeitdownandpassitaround,"+12 "nomorebottlesofbeeronthewall.\n"13 else14 "#{number}bottlesofbeeronthewall,"+15 "#{number}bottlesofbeer.\n"+16 "Takeonedownandpassitaround,"+17 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"18 end19 end
The 1 casediffersfromthe else caseinseveralways.Itusesahardcoded 1 asthestartingnumber,ittakes "it" insteadof"one" down,anditendswith "nomore" insteadof number-1bottles.
The 0 caseisevenmoredifferentfromthe else case.Itstartswith "Nomore" ,itsays "Gotothestoreandbuysomemore" ,anditendswith "99" .
Finally,the 1 and 0 casesdifferfromoneanotherinlotsofways.Theybothhavemoreincommonwiththe else casethaneachother.
Ofthesethreeversetemplates,the 1 and else casesaremostalike,sothey’rethenexttoaddress.Startbylookingatthefirstlinesofeach:
Listing4.2:1andElse,1stPhrasesDiffer
1 when1
2 "1bottleofbeeronthewall,"+3 #...4 else5 "#{number}bottlesofbeeronthewall,"+6 #...7 end
Justaswiththe 2 and else cases,theveryfirstcharacterisdifferent.Removethisdifferencebyinterpolating number inplaceofthehard-coded 1 inthe 1 case,asbelow:
Listing4.3:1andElse,1stPhrasesinProgress
1 when12 "#{number}bottleofbeeronthewall,"+3 #...4 else5 "#{number}bottlesofbeeronthewall,"+6 #...7 end
Asimilarchangewasmadeinthepreviouschapter,wherethehard-coded "2" wasreplacedby #{number} whencombiningthe2 and else cases.Theactofsubstitutingavariableforanexplicitnumberissominorthatitdoesn’tadequatelyreflecttheenormityoftheunderlyingidea,butstepbackandconsiderwhatjusthappened.Replacingdifferingconcretevalueswithareferencetoacommonvariablechangesdifferenceintosameness.
Thefactthattheargumentisknowntoequal 1 doesnotmatter.
Thissubstitutionisimportant,notbecauseitchangestheresultingvalue,butbecauseitincreasesthelevelofabstraction.Itisthisincreaseinabstractionthatmakesthingsthesame.Withoutit,youaredoomedtotheconditional.
Thenextdifferenceis"bottle"versus"bottles."This,conveniently,isthepreviouslyidentified"container"concept.Eachlineischangedtosendthe container message,whichresultsinthefollowingcode:
Listing4.4:1andElse,1stPhrasesIdentical
1 when12 "#{number}#{container(number)}ofbeeronthewall,"+3 #...4 else5 "#{number}#{container(number)}ofbeeronthewall,"+6 #...7 end
Thefirstphrasesarenowidentical.
Thesecondphraseofeachcaseisverysimilartothefirst,asyoucanseehere:
Listing4.5:1andElse,2ndPhrasesDiffer
1 when12 "#{number}#{container(number)}ofbeeronthewall,"+3 "1bottleofbeer.\n"+4 #...
5 else6 "#{number}#{container(number)}ofbeeronthewall,"+7 "#{number}bottlesofbeer.\n"+8 #...9 end
Thesecondphraseissosimilartothefirstthatrepeatingthesamechangewillmakethemidentical.Here’stheresult:
Listing4.6:1andElse,2ndPhrasesIdentical
1 when12 "#{number}#{container(number)}ofbeeronthewall,"+3 "#{number}#{container(number)}ofbeer.\n"+4 #...5 else6 "#{number}#{container(number)}ofbeeronthewall,"+7 "#{number}#{container(number)}ofbeer.\n"+8 #...9 end
Aftertheabovechanges,thefirsttwophrasesofthe 1 and elsecasesareidentical.
4.2.EquivocatingAboutNamesThename container feelsright.Itwasfairlyeasytofind,inpartbecausetheunderlyingconceptissoobvious.Onceyourealize
thatyou’retryingtonameacategorythatcontainsbottles,juiceboxes,andcarafes, container naturallyfollows.
However,whenconceptsarefuzzier,findingagoodnamecanbemuchharder.Thissectiondealswithjustsuchaconcept,andoffersseveralsuggestionsforwhattodowhenyoucan’tfindagoodname.
Nowthatphrasesoneandtwoarethesame,it’stimetoconsiderphrasethree.Here’sareminderofthatcode:
Listing4.7:1andElse,3rdPhrasesDiffer
1 when12 #...3 "Takeitdownandpassitaround,"+4 #...5 else6 #...7 "Takeonedownandpassitaround,"+8 #...9 end
Thedifferenceaboveisthat "it" matchesupwith "one" .
Ifallversevariantsarealikeinanunderlying,moreabstract,way,then"it"and"one"mustrepresentasmallerabstractionwithinthatlargerone.Onceyounamethisconcept,youcancreateamethodwiththatname,andthenmaketheselinesalikebysendingamessageinplaceofthedifferentstrings.
Ifthepreviousparagraphgaveyouasenseofdeja-vu,that’sunderstandable.Thisisexactlyhow"bottle"and"bottles"becamecontainer ,andhoweveryfuturedifferencewillberesolved.Theprocessmayseemtoostraightforwardtobelieve,butthe
mechanismtrulyisthishumble.Therulesofrefactoringaresimple,butwhenfollowed,preciseandcomplexbehavioremerges.
Thechallenge,asalways,isidentifyingthecurrentconceptandcomingupwithagoodname.Thewords "it" and "one" aresoinnatelygenericthatnamingtheunderlyingconceptisparticularlytough.Namesshouldneitherbetoogeneralnortoospecific.Forexample, thing istoobroad,and it_or_one toonarrow.
Ifyouweretoaskyourcustomertonamethiscategory,theywouldlikelyshrugandcallit pronoun .Ifyouobjectto pronoun onthegroundsthatit’soverlygeneral,andinsistthattheygivethecategoryamorespecificname,theymightcomeupwithsomethinglike thing_drunk .
Although pronoun doesfeelabittoogeneral, thing_drunk isjustaboutunbearable.Neitherfeelsperfect.Thissituation,unfortunately,isalltoocommon.Whentheperfectnameforaconceptiselusive,therearethreestrategiesformovingforward.
Somefolksallotthemselvesfivetotenminutestoponder(usuallywiththesaurusinhand),andthenusethebestnametheycancomeupwithduringthatinterval.Theirrationaleisthatthenametheychoosemightbegoodenough,andiftheylaterdiscoverit’snot,theycanalwaysimproveit.Thesefolkshavetheadvantageofworkingwithcodethatcontainsnamesthatareatleastsomewhatuseful,evenifnotentirelycorrect,butmustlivewiththepossibilitythatagood-enoughnamewillpersist,evenafterabetternamebecomesobvioustothehumansinvolved.
Otherfolksfinditmorecosteffectivetoinstantlychooseameaninglessnamelike foo or namethis .Thisstrategyallowsthemtomoveforwardquickly,and(onehopes)insuresthatthenamewillgetimprovedlater.Thesefolksbelievestronglyinthe"You’ll
neverknowlessthanyouknowrightnow"dictum,[10]andfullyexpectthatabetternamewilloccurastheyworkonthecode.Theybelievethere’snopointinwastingtimethinkingaboutitnow,whenthenamewillbeobviouslater.
Finally,insteadoffollowingoneoftwopreviousstrategiesbyyourself,youcansimplyasksomeoneelseforhelp.Withinanygroupofprogrammers,there’softensomeonewho’sgoodatnamingthings.Ifyourgrouphassuchaperson,youknowwhotheyare.Appointthemthe"nameguru,"andleveragetheirstrengthswhenyouneedaname.
Inthecaseof "it" or "one" herein99Bottles, pronoun isgoodenoughfornow.Ifsomethingbetteroccurslater,youcanalwaysimprovethename.
Theproceduretoturn "it" and "one" into pronoun isidenticaltotheonethattransformed"bottle"and"bottles"into container .Havingpreviouslypracticed,thisnextrefactoringwillgoquickly.Thefollowingexamplesstepthroughthetransitions.Remembertorunthetestsaftereachchange.
First,defineanempty pronoun method.
Listing4.8:EmptyPronounMethod
1 defpronoun2 end
Alter pronoun toreturn"one,"i.e.thevaluefromthe else branch.
Listing4.9:SparsePronounMethod
1 defpronoun2 "one"3 end
Alterthe else branchtosend pronoun inplaceof"one":
Listing4.10:SendPronouninElseBranch
1 when12 #...3 "Takeitdownandpassitaround,"+4 #...5 else6 #...7 "Take#{pronoun}downandpassitaround,"+8 #...9 end
Addadefaultedargumentto pronoun .
Listing4.11:PronounWithDefaultedArgument
1 defpronoun(number=:FIXME)2 "one"3 end
Alter pronoun tobeopentothe 1 case.
Listing4.12:PronounWithConditional
1 defpronoun(number=:FIXME)2 ifnumber==13 "it"4 else5 "one"6 end
7 end
Alterthe else casetopassthe number argumentto pronoun (line9).
Listing4.13:PassinganArgumenttoPronoun
1 defverse(number)2 #...3 when14 #...5 "Takeitdownandpassitaround,"+6 #...7 else8 #...9 "Take#{pronoun(number)}downandpassitaround,"+10 #...11 end12 end
Alterthe 1 casetosend pronoun(number) inplaceof"it"(line5).
Listing4.14:1andElseCasesSendPronoun
1 defverse(number)2 #...3 when14 #...5 "Take#{pronoun(number)}downandpassitaround,"+6 #...7 else
8 #...9 "Take#{pronoun(number)}downandpassitaround,"+10 #...11 end12 end
Alterthe pronoun methodtoremovethe :FIXME default:
Listing4.15:FinalPronounMethod
1 defpronoun(number)2 ifnumber==13 "it"4 else5 "one"6 end7 end
Therefactoringstepsthatadded pronoun wereexactlylikethoseusedtoadd container .Ineachcase,differingstringswerereplacedbyacommonmessagesend.The container abstractionreplacedthe"bottle"and"bottles"strings,andthe pronounabstractionreplaced"it"and"one."
Thiscompletestheadditionofthe pronoun method,andmakesphrasethreeofthe 1 and else casesidentical.It’stimetomoveontothefourthandfinalphrase.
4.3.DerivingNamesFromResponsibilitiesAlthough pronoun mayfeeltoogeneral,theconceptitrepresentsisclear.Ifyouhadtodescribetheunderlyingidea,youmightsaysomethinglike"The pronoun messagereturnsthewordthatis
usedinplaceofthenoun'bottles,'followingtheword'Take,'inphrase3ofeachverse."Pedanticasthatexplanationis,it’sentirelycorrect.Thatis pronoun 'sresponsibility.
Thedifficultynaming pronoun illustrateshowharditcanbetochooseaname,evenwhenyouunderstandtheconcept.Imagine,then,howimpossibleitistochooseanamewhenyoudon’t.Thisnextsectionaddressesthefourthandfinalphrase,andtakesonthechallengeofnamingaconceptthatismuchlessclear-cut.
Thefirstdifferenceinphrasefourlooksabit,well,different,butregardless,itcanberesolvedusingthetechniqueyou’vebeenusing.Thetricktogettingthisnextrefactoringrightistotrusttherules,andtowriteonlythecodethattheyrequire.
Here’salookatphrasefourofthe 1 and else cases:
Listing4.16:1andElse,4thPhrasesDiffer
1 when12 #...3 "nomorebottlesofbeeronthewall.\n"4 else5 #...6 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"7 end
Lookatthecodeaboveandidentifythedifferences.Itmighthelptofirstdecidewhatisnotadifference.Bothphasesendwith"ofbeeronthewall,"sothatpartisclearlythesame.Ifyoudisregardthatsameness,you’releftwith:
"nomorebottles"
whichmatchesupagainst:
Ifit’snotclearhowtoproceed,lookforawaytomakethelinesmorealike(evenifnotyetidentical),usingcodeyou’vealreadywritten.Rememberthatthegoalistolocatethenextsmalldifference,notthenextclumpofdifferences.
Noticetheword"bottles"inthe 1 case.Theabstractionthatunderlies"bottles"haslongsincebeenidentified.It’sencapsulatedinthe container method,whichisalreadybeingusedbythe else case.
If "bottles" isactuallythesameas container(number-1) ,thenthatpart(atleastlogically),isresolved.Thisleaves:
whichgoeswith:
Untilnow,thedifferencesbetweenphraseshavebothbeenstrings.Here,forthefirsttime,oneisastringandtheotherisinterpolatedcode.However,itdoesn’tmatterwhatformthedifferencetakes.Ifeachversevariantreflectsamoregeneralverseabstraction,thenthedifferencesbetweenthevariantsmustrepresentsmallerconceptswithinthatlargerabstraction.Again,youcanresolvethisdifferencebyfollowingthepatternyoulearnedfrom container and pronoun .Nametheconcept,createthemethod,andreplacethedifferencewithacommonmessagesend.
Tohelpyounamethenewconcept,rememberthe"whatwouldthecolumnheaderbe?"technique.Thefollowingtableshowsa
"#{number-1}#{container(number-1)}"
"nomore"
"#{number-1}"
samplingofnumbersandassociatedvalues:
Table4.1:NumbertoXXXColumnHeader
Number XXX?
99 '99'
50 '50'
1 '1'
0 'nomore'
Inthetableabove,theleftcolumncontainsanumberbetween99and0,andtherightholdsthestringtobesunginitsplace.Mosttimesthevalueontherightisthedirectstringrepresentationofthenumberontheleft,i.e. 99 becomes "99" , 50 , "50" ,etc.Theexceptionis 0 ,whichbecomes,not "0" asyoumightexpect,but"nomore" .
Phrasefouristhefinalphraseofthesongwherethenumbergetsdecremented,andsotheargumentisalways number-1 .It’stempting,therefore,tothinkof"nomore"and #{number-1} asrepresentingthenumberofbottlesthatremainonceaverseiscomplete.
Youcouldindeednamethisconcept"remainder,"andproceedwiththerefactoring.However,intheinterestofsavingabitofpain,takeabriefpeekforward.You’llsoonbeconsideringthe 0case,whichsays:
Noticethatthe 0 casestartswith "Nomore" ,justasthe 1 case
Nomorebottlesofbeeronthewall,nomorebottlesofbeer.Gotothestoreandbuysomemore,99bottlesofbeeronthewall.
endswith "nomore" .Thewaythesongworksisthatwheneverthereare 0 bottles,yousing"nomore,"capitalizedappropriately.
When "Nomore" comesatthebeginningofthesong,it’sclearlynottheremainder.Thismeansthatif "nomore" and "Nomore"representthesameidea,then remainder isn’tagoodnamefortheunderlyingconcept.
Ifyoureconsidertheabovetable,therightsideisactuallythename,ordescription,orperhapsquantityofbottlesbeingsungabout.Itisthestringtobesungintheplaceofanynumber.Whilenotperfect, quantity atleastattemptstoindicatetheresponsibilityonthemethodyouplantocreate,andsoisareasonablefirstattemptataname.
Beforeimplementing quantity ,considerwhatwouldhavehappenedhadyounamedthisconcept remainder .Afterfinishingthe 1 case,you’dhaveadvancedtothe 0 caseanddiscoveredthatitstartedwith "Nomore" .Thiswouldhavecausedyoutoreconsider remainder .You’dlikelyhaverevertedtherefactoringtothispoint,andre-startedyoursearchforaname.
Reallifeislikethis,whereyoumakethebestdecisionyoucaninthemoment,andreassesswhenyouknowmore.Hadyoubeendoingthisrefactoringalone,youmightwellhavegonedowntheremainder path,andsufferedtheeventualreversal.There’senoughpaininreallife;hereyou’vebeenlefttoimagineit.
Donottakethisasagenerallicensetothinkfarahead.Whileyouareallowedtousecommonsense,it’susuallybesttostayhorizontalandconcentrateonthecurrentgoal.Whencreatinganabstraction,firstdescribeitsresponsibilityasyouunderstanditatthismoment,thenchooseanamewhichreflectsthatresponsibility.Theeffortyouputintoselectinggoodnamesrightnowpaysoffbymakingiteasiertorecognizeperfectnameslater.
4.4.ChoosingMeaningfulDefaultsThepreviousfewrefactoringsusedthetechniqueoftemporarilysettinganargumenttoadefault.Ineachcase,thesymbol :FIXMEwasusedasthatdefault. :FIXME ishandybecausethenameitselfremindsyouofitstemporarynature,andactsasaremindertoremoveitattheendoftherefactoring.Helpfulas :FIXME is,however,itwon’tworkineverycase.Sometimescircumstancesconspiretoforceyoutousearealvalueasadefaultduringtheserefactorings.Thisnextsectiondelvesintojustsuchacase.
Rememberthatthedifferencecurrentlybeingaddressedis:
whichgoeswith:
Theunderlyingconceptis quantity .Toremovethisdifference,firstaddthe quantity method:
Listing4.17:InitialQuantityMethod
1 defquantity2 end
Thenextstepistochangethismethodtoreturnoneofthetwodifferences.Untilnow,you’vechosentoreturnthevaluefromthe else branchfirst.Butinthiscase,the else branchcontainsinterpolatedcodethatreferences number .Therefore,youcan’tcopythe else branchdifferenceinto quantity unlessyoufirstalter quantity totake number asanargument.
The 1 branchcontainsthestring"nomore,"whichisasimplerdifference.Thatsimplicitymakesthisagoodplacetoexplore
"nomore"
"#{number-1}"
whathappensifyouswitchupandreturnthenon-elsevaluefirst.
Becauseofthischangeintactics,proceedingexactlyasyou’vedonepreviouslywilleventuallyleadtoanerror.It’sinstructivetowatchthishappen,asshowninthefollowingcode.
Beginbyreturningthevaluefromthe 1 case:
Listing4.18:QuantityMethodFirstReturn
1 defquantity2 "nomore"3 end
Send quantity inplaceof "nomore" inthe 1 case:
Listing4.19:QuantityMessageFirstSend
1 when12 #...3 "#{quantity}bottlesofbeeronthewall.\n"4 else
Addthenormal :FIXME defaulttothe number argumentinquantity :
Listing4.20:NumberArgumentDefaultedtoFIXME
Ifyou’reconcernedaboutthe :FIXME defaultabove,yourSpidey-sense[11]isworking.Yes,everythingwillgoterriblywrongina
1 defquantity(number=:FIXME)2 "nomore"3 end
minute,butuntilthen,castyourworriesasideandchargeforward.
Thenextstepistoalter quantity tobeopentothe else case.Rememberthatyou’reworkingonthefinalphraseofverse1,andthatthevalueofthepassedargumentwillbe number-1 ,or 0 .If number is 0 ,theconditionshouldreturn "nomore" ;otherwise,itshouldreturnthenumber.
Here’sthe quantity method,alteredtocontainthatnewconditional:
Listing4.21:QuantityMessageWithConditional
1 defquantity(number=:FIXME)2 ifnumber==03 "nomore"4 else5 number6 end7 end
Ifyounowhaveadditionalconcernsaboutthiscode,hanginthere.Anumberoferrorswillarise,buttheywillsoongetresolved.
Atthispointineachofthepreviousrefactorings,thetestspassedbutinthiscase,notso.Thetestsarenowfailingwith:
Havealookatthe case statementbelow.Examineline3andtry
-Takeitdownandpassitaround,nomorebottlesofbeeronthewall.+Takeitdownandpassitaround,FIXMEbottlesofbeeronthewall.
toexplainwhatwentwrong.
Listing4.22:UsingtheNumberDefaultFromthe1Case
1 when12 #...3 "#{quantity}bottlesofbeeronthewall.\n"4 else5 #...6 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"
Thisfailureoccursbecauseline3abovecalls quantity withoutpassinganargument.Uponinvocation,the quantity methodsetsnumber to :FIXME ,whichsendsexecutiontothefalsebranchofitsconditional.Thefalsebranchobedientlyreturns number ,whichunfortunatelystillcontains :FIXME .Thisresultthengetsinterpolatedbackintotheverse.Thus, "FIXMEbottlesofbeer" .
Thereasonthe :FIXME defaultworkedinprevioussituationswasbecauseinthosecasesyouwantedtoexecutethefalsebranch.However,nowyouneedthetruebranch,andthereforerequireamuchmorespecificdefault.
Thetestsarefailing,andtherulesdictatethatyoumustundoandreturntogreen.Fortunately,thistakesjustoneundo,whichreverts quantity tothefollowing:
Listing4.23:NumberArgumentDefaultedtoFIXMEReprise
1 defquantity(number=:FIXME)2 "nomore"3 end
Anobviouslywrongandtemporaryvaluelike :FIXME canbeahandydefault,butyoucanonlyusethistechniqueifyoubegintheserefactoringsbyreturningthedifferencefromthe elsebranch.Whileit’sperfectlyacceptabletobeginbyreturning"nomore"(thenon-elsedifference),doingsomeansthatyouhavetothinkmorecarefullyaboutthedefault.So,useadefaultlike:FIXME thoughtfully.
Inthiscase,thedefaultthatwilldriveexecutiontothecorrectbranchis 0 ,asshownbelow:
Listing4.24:NumberArgumentDefaultsto0
Nowthatthedefaultiscorrect,theconditionalcanbere-addedtoquantity ,asfollows:
Listing4.25:DefaultTakestheTrueBranch
1 defquantity(number=0)2 ifnumber==03 "nomore"4 else5 number6 end7 end
Althoughnothingabouttheconditionalhaschangedsincethelastattempt,thedefaultisnowcorrect,sothetestspass.
Takingthedefaultcausedthe true branchtoexecute.Nowit’stimetoensurethatpassinganargumentdoesthesame.Line5belowhasbeenchangedtopass number-1 to quantity :
1 defquantity(number=0)2 "nomore"3 end
Listing4.26:1CasePassesanArgument
1 defverse(number)2 #...3 when14 #...5 "#{quantity(number-1)}bottlesofbeeronthewall.\n"6 else7 #...8 "#{number-1}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Thetestsstillpass.Thenextstepistouse quantity inthe elsecase,asshownonline8below:
Listing4.27:ElseCaseSendsQuantity
1 defverse(number)2 #...3 when14 #...5 "#{quantity(number-1)}bottlesofbeeronthewall.\n"6 else7 #...8 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Atthispoint quantity isfullyimplemented.Thedefaultisnolongerneeded,andcanberemoved.Thefinalmethodisshownbelow:
Listing4.28:QuantityMethod
1 defquantity(number)2 ifnumber==03 "nomore"4 else5 number6 end7 end
Afterresolving quantity ,oneminordifferenceremainsbetweenthe 1 and else cases.Thefinalphraseofthe 1 casesays"bottles" (line4below)whereasinthatplacethe else casesendscontainer(number-1) .
Listing4.29:1andElseCasesMoreAlike
1 defverse(number)2 #...3 when14 "#{quantity(number-1)}bottlesofbeeronthewall.\n"5 else6 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"7 end8 end
Thisdifferencecanberesolvedbysendingthewell-knowncontainer messageinplaceoftheword "bottles" .Afterthis
change,the 1 and else casesareidentical,asshownintheirfullglorybelow:
Listing4.30:1andElseCasesIdentical
1 defverse(number)2 #...3 when14 "#{number}#{container(number)}ofbeeronthewall,"+5 "#{number}#{container(number)}ofbeer.\n"+6 "Take#{pronoun(number)}downandpassitaround,"+7 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"8 else9 "#{number}#{container(number)}ofbeeronthewall,"+10 "#{number}#{container(number)}ofbeer.\n"+11 "Take#{pronoun(number)}downandpassitaround,"+12 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"13 end14 end
Thiscompletelyresolvesthe 1 case,whichcannowbedeleted.
Twonewconceptshavebeenidentified, pronoun and quantity .Althoughtherefactoringthatcreated quantity obedientlyfollows
theFlockingRules,theorderinwhichcodeiswrittendiffersslightlyfromthatofpreviousmethodextractions.Theearlierexamplesbeganbyreturningthevaluefromthe else branchofthecasestatement,butthe quantity methoddiffersinthatitinitiallyreturnsthevaluefromthe 1 ,ornon- else case.
Alloftheserefactoringsextractamethod.Becausethisisdoneinverysmallsteps,theextractedmethodsstartoutsimpleandthengraduallybecomemorecomplicated.Oneofthecomplicationsisthateachmethodchangestotakeaparameter.Inordertokeepthetestsrunninggreenduringthetransitiontotakingaparameter,theparameterhastobeassignedadefault.Thedefaultistemporary,anditismeanttobedeletedwhenthetransitioniscomplete.
Whenthe else branchisimplementedfirst, :FIXME canalwaysbeusedforthedefault.Thisnotonlysavesyoufromhavingtofigureouttherightvalue,italsoservesasaremindertoremovethistemporarydefaultlater.Ifthenon- else branchisimplementedfirst,thedefaulthastobesettosomethingthatactuallymeetstheconditionandsomakesthe true branchexecute.Therefore,implementingthenon- else branchfirstplacesaslightlygreaterburdenonyou.Youhavetouseaspecific,real,valueforthedefault,andthenmustremembertoremovethedefaultoncethetransitioniscomplete.
4.5.SeekingStableLandingPointsAtthispoint,the 2 and 1 caseshavebeenremoved,andthreenewconcepts, quantity , pronoun and container ,havebeenidentified.Tosaveyoufromhavingtoremember,thelistingbelowrepeatsthecodefortheseconcepts:
Listing4.31:ThreeAbstractedConcepts
1 defquantity(number)2 ifnumber==03 "nomore"4 else5 number6 end7 end8 9 defpronoun(number)10 ifnumber==111 "it"12 else13 "one"14 end15 end16 17 defcontainer(number)18 ifnumber==119 "bottle"20 else21 "bottles"22 end
23 end
Noticethesimilaritiesintheabovemethods.Eachhasasingleresponsibility.Theyareidenticalinshape.Alltakethesameargument.Eachcontainsaconditionalandthatconditionalteststheargumentagainstaspecificvalue;itcheckstoseeiftheargumentisequaltosomething,asopposedtogreaterorlessthansomething.
Thesemethodsareincrediblyconsistent,andthisdidnothappenbyaccident--they’readirectresultoftherefactoringrules.Therulesleadtoconsistentcode,andconsistencymattersdeeply.First,itmakescodeeasytounderstand.Codeisreadmanymoretimesthanitiswritten,soanythingthatincreasesunderstandabilitylowerscosts.Next,andjustasimportant,consistentcodeenablesfuturerefactorings.
Imagineyourselfachild,traipsingdownastream,hoppingfromrocktorock.Somerocksarebroadandflatanddry,othersaremossyandwobblyandslick.Imaginealsothatyouarenotallowedtoreturnhomewet.
Thedryrocksarestablelandingpointsonwhichyoucansafelyrest,planningyournextmove.Thewetrocksareriskyinterludesthatgoodsensesuggestsyoutraverseasquicklyaspossible.
Rearrangingcodeislikerockhoppingdownastream.Ifyoufollowtherulesofrefactoring,you’llquicklypassovertheslipperyplaces,andarriveatstable,consistentrestingpoints.Changingcodewilly-nilly,however,canleadtosurprisingandunexpectedbaths.
Theconsistencyinthecodeaboveenablesthenextrefactoring.Fornowyoumusttakethisassertiononfaith,butthatfaithwillberewardedinfuturechapters.
4.6.ObeyingtheLiskovSubstitutionPrincipleNow,backtothehorizontalrefactoring.Thischapterstartedwithathree-branchcasestatement.Onecase(the 1 case)hasbeenremoved,leavingthe 0 and else casesstilltoberesolved.Here’sareminderofthecurrentstateofthecode:
Listing4.32:0andElseCasesDiffer
1 defverse(number)2 casenumber3 when04 "Nomorebottlesofbeeronthewall,"+5 "nomorebottlesofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 else9 "#{number}#{container(number)}ofbeeronthewall,"+10 "#{number}#{container(number)}ofbeer.\n"+11 "Take#{pronoun(number)}downandpassitaround,"+12 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"13 end14 end
Beginthisnextrefactoringbyfocusingonlines4and9above,thefirstphrasesofthetworemainingcases.Lookingforthesmallestdifference,bothlinesendwith "ofbeeronthewall," ,sothisisasimilaritythatcanbeignored.The container methodisusedon
line9inthe else case.Thismethodcanbesubstitutedonline4fortheword "bottles"` ,andneedsnofurtherconsideration.
Theremainingdifferenceisattheverybeginningoflines4and9,where:
goeswith:
Thisfeelslikethe quantity concept,butasitstands,thatmethodwon’tworktoresolvethisdifference.Ifyouweretochangeline4tosend "#{quantity(number)}" inplaceof "Nomore" ,you’dgetbackanalllowercase"nomore,"andthetestswouldfail.
Thisisaconundrum.Thelowercasevariantof"nomore"isrequiredbyverse1,andnowverse0needsthesametwowords,exceptcapitalizedasthestartofasentence.Theunderlyingconceptisthesameinbothcases("nomore"istobesungwhenthenumberofbottlesis0),butitgetsexpressedinslightlydifferentways,dependingonwhereitfallsinthesong.
Thesewordsareonething,andwhethertheyneedtobecapitalizedisquiteanother.Perhapsknowledgeofthewordsbelongsinoneplace,andknowledgeofthecapitalizationrequirementsbelongsinanother.
Ifthat’sthecase,capitalizationcanreasonablyhappenhereinthe case statement.Replace "Nomore" with "#{quantity(number)}" ,andcapitalizetheresult,asonline4below:
Listing4.33:QuantityCapitalizedin0Case
1 defverse(number)
"Nomore"
"#{number}"
2 casenumber3 when04 "#{quantity(number).capitalize}bottlesofbeeronthewall,"+5 #...6 else7 "#{number}#{container(number)}ofbeeronthewall,"+8 #...9 end10 end
Theabovechangefollowsthestrategyofgraduallymakingthingsmorealikeinhopesthatitwillthenbecomeclearhowtomakethemidentical.Whennibblingawayattheproblem,youdon’thavetounderstandeverythingbeforeyoucandoanything.Takingcareofthesmallthingsoftencutsthebigonesdowntosize.
Havingmadetheabovechange,itnowseemsreasonabletomakeasimilaroneinthe else case,shownonline7below:
Listing4.34:QuantityCapitalizedinElseCase
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}bottlesofbeeronthewall,"+5 #...6 else7 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+
8 #...9 end10 end
Despiteseemingreasonable,thechangemakesthetestsfailwith:
Becauseyou’reworkinginsuchsmallsteps,youknowthatthepreviouschangecausedthiserror.Havealookatthefollowingcodeandseeifyoucanfigureoutwhat’swrong:
Listing4.35:QuantityMethodReprise
1 defquantity(number)2 ifnumber==03 "nomore"4 else5 number6 end7 end
Themostrecentchangeinvokes quantity withannon-zeroargument.Thiscausesexecutiontoproceedtothefalsebranch.Thetruebranchreturnsastring,butthefalsebranchreturnstheargumentthatwaspassed,whichisindeedaninstanceof Fixnum .String understands capitalize ,but Fixnum doesnot;thusthiserror.
Youmaybeitchingtofixthiserrorbymakingachangeinthequantity method,butit’sinstructivetotryattackingithereinverse .Goaheadandremovetheerrorbyconvertingtheresultintoastringbeforesending capitalize .Line7belowinserts to_sintothemethodchain:
NoMethodError:undefinedmethod`capitalize'for99:Fixnum
Listing4.36:ElseBranchConvertsResult
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}bottlesofbeeronthewall,"+5 #...6 else7 "#{quantity(number).to_s.capitalize}#{container(number)}ofbeeronthewall,"+8 #...9 end10 end
Theabovechangefixesthefailingtest,butintroducesanewdifferencebetweenthephrases.Toremovethisdifference,youmustalsoinsert to_s intothe 0 case,asonline4below:
Listing4.37:BothBranchesConvertResult
1 defverse(number)2 casenumber3 when04 "#{quantity(number).to_s.capitalize}bottlesofbeeronthewall,"+5 #...6 else7 "#{quantity(number).to_s.capitalize}#{container(number)}ofbeeronthewall,"+8 #...9 end10 end
Nowthatthedifferenceisresolvedandthetestsarerunning,stepbackandconsiderthissolution.Therootoftheproblemisthat quantity returnsthingsthatconformtodifferentAPIs.Sendersof quantity expectthereturntounderstand capitalize ,yet quantity doesn’talwaysoblige;itsometimesreturnsa"capitalizable,"butothertimesdoesnot.Thisinconsistencyofreturntypesforcesthesenderofthemessagetoknowmorethanitshould.
The verse methodaboveknowsthatitcannottrust quantity toreturnsomethingthatunderstands capitalize .The verse methodknowsthatinstancesof String dounderstand capitalize .Itknowsthatanyobjectcanbeconvertedtoastringbysendingto_s .Therefore,itknowsthatitcanconvertanyobjectintosomethingthatunderstands capitalize bysendingitthe to_smessage.
Everypieceofknowledgeisadependency,andthewaythatquantity iswrittenrequires verse toknowtoomanythings.Ifquantity weremoretrustworthy, verse couldknowless.
TheideaofreducingthenumberofdependenciesimposeduponmessagesendersbyrequiringthatreceiversreturntrustworthyobjectsisageneralizationoftheLiskovSubstitutionPrinciple.TheofficialdefinitionofLiskovsaysthat"subtypesmustbesubstitutablefortheirsupertypes."Thisprinciplewasoriginallypostulatedintermsoftypesandsubtypes,butyoucanthinkofitintermsofclassesandsubclasses.
Liskov,inplainterms,requiresthatobjectsbewhattheypromisetheyare.Whenusinginheritance,youmustbeabletofreelysubstituteaninstanceofasubclassforaninstanceofitssuperclass.Subclasses,bydefinition,areallthattheirsuperclassesare,plusmore,sothissubstitutionshouldalwayswork.
TheLiskovSubstitutionPrinciplealsoappliestoducktypes.Whenrelyingonducktypes,everyobjectthatassertsthatitplaystheduck’srolemustcompletelyimplementtheduck’sAPI.Ducktypesshouldbesubstitutableforoneanother.
Liskovprohibitsyoufromdoinganythingthatwouldforcethesenderofamessagetotestthereturnedresultinordertoknowhowtobehave.Receivershaveacontractwithsenders,anddespitetheimplicitnatureofthiscontractindynamicallytyped,object-orientedlanguages,itmustbefulfilled.
Liskovviolationsforcemessagesenderstohaveknowledgeofthevariousreturntypes,andtoeithertreatthemdifferently,orconvertthemintosomethingconsistent.Inthe quantity methodabove,oneofthereturnshonoredthe"capitalizable"contractandonedidnot.Aninconsistencylikethisveryoftenforcesthesendertoimplementaconditionaltoidentifyandfixtheerrantreturn.Inthiscase,allRubyobjectsunderstand to_s ,soitwasprogrammaticallyconvenienttoblithelyconverteveryreturnintoastring,eventhosethatalreadywere.Thisunconditionalconversionavoidscheckingtoseewhichobjectsneedtobesentto_s ,butaddstheoverheadofsending to_s toeveryobject,evenifit’salreadyastring.
Thesender’sentireburdenisremovedifthereceiverhonorsthecontract,andprovidesaconsistentreturn.Insteadofforcingtheverse methodtosolvethisproblem, quantity shouldreturnatrustworthyobject.
Thisiseasilyaccomplishedbydoingtheconversioninthequantity method,asshownonline5below:
Listing4.38:QuantityObeysLiskov
1 defquantity(number)2 ifnumber==0
3 "nomore"4 else5 number.to_s6 end7 end
Nowthat quantity alwaysreturnsa"capitalizable,"youcanpretendthatthe to_s dependencyneverexistedinverse,whichreturnsthecodetothestateshownhere:
Listing4.39:VerseTrustsQuantity
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}bottlesofbeeronthewall,"+5 #...6 else7 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+8 #...9 end
Havingaltered quantity tomakeitusableinallcases,theremainingdifferenceinthefirstphraseistheword"bottles."Thisiseasilyresolvedbysending container initsplace:
Listing4.40:0CaseSendsContainer
1 defverse(number)2 casenumber3 when0
4 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+5 #...6 else7 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+8 #...9 end10 end
Afterthatchange,thefirstphrasesofthe 0 and else casesareidentical.
4.7.TakingBiggerStepsYou’venowturnedsmalldifferencesintomessagesendsseveraltimes,andhavelikelynoticedthesimilaritybetweenthestepstakenandtheresultingcode.Sofar,theextractedmethodsallhavethesamegeneralshape,andareinvokedinthesameway.
Differencesremain.However,it’sbeginningtofeellikethere’sacommonrefactoringpattern,andonemightreasonablytheorizethatfuturedifferenceswillberesolvedfollowingthesameprocessthatwasusedinthepast.Ifthistheoryiscorrect,itmakessensetospeedupthenextrefactoringbycombiningseveralstepsintoasinglechange.
Thefirstphraseofthe 0 and else casesareidentical,soit’stimetoexaminethesecond.It’srepeatedbelow:
Listing4.41:0andElse,2ndPhrasesDiffer
1 defverse(number)2 casenumber
3 when04 #...5 "nomorebottlesofbeer.\n"+6 #...7 else8 #...9 "#{number}#{container(number)}ofbeer.\n"+10 #...11 end12 end
Theabovedifferencesreflectthe quantity and containerconcepts,whichhavelongsincebeenidentified.Resolvethembychangingthecodeasfollows:
Listing4.42:2ndPhrasesSendQuantityandContainer
1 defverse(number)2 casenumber3 when04 #...5 "#{quantity(number)}#{container(number)}ofbeer.\n"+6 #...7 else8 #...9 "#{quantity(number)}#{container(number)}ofbeer.\n"+10 end11 end
Nowthatphrases1and2areidentical,here’salookatthewholeverse method.Considerthecode,andidentifythenextdifference:
Listing4.43:Phrases1and2AreIdentical
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+5 "#{quantity(number)}#{container(number)}ofbeer.\n"+6 "Gotothestoreandbuysomemore,"+7 "99bottlesofbeeronthewall.\n"8 else9 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+10 "#{quantity(number)}#{container(number)}ofbeer.\n"+11 "Take#{pronoun(number)}downandpassitaround,"+12 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"13 end14 end
Tolocatethenextdifference,itcanagainbehelpfultoscantheversefromtheend.Bothvariantsendwith"ofbeeronthewall."Online7,phrase4ofcase 0 beginswith "99" followedby"bottles" .Theseseemtogowith quantity and container online12.Ignorethisfourthphrasefornowandturnyourthoughtstophrase3,isolatedbelow:
Listing4.44:0andElse,3rdPhrasesDiffer
1 defverse(number)2 casenumber3 when04 #...5 "Gotothestoreandbuysomemore,"+6 #...7 else8 #...9 "Take#{pronoun(number)}downandpassitaround,"+10 #...11 end12 end
Theonlythingtheabovelineshaveincommonisthetrailing "," ,whichmeansthateverythinguptothatpointisadifference.Ifthe 0 and else versevariantsreflectacommonverseabstraction,thisdifferencemustrepresentasmallerconceptwithinthatlargerabstraction.Itdoesn’tmatterhowlongthesestringsare,theirpresencehereinoppositionmeanstheyreflectasingleconcept.
Youmustnametheconcept,createamethodtorepresentit,andthenreplacethisdifferencewithamessagesend.Thefirststepisthereforetonamethecategoryinwhichthesetwophraseareconcreteexamples.
Thispartofthesongisaboutwhathappensasaresultofthecurrentnumberofbeers.Ifbeersexist,youdrinkone.Ifnot,yougoshopping.Theselinesdescribetheactiontotake,sothat’sagoodnameforthisconcept.
Untilnow,you’vebeendoingthisrefactoringinthesmallestpossiblesteps.Asareminder,thosestepsare:
Defineamethodfortheconcept.
Alterittoreturnoneofthedifferences.
Replacethatdifferencewithamessagesend.
Addthe number argumenttothenewmethod,withappropriatedefault.
Implementtheconditional.
Passthe number argumentfromthecurrentsender.
Sendthemessagefromtheotherbranch,thistimeincludingthe number argument.
Cleanup.
Youmayhavenoticedthatthemethodyoucreateduringthisrefactoringcontainscodethatexactlymirrorstheshapeoftheoriginalcasestatement.Oncethisbecomesapparent,itmakessensetobeginextractingthemethodinasinglestep,asshownbelow:
Listing4.45:LeapIntoAction
1 defaction(number)2 ifnumber==03 "Gotothestoreandbuysomemore"4 else5 "Take#{pronoun(number)}downandpassitaround"6 end
7 end
Thisnew action methodcontainsaconditionalthatreflectsthecasestatementfromwhenceitcame.Justastheoriginal casestatementswitchedon number ,thenew action methodtakesanumber argument,andusesitsvaluetochoosewhattoreturn.Thetrue and false branchesofthenewconditionalcontaincodeextracteddirectlyfromthe 0 and else branchesofthe casestatement.
Once action exists,theoriginalphrasescanbemadeidenticalbyreplacingtheirdifferenceswithacommonmessagesend.Thisresultsinthefollowingcode:
Listing4.46:3rdPhrasesSendAction
1 defverse(number)2 casenumber3 when04 #...5 "#{action(number)},"+6 #...7 else8 #...9 "#{action(number)},"+10 #...11 end
Thepreviouschaptershowedanexamplewheretheentirecontainermethodwascreatedatonce.Thatwasheldupasanexampleofwhatnottodo.The action methodabovelooksalotlikethatoriginal container method,anditmayseemasifyouarenowbeinggivenpermissiontoactinawaythatwaspreviouslyprohibited.
However,thereisadifference.Backwhentheoriginal containermethodwasfirstintroduced,youhadnotyetlearnedhowtocreateitusingsmallsteps.Sincethattime,you’vepracticedtheFlockingRules,refactoringbitbybit,andonseveraloccasionshaveseendifferencesfromtwobranchesofthecasestatementturnintoasingleconditional.Nowthatyouknowhowtomakethischangeusingsmallsteps,andhaveseenthispattern,itmakessensetostartwritinglargerchunksofcode.
However,ifyoutakebiggerstepsandthetestsbegintofail,there’ssomethingabouttheproblemthatyoudon’tunderstand.Ifthishappens,don’tpushforwardandrefactorunderred.Undo,returntogreen,andmakeincrementalchangesuntilyouregainclarity.
4.8.DiscoveringDeeperAbstractionsSofarthe container , pronoun , quantity ,and action conceptshavebeenidentified,andmethodshavebeenextractedtoberesponsibleforeach.Thishorizontalrefactoringtoremovethecasestatementisalmostcomplete.Thisnextsectionresolvesthefinaldifference,andinsodoingillustratesthedeeppoweroftheFlockingRulestounearthunanticipatedabstractions.
Theremainingdifferencesareinthefourthphrasesofthe 0 andelse cases,onlines7and12below.Here’sthecurrentstateofthecode:
Listing4.47:Phrases1,2,and3AreIdentical
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+5 "#{quantity(number)}#
{container(number)}ofbeer.\n"+6 "#{action(number)},"+7 "99bottlesofbeeronthewall.\n"8 else9 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+10 "#{quantity(number)}#{container(number)}ofbeer.\n"+11 "#{action(number)},"+12 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"13 end
Thetrailing "ofbeeronthewall" inthelinesaboveisasameness,andtheword "bottles" inline7isanexampleofthe containerabstraction,whichisalreadyusedinthisplaceinline12.Ifyouignorethesefornow,theremainingdifferenceisthat:
seemstobesetagainst:
Thismayleadyoutoconcludethat "99" isathirdexampleofthequantity abstraction.Ifso,thenyoushouldalter quantity tosometimesreturn "99" .Theresultingmethodwouldlooklikethis:
Listing4.48:QuantityOverreachestoHandle99
"99"
"#{quantity(number-1)}"
1 defquantity(number)2 casenumber3 when-1
Ifyoumadethealterationshownabove,andthenreplaced "99"with "#{container(number-1)}" ,thetestswouldcontinuetopass.However,justbecausethetestspassdoesn’tmeanthattheabstractioniscorrect.There’ssomethingdeeplywrongwiththissolution,andtherearemanycluestotheproblem.
Thefirstclueisthattheabovechangegives quantity adifferentshapethanthatoftheotherextractedmethods.Here’sareminderofhowthemethodslookedbeforethisalteration:
Listing4.49:ConsistentAbstractions
1 defaction(number)2 ifnumber==03 "Gotothestoreandbuysomemore"4 else5 "Take#{pronoun(number)}downandpassitaround"6 end7 end8 9 defquantity(number)10 ifnumber==011 "nomore"12 else13 number.to_s14 end
4 "99"5 when06 "nomore"7 else8 number.to_s9 end10 end
15 end16 17 defpronoun(number)18 ifnumber==119 "it"20 else21 "one"22 end23 end24 25 defcontainer(number)26 ifnumber==127 "bottle"28 else29 "bottles"30 end31 end
Theproposedchangealters quantity suchthat:
itsconditionalhas3branchesinsteadof2
itsometimeschecks -1 ,whichisaninvalidnumberofbeers
Theseinconsistenciesdon’tguaranteethatsomethingiswrong,buttheyshouldcertainlymotivateyoutothinkmoredeeplyabouttheunderlyingabstraction.
Askyourselfthesetwoquestions:
1. Whatistheresponsibilityofthe quantity method?
2. Isthereawaytomakethefourthphrasesmorealike,evenif
notyetidentical?
First,considerresponsibilities.The quantity conceptisresponsibleforknowingwhattosingintheplaceofanumber.Ifthereare 50 beers,thequantityis "50" ,if 5 beers, "5" ,andif 0beers, "nomore" .Thisconceptrepresentsthemappingbetweenthevalueofanumberandthestringthatgetssung.
Asthesongprogresses,theversenumbergetsdecremented.It’sbeenawhilesinceyou’veseenthem,sohere’sareminderofthesong and verses methods:
Listing4.50:SongandVersesReprise
1 defsong2 verses(99,0)3 end4 5 defverses(starting,ending)6 starting.downto(ending).collect{|i|verse(i)}.join("\n")7 end
Line2aboveencodestheknowledgethattheoverallsongstartsonverse99andcountsdownto0.Line6decrementstheversenumber,whichmovesthesongfromoneversetothenext.Butifyouarefamiliarwith99Bottles,youaresurelyawarethatthesongislongerthanthiscodesuggests.Therealsonggoesonforever(oratleastuntilallsingersbecomesufficientlybored).
This"forever"happensinphrase4ofthe 0 case,repeatedbelow:
Listing4.51:Case0HandlesRestart
1 defverse(number)
2 casenumber3 when04 #...5 "99bottlesofbeeronthewall.\n"6 else7 #...8 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Line5abovecontainsahard-coded 99 .Thisisnotaspecialcaseofthe quantity concept,whichistheruleforwhattosinginplaceofanumber.
There’ssomethingsubtleaboutthedifferenceabove,suchthattheunderlyingconceptisnotimmediatelyobvious.Andthis,unfortunately,isaconstantofprogramminglife.Ifyouhadperfectunderstanding,you’dwriteperfectapplications.Mostly,however,you’restumblingaround,sufferingfrominsufficientinformation,seeingproblemsthroughaglass,darkly.[12]
Whenyou’reconfused,don’ttrytosolvetheentireproblemstraightaway.Themoreconfusedyouare,themoreimportantitistonibble.Youalreadyknowthatitbecomeseasiertoseehowthingsaredifferentifyoumakethemmorealike.Insteadoftryingtounderstandeverythingatonce,simplysearchforawaytomakeline5abovelookmorelikeline8(evenifnotidentical),usingexistingcode.
Itmayhelptoconsiderthesequestions.Whenthevalueof numberis 5 ,whatdoes quantity return?Howaboutwhennumberis 95?Andfinally,whatwould quantity returnifyoupassedin 99?
Ifyoujustrealizedthatyoucanmaketheselinesalittlebitmorealikebypassingthe 99 into quantity ,you’vegotit.Here’stheresultingcode:
Listing4.52:99IsaQuantity
1 defverse(number)2 casenumber3 when04 #...5 "#{quantity(99)}bottlesofbeeronthewall.\n"6 else7 #...8 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Asyoucanseefromtheabove,theexisting quantity ruleisfine,anditalreadyapplies.Whenthenumber99appearsinthesong,youshouldsingthestring"99."
Atthispointitmakessensetoscanovertotheword"bottles"andreplaceitwiththe container method.Thisisawell-understooddifference,andtakingitoffthetablenowreducesmentalclutter.Here’stheresultingcode:
Listing4.53:Case0SendsContainer
1 defverse(number)2 casenumber3 when04 #...
5 "#{quantity(99)}#{container(number-1)}ofbeeronthewall.\n"6 else7 #...8 "#{quantity(number-1)}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Havingmadetheselinesassimilaraspossible,itisnowobviousthat:
mustrepresentthesameconceptas:
Asalways,youmustnamethisconcept,createamethod,andsendthemessageinplaceofthedifference.
Thisconceptisaboutknowingthatwhen number is 50 ,theresultis 49 ,when 5 , 4 ,when 1 , 0 ,andwhen 0 , 99 .It’swherethesongdeterminesthenextversetobesung."Next"isakeywordinRuby,soitcan’tbeusedasthename,butaconceptlikethisalreadyexistsformanyRubyobjects,anditmakessensetoleverageRuby’sexistingname.
SeveralclassesintheRubystandardlibrarydefinea"successor,"usingtheunfortunatelynamed succ method.Hereareafewexamples:
"99"
number-1
"a".succ#=>"b"9.succ#=>10
Mostversesaresucceededbythenextlowerverse,withtheexceptionofverse0,whichisfollowedbyverse99."Successor"isagoodnameforthisconcept.
The successor conceptwasunearthedusingthesamerefactoringrulesthatledto container , pronoun , quantity ,and action .Asthisideaisabitmoreabstractthantheothers,anabundanceofcautionsuggeststhattherefactoringbedoneinmoderatelysmallsteps.Inthatspirit,firstcreatethemethod,andhaveitreturntheelse branchdifference.Here’sthatcode:
Listing4.54:SuccessorHandlesDefault
1 defsuccessor(number)2 number-13 end
Thecodein successor refersto number ,sotheargumentmustbedefinedfromthefirst.
Nowthat successor exists,useitinthe else branchinplaceofnumber-1 (line8below):
Listing4.55:ElseCaseSendsSuccessor
1 defverse(number)2 casenumber3 when04 #...5 "#{quantity(99)}#{container(number-1)}ofbeeronthewall.\n"6 else7 #...8 "#{quantity(successor(number))}#{container(number-1)}ofbeeronthewall.\n"
9 end10 end
Thenextstepistomakethesuccessoropentobeingusedinthe0 case,byaddingaconditionaltoreturnthecorrectvalue:
Listing4.56:SuccessorHandlesBothCases
1 defsuccessor(number)2 ifnumber==03 994 else5 number-16 end7 end
Nowthattheconditionalexists,the 99 canbereplacedbyasendof successor ,asshownonline5below:
Listing4.57:BothCasesSendSuccessor
1 defverse(number)2 casenumber3 when04 #...5 "#{quantity(successor(number))}#{container(number-1)}ofbeeronthewall.\n"6 else7 #...8 "#{quantity(successor(number))}#{container(number-1)}ofbeeronthewall.\n"9 end10 end
Afterthischange,the 0 and else casesareidentical.
The successor conceptillustratesthepowerofiterativeapplicationoftheFlockingRules.Thisconceptwasn’tevenhintedatinthesolutionsgiveninChapter1,andifyoufounditwhenyouworkedtheproblemyourself,you’reinaminority.Theconceptissosubtlemostprogrammersdon’tnoticeit,andyetitsimplyappearsifyoufollowthissimplesetofrules.
Successor isimportant,andseparatingitfrom quantity givesbothmethodsasingleresponsibility.Ifyouconflatechoosing-what-to-sing-for-any-number( quantity )withdeciding-what-verse-to-sing-next( successor ),theresultingmethodwouldbehardertounderstand,futurerefactoringswouldbemoredifficult,andattemptstochangethecodeforoneideamightaccidentallybreakitfortheother.
4.9.DependingonAbstractionsAbstractionsarebeneficialinmanyways.Theyconsolidatecodeintoasingleplacesothatitcanbechangedwithease.Theynamethisconsolidatedcode,allowingthenametobeusedasashortcutforanidea,independentofitscurrentimplementation.Thesearevaluablebenefits,butabstractionsalsohelpinanother,moresubtle,way.Inadditiontotheabove,abstractionstellyouwhereyourcodereliesuponanidea.Buttogetthislastbenefit,youmustrefertoanabstractionineveryplacewhereitapplies.
Studythecodeabove,andconsiderthebitsthatsay "#{container(number-1)}" .When container iscalledfromthe 0 case,thevalueofthepassedargumentis -1 .The -1 causestheconditionalin container tofallthroughtothefalsebranchandreturn"bottles."Althoughthiscodepassesthetests,itdoessobyaccident,notbydesign.
Thecodeabovedoesn’twantthe container of number-1 ;itwants
the container ofthefollowingverse.The successor methodisresponsiblefordeterminingthefollowingverse.Therefore,youshouldnowdefertothatabstraction,andreplacealloccurrencesof number-1 with successor(number) .
Thatfinalchangeresultsinthiscode:
Listing4.58:DeferringtoSuccessor
1 defverse(number)2 casenumber3 when04 #...5 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"6 else7 #...8 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"9 end10 end
Here’sthewhole verse method,showingthe 0 and else casestobeidentical:
Listing4.59:Identical0andElseCases
1 defverse(number)2 casenumber3 when04 "#{quantity(number).capitalize}#
{container(number)}ofbeeronthewall,"+5 "#{quantity(number)}#{container(number)}ofbeer.\n"+6 "#{action(number)},"+7 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"8 else9 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+10 "#{quantity(number)}#{container(number)}ofbeer.\n"+11 "#{action(number)},"+12 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"13 end14 end
Onelastrefactoringtrickprovesthatthiscommontemplateworksforallcases.Copythetemplateandinsertitbelowthecasestatement,asfollows:
Listing4.60:UsingtheSameTemplateforEveryVerse
1 defverse(number)2 casenumber3 when04 #...5 else6 #...7 end
8 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+9 "#{quantity(number)}#{container(number)}ofbeer.\n"+10 "#{action(number)},"+11 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"12 end
Rubymethodsreturntheresultofthelastbitofevaluatedcode,sotheabovechangeletsyoutrythisonetemplateforallcases,whilepreservinganeasyreturntogreenifitfails.Thetestsaregreenafterthischange,andsoyoucansafelydeletetheentirecase statement.
Here’sacompletelistingoftheresultingcode:
Listing4.61:FinalListing
1 classBottles2 defsong3 verses(99,0)4 end5 6 defverses(starting,ending)7 starting.downto(ending).collect{|i|verse(i)}.join("\n")8 end9 10 defverse(number)11 "#{quantity(number).capitalize}#{container(number)}ofbeeronthewall,"+
12 "#{quantity(number)}#{container(number)}ofbeer.\n"+13 "#{action(number)},"+14 "#{quantity(successor(number))}#{container(successor(number))}ofbeeronthewall.\n"15 end16 17 defquantity(number)18 ifnumber==019 "nomore"20 else21 number.to_s22 end23 end24 25 defcontainer(number)26 ifnumber==127 "bottle"28 else29 "bottles"30 end31 end32 33 defaction(number)34 ifnumber==035 "Gotothestoreandbuysomemore"36 else37 "Take#{pronoun(number)}downandpassitaround"38 end
39 end40 41 defpronoun(number)42 ifnumber==143 "it"44 else45 "one"46 end47 end48 49 defsuccessor(number)50 ifnumber==051 9952 else53 number-154 end55 end56 end
Thiscompletesthecurrentrefactoring.The verse case statementhasbeenreducedtoasingletemplatethatreferstoaseriesofsmall,consistent,abstractions.
Nowthatyou’redone,it’simportanttoaskwhetherthisnewcodeactuallyimprovesupontheShamelessGreenfromwhenceyoubegan.Mostprogrammersarguethatit’sbetter,soyoumaybedistressedtohearthatFlogthinksit’sworse.FromFlog’spointofview,allyou’veaccomplishedisturnoneconditionalintomany,whilesimultaneouslyadding55%morecode.
However,beofgoodcheer.DespitetheFlogscore,thiscodeisbetter.Animprovementhasbeenmadethatisinvisibletostaticanalysistools.The container , pronoun , quantity , action and
successor conceptswereinvisibleinShamelessGreen,butarebothrevealedandisolatedinthisnewcode.
4.10.SummaryThischapterfinishedtherefactoringthatbeganinChapter3.ItiterativelyfollowedtheFlockingRulestoremovedifferencesinthe verse method,andasaresultunearthedabstractionsthatweredeeplyhiddenwithinthe99Bottlessong.
ItillustratedthepoweroftheFlockingRulestouncoversophisticatedconcepts,eventhosewhichcastonlydimshadowsintheexistingcode.Youdon’thavetounderstandtheentireprobleminordertofindandexpressthecorrectabstractions—youmerelyapplytheserules,repeatedly,andabstractionswillnaturallyappear.
Onefinalthoughtbeforemovingon.Considerthisquestion:IfseveraldifferentprogrammersstartedfromShamelessGreenandrefactoredthe verse methodaccordingtotheFlockingRules,whatwouldtheresultingcodelooklike?Ifyou’veguessedthateveryone’scodewouldbeidentical,exceptingthenamesusedfortheconcepts,you’dberight.Thishasenormousvalue.
NowontoChapter5,whichreturnstothe"six-pack"problem.
5.SeparatingResponsibilitiesTheprevioustwochaptersappliedtheFlockingRulestoreduceduplicationinthe verse method.Theresultingcodeisgratifyinglyconsistent,andnowexplicitlyexposesconceptsthatcastonlyfaintshadowsintheoriginalcode.Remember,however,thattheimpetusbehindthatentirerefactoringwasthearrivalofthesix-packrequirement.Withoutthischangeinrequirements,youmightverywellhavestoppedatShamelessGreen.
Thischapterreturnstothesix-packproblem.Codesmellsagainguidethechoiceofthenextrefactoring.Anewclasseventuallygetscreated,andalongthewayanumberofbigideasareexamined.Thischapterexploreswhatitmeanstomodelabstractionsandrelyonmessages;itconsiderstheconsequencesofmutationandtheperilsofprematureperformanceoptimization.
5.1.SelectingtheTargetCodeSmellCodeshouldbeopenforextensionandclosedformodification.It’stimetoreexaminethecurrentcodeinlightoftheongoingsix-packrequirement.Recallthefollowingflowchart(whichoriginallyappearedinChapter3):
Figure5.1:OpenClosedFlowchart
Despitethefactthatyou’vesuccessfullyreplacedafairamountofduplicationwithwell-namedmethodsthatexposeconceptsinthe99Bottlesdomain,theresultingcodeisnotyetopentothesix-packrequirement.Ifanything,thecurrentincarnationislessamenabletothisrequirementthanwasShamelessGreen.WithinShamelessGreen,youcouldhavesimplyamendedthecasestatementtoaddabranchforverses6and7.Thechangesneededtomeetthesix-packrequirementwithinthenewcodearenotnearlysoobvious.Itmayseemasifyouhavecomplicatedthingswithoutmakinganyprogresstowardsmeetingthegoal.
Thetruthaboutrefactoringisthatitsometimesmakesthingsworse,inwhichcaseyoureffortsservegallantlytodisproveanidea.Therefactoringrecipesdon’tpromisetoresultincodethatbetterexpressestheproblem—theymerelymakeiteasytocreatethatnewexpression,andjustaseasytorevertit.Properrefactoringallowsyoutoexploreaproblemdomainsafely.
You’venowcompletedonerefactoring,andtheresultingcodeisnotyetopentothesix-packrequirement.Notonlythat,butitisentirelypossiblethatyoudonotyetknowwhatchangewillmakeitopen.Atthispoint,youmustdecidewhetherit’sbettertoproceedwithadditionalmodificationstothecode,orbettertorevertthepreviouschangeandtakeadifferenttack.
Thecurrentcode,althoughnotopentothenewrequirement,isimproved.Thissuggeststhatit’sreasonabletocontinueforward,inhopesthatmoregoodthingswillcome.
Therefore,havefaith,anditerate.Thismeansyoumustcontinuetobeguidedbycodesmells,anddoingsorequiresthatyouidentifythesmellsinthecurrentcode.
5.1.1.IdentifyingPatternsinCodeOnewaytogetbetteratidentifyingsmellsistopracticedescribingthecharacteristicsofcode.Lookatthe Bottles classbelowandmakenoteofthethingsthatcatchyoureye.Includeanypatternsthatyousee,andthingsyoulike,hate,ordon’tunderstand.Thislistingisfollowedbyaseriesofquestionsintendedtoinspirefurtherthoughts,sotakeaminutetoponderbeforereadingon.
Listing5.1:DRYBottlesClass
1 classBottles2 3 defsong
4 verses(99,0)5 end6 7 defverses(starting,ending)8 starting.downto(ending).collect{|i|verse(i)}.join("\n")9 end10 11 defverse(number)12 "#{quantity(number).capitalize}#{container(number)}"+13 "ofbeeronthewall,"+14 "#{quantity(number)}#{container(number)}ofbeer.\n"+15 "#{action(number)},"+16 "#{quantity(successor(number))}#{container(successor(number))}"+17 "ofbeeronthewall.\n"18 end19 20 defcontainer(number)21 ifnumber==122 "bottle"23 else24 "bottles"25 end26 end27 28 defquantity(number)29 ifnumber==030 "nomore"
31 else32 number.to_s33 end34 end35 36 defaction(number)37 ifnumber==038 "Gotothestoreandbuysomemore"39 else40 "Take#{pronoun(number)}downandpassitaround"41 end42 end43 44 defpronoun(number)45 ifnumber==146 "it"47 else48 "one"49 end50 end51 52 defsuccessor(number)53 ifnumber==054 9955 else56 number-157 end58 end59 end
Thefollowingquestionsdrawattentiontoanumberofinterestingcharacteristicsofthecodeasit’swrittensofar:
1. Doanymethodshavethesameshape?
2. Doanymethodstakeanargumentofthesamename?
3. Doargumentsofthesamenamealwaysmeanthesamething?
4. Ifyouweretoaddtheprivatekeywordtothisclass,wherewoulditgo?
5. Ifyouweregoingtobreakthisclassintotwopieces,where’sthedividingline?
ForthosemethodscreatedbytheFlockingRules( container ,quantity , action , pronoun and successor ,hereafterreferredtoasthe"flockedfive"):
6. Dothetestsintheconditionalshaveanythingincommon?
7. Howmanybranchesdotheconditionalshave?
8. Dothemethodscontainanycodeotherthantheconditional?
9. Doeseachmethoddependmoreontheargumentthatgotpassed,orontheclassasawhole?
Theremainderofthissectionexaminestheabovequestions.Ifanydidn’toccurtoyou,lookbackatthecodeandtrytoanswerthembeforeproceeding.
5.1.2.SpottingCommonQualitiesThefirstfivequestionsabovelookattheclassasawholeandexposecommonqualitiesofthecode.Thisnextsectionexamines
thesequestionsindetail.
Question1:Doanymethodshavethesameshape?
Yes.Theflockedfiveallhavethesameshape.
Youcaneasilyidentifysame-shapedmethodsbydoingtheSquintTest(seesidebar).ThefactthatthesemethodsaresoconsistentisatributetotheFlockingRules.Hadthemethodsbeencreatedatdifferenttimes,bydifferentpeople,fordifferentreasons,theycouldeasilyhavecontainedavarietyofshapes.Forexample,thefollowingthreemethodsarelogicallythesame:
Listing5.2:VariousConditionalForms
1 #verboseconditional2 defcontainer(number)3 ifnumber==14 "bottle"5 else6 "bottles"7 end8 end9 10 #guardclause11 defquantity(number)12 return"nomore"ifnumber==013 number.to_s14 end15 16 #ternaryexpression17 defpronoun(number)
18 number==1?"it":"one"19 end
Alloftheabovemethodspassthetests.Theproblemisnotthatthecodedoesn’twork;it’sthatthenon-essentialvariationdisguisesacommonshape.Thisunnecessaryvariationmakesthemethodsappeartobedifferentwhentheyareactuallyverymuchthesame.
Programmersnaturallyassumethatdifferenceexistsforareason,butherethereisn’tone.Superfluousdifferenceraisesthecostofreadingcode,andincreasesthedifficultyoffuturerefactorings.
It’snotyetclearwhatitmeansthatthesemethodshavethesameshape,butit’simportanttonoticethattheydo.
SquintTestOneeasywaytojudgecodeisbyperformingaSquintTest.Thistestrequiresnosetup,andcanbeperformedonanycodeatanytime.
Here’showitworks:
1. Putthecodeofinterestonyourscreen.
2. Leanback.*
3. Squintyoureyessuchthatyoucanstillseethecode,butcannolongerreadit.
4. Lookfor:
a. changesinshape,and
b. changesincolor.
Changesinindentationrevealthepresenceofconditionals.Twoormorelevelsofindentationexposenestedconditionals.Conditionalsresultinmultipleexecutionpathsthroughthecode,whichaddcomplexityandmakecodehardtounderstand.
Changesincolorindicatedifferencesinthelevelofabstraction.Amethodthatintermixesmanycolorstellsastorythatwillbedifficulttofollow.
*Insteadofleaningbackandsquinting,it’sacceptabletozoomoutinyourtexteditoruntilyoucannolongerreadthecode,butcanstillseeitsshapeandcolor.
Question2:Doanymethodstakeanargumentofthesamename?
Sixmethodstake number asanargument—the verse methodandtheflockedfive.
Listing5.3:MethodsWhichTakeanArgumentNamedNumber
1 defverse(number)2 defcontainer(number)3 defquantity(number)4 defaction(number)5 defpronoun(number)6 defsuccessor(number)
Question3:Doargumentsofthesamenamealwaysmeanthe
samething?
Theeasiestwaytounderstandwhat number representsistofollowitspaththroughthecode,beginningwith song .Here’sareminderofthatmethod:
Listing5.4:SongMethod
1 defsong2 verses(99,0)3 end
When song sends verses(99,0) ,the 99 and 0 representthestartingandendingversenumberstosing.Youcouldarguethatthe 99 and 0 representthestartingnumberofbottlesintheversetobesung,butthatwouldbestretchingitandyou’dbeinaminority.Mostfolksinterpretthe 99 and 0 asversenumbers.
If song issendingversenumbersto verses ,the verses methodmustbereceivingthem.Here’sthatmethod:
Listing5.5:VersesMethod
1 defverses(starting,ending)2 starting.downto(ending).collect{|i|verse(i)}.join("\n")3 end
The starting and ending argumentsareversenumbers.Theverses methoditeratesbetweenthem,so i ,theargumentyieldedtotheblock,mustalsorepresentaversenumber.Therefore,andquitesensiblyso,theargumentwithwhich verseisinvokedmustbetheversenumbertobesung.Asreceivedbyverse ,thisargumentisnamed number .
Torepeat(withnointentiontobelaborthepoint),the numberargumenttakenby verse representsaversenumber.
Nowswitchyourattentiontotheflockedfive,allofwhichalsotakeanargumentnamed number .Here,forexample,is container :
Thequestionathandiswhether number asreceivedby containerrepresentsthesameconceptas number asreceivedby verse .Toanswerthisquestion,considertheentire verse method:
Listing5.6:VerseMethod
1 defverse(number)2 "#{quantity(number).capitalize}#{container(number)}"+3 "ofbeeronthewall,"+4 "#{quantity(number)}#{container(number)}ofbeer.\n"+5 "#{action(number)},"+6 "#{quantity(successor(number))}#{container(successor(number))}"+7 "ofbeeronthewall.\n"8 end
Noticethatline2aboveinvokes container with number ,whileline6invokes container with successor(number) .Withineveryverse,container isinvokedtwice,ontwodifferentvalues.
Thishappensbecauseeachverseknowsabouttwodifferentnumbersofbottles.Verse37,forexample,beginswith37bottles
defverse(number)
defcontainer(number)
ofbeer,andendswith36.Asyou’vealreadyseen,theincomingnumber argumentto verse representsaversenumber.However,theparameterthat verse thenpassesonto container standsforsomethingelse—abottlenumber.
Thesameistruefortheotherflockedfivemethods—theargumenttheyreceiveisabottlenumberratherthanaversenumber.Thus,the verse methodandtheflockedfivemethodsusethesameargumentnametorepresentdifferentconcepts.
Thisisrarelyagoodidea.
Ifyouhavelongsincenoticedthisissue,congratulations,butyou’reinaminority.Mostfolkswhoworkthisproblemnametheargumenttakenbytheflockedfivemethodsaftertheparameterpassedfrom verse .Initially,thismadeperfectsense.BackinChapter3,whentheFlockingRulesledtotheextractionofthecontainer method,yourgraspoftheproblemwaslessdevelopedthanitisnow.Thenitwasclearonlythat:
thecasestatementin verse switchedon number ,and
container neededanargumentinordertodecidewhethertoreturn"bottle"or"bottles."
Intheinterestsofconsistency,itwasreasonablebackinChapter3tonametheargumenttakenby container aftertheparameterbeingpassedfrom verse .Intheinterimithasn’tmatteredthatnumber standsforaversenumberwithin verse butabottlenumberwithin container .
Now,however,itbeginsto.Havingmultiplemethodsthattakethesameargumentisacodesmell.It’simportant,however,torecognizethatheretheterm"same"meanssameconcept,notidenticalname.Inanidealworld,eachdifferentconceptwouldhaveitsownunique,precisename,andtherewouldbeno
ambiguity.Unfortunately,realworldcodeoftenfailstomeetthisideal.Inlong-livedapplications,thesameconceptmightgobyseveraldifferentnames,or,asinthiscase,differentconceptsmighthidebehindasinglename.Thesenamingmistakesmakeithardertonoticeunderlyingcodesmells,andnowthatyou’relookingforpatternsinthecode,youmustexaminetheargumentsandclarifytheabstractionsthattheyrepresent.
Havingexaminedtheuseof number in Bottles ,it’snowclearthatthisargumentrepresentsaversenumberto verse ,butabottlenumbertotheflockedfivemethods.
Question4:Ifyouweretoaddtheprivatekeyword,wherewoulditgo?
After verse andbeforetheflockedfivemethods.
Question5:Ifyouweregoingtobreakthisclassintotwopieces,where’sthedividingline?
Sameasabove,i.e.after verse andbeforetheflockedfivemethods.
5.1.3.EnumeratingFlockedMethodCommonalitiesNowthatyou’veconsideredtheclassasawhole,it’stimetomoveontoquestionssixthroughnine,whichapplyonlytotheflockedfivemethods.
Question6:Dothetestsintheconditionalshaveanythingincommon?
Here’sasummaryoftheconditionals:
Listing5.7:FlockedFiveConditionalTests
1 defcontainer(number)2 ifnumber==13 #...4 end5 6 defquantity(number)7 ifnumber==08 #...9 end10 11 defaction(number)12 ifnumber==013 #...14 end15 16 defpronoun(number)17 ifnumber==118 #...19 end20 21 defsuccessor(number)22 ifnumber==023 #...24 end
Inthecodeabove,notonlydoalloftheconditionalstestthevalueof number ,buttheytestfor number tobeexactlyequaltoanothervalue.
Theseconditionalscouldlogicallyhaveusedthelessthan,
greaterthanornotequaloperators,andstillpassthetests.TheIncomprehensiblyConciseexampleinChapter1managedtouseallfouroftheseoperations,andyourownsolutionmayalsohavehadtestsforsomethingotherthanequality.
Programmerstendtoblithelyinterchangethesedifferentcomparisonoperators,confidentthatifthetestspass,thecodeiscorrect.However,havingteststhatpassdoesn’tguaranteethebestexpressionofcode,andthisisacasewhereyourchoiceofoperatoraffectsfuturecosts.
Testingforequalityhasseveralbenefitsoverthealternatives.Mostobviously,itnarrowstherangeofthingsthatmeetthecondition.Intheaboveexamples,ifunexpectedvaluesof numberarrive,the else branchexecutes.Knowingthattheonlywaytogettothe true branchisbysupplyinganexactvalueof numbermakesiteasierforfuturereaderstounderstandthecode.Thisreducesthedifficultyofdebuggingerrorscausedbyincorrectinputs.Testingforequalityalsomakesthecodemoreprecise,andthisprecision,asyouwillsoonsee,enablesfuturerefactorings.
Question7:Howmanybranchesdotheconditionalshave?
Eachconditionalcontainstwobranches.Thismayornothavemeaning,butit’scertainlyavisiblequalityofthecodeandthusworthnoting.
Question8:Dothemethodscontainanycodeotherthantheconditional?
No.Eachmethodisnamedafteraconcept,andcontainsasingleconditional.Thisconditionalusesthevalueof number tochoosethecorrectconcreteexpressionoftheconcept.Thesemethodsarefiercelycommittedtohavingoneresponsibilityandnever
conflatingtwoconcepts.
Question9:Doesmethodsthattake number asanargumentdependmoreon number ,ormoreontheclassasawhole?
Theflockedfivedependonlyonthe number argument,ratherthanontherestoftheclass.Thisistrueevenfor action ,ifyouacceptthatalthough action dependson pronoun , pronoun dependsonlyon number .
Inconjunction,theseninequestionsgroupcertainmethodstogether.Thesame-shaped,same-kind-of-conditional-testing,bottlenumber-taking,argument-depending,flockedfivemethodsfallintoonegroup,andthe song , verses ,and verse methodsintoanother.Theanswerstothequestionsaboverevealmanycharacteristicsofthecode,butthere’sonemorequalitytodiscussbeforemovingon.
5.1.4.InsistingUponMessagesThiscodecontainsadeeplynon-object-orientedpattern:theflockedfivemethodstakeanargument,examineit,andthensupplybehaviorforit.
Asyou’veseen,thosefivemethodssharethiscommonshape:
TheabovemethodwascreatedbytheFlockingRules,andsoexhibitsmanydesirablequalities.Despitethat,it’sdeeplyflawed
defcontainer(number)ifnumber==1"bottle"else"bottles"endend
whenconsideredfromthepointofviewofanindependentOOpractitioner.Whatthatpractitionerwouldseehereisthatsomeonehasgonetothetroubleofinjectingadependency( number ),butthatdependencyistooimpairedtosupplytheneededbehavior.Consequently,notonlydoes container knowabout number ,butit’salsoforcedtounderstandwhatthespecificvaluesof number mean,andtoknowwhattodoineachcase.Thecontainer methoddependsoneachofthesethings.Ifanyofthemchange,the container methodmightbeforcedtochangeinturn.
ItmadesensetotolerateaconditionalbackinShamelessGreen.Thatsolutionoptimizedforunderstandabilitywithoutregardforchangeability.Itsgoalwastogettogreenquickly.Theresultingcodewasmoreproceduralthanobject-oriented,butwouldhavebeengoodenoughifnothingeverchanged.However,nowthatyouhaveanewrequirementandarerearrangingthecode,you’dliketoapplyafull-blownOOmindset,andthatmindsetisdeeplysuspiciousofconditionals.
AsanOOpractitioner,whenyouseeaconditional,thehairsonyourneckshouldstandup.Itsverypresenceoughttooffendyoursensibilities.Youshouldfeelentitledtosendmessagestoobjects,andlookforawaytowritecodethatallowsyoutodoso.Theabovepatternmeansthatobjectsaremissing,andsuggeststhatsubsequentrefactoringsareneededtorevealthem.Beonthelookoutforthiscodeshape,asitimpliesthatthere’smoretobedone.
Thisisnottosaythatyou’llneverhaveaconditionalinanobject-orientedapplication.ThereisaplaceforconditionalsinOO.ManageableOOapplicationsconsistofpoolsofsmallobjectsthatcollaboratetoaccomplishtasks.Collaboratorsmustbebroughttogetherinusefulcombinations,andassemblingthesecombinationsrequiresknowingwhichobjectsaresuitable.Someobject,somewhere,mustchoosewhichobjectstocreate,andthisofteninvolvesaconditional.
However,there’sabigdifferencebetweenaconditionalthatselectsthecorrectobjectandonethatsuppliesbehavior.Thefirstisacceptableandgenerallyunavoidable.Thesecondsuggeststhatyouaremissingobjectsinyourdomain.
Codeisstrivingforignorance,andpreservingignorancerequiresminimizingdependencies.The container methodyearnstobeinjectedwithasmarterobjecttowhichitcouldmerelyforwardthemessage,asshownhere:
Theexistingcodeisimploringyoutocreatethatsmarterobject.
5.2.ExtractingClassesThequestionsaboveidentifycharacteristicsthatgroupmethodstogether,andmanyofthesegroupsoverlap.Forexample,anumberofmethodstakethesameargument.Mostmethodsthatdosohavethesameshape,containaconditional,couldbeconsideredprivate,anddependmoreontheargumentthanontheclassasawhole.
Eachitemaboveactslikeavote,andthesevotescombinetopointtoPrimitiveObsessionasthedominantcodesmell.Built-indataclasseslike String , Fixnum , Integer (Fixnum 'ssuperclass),Array ,and Hash areexamplesof"primitives."PrimitiveObsessioniswhenyouuseoneofthesedataclassestorepresentaconceptinyourdomain.Obsessingonaprimitiveresultsincodethatpassesbuilt-intypesaround,andsuppliesbehaviorforthem.
ThecureforPrimitiveObsessionistocreateanewclasstouseinplaceoftheprimitive.Forthisoperation,therefactoringrecipeisExtractClass.
defcontainer(smarter_number)smarter_number.containerend
5.2.1.ModelingAbstractionsHavingdecidedtocurethePrimitiveObsessioncodesmellwiththeExtractClassrefactoring,youmustnowchooseanameforthisnewclass.
Theprimitivethatyou’rereplacingrepresentsabottlenumber.Noticethatitisnotabottle:it’sabottlenumber.Abottleismadeofplastic,orglass,oraluminum,andcontainswater,orsoda,orbeer.Abottlehasashapeandavolume.Itexistsinthephysicalworld.
Unlikebottles,numbersaren’tthings—they’reideas,albeitonessoubiquitousthatyou’velikelyforgottenhowabstractandunlikelytheyare.Numbersaresymbolsusedtodescribequantitiesofthings.Theydon’tphysicallyexist.Youcanpickupabottle,butyoucannotpickupa"six."
Thisnewclassdoesnotrepresentakindofbottle:itrepresentsakindofnumber.Thedistinctionmayseemsubtle,butthedividebetweenthesetwoconceptsischasmic.Abottleisathing,whileanumberisanidea.It’seasytoimaginecreatingobjectsthatstandinforthings,butthepowerofOOisthatitletsyoumodelideas.
Model-ableideasoftenliedormantintheinteractionsbetweenotherobjects.Forexample,aneventmanagementapplicationmightcontain Buyer and Ticket classes. Buyer and Ticket areobviousbecauseyoucanreachoutandtouchthemintherealworld.Theseobjectsinteractinmanyways:buyersbuytickets,perhapsatadiscount,andmaylaterchangetheirmindsandreturntheticketsforrefunds.
Where,insuchanapplication,shouldthelogictomanagepurchases,discounts,andrefundsreside?Youcouldjameverythinginto Buyer and Ticket ,butthepowerofOOisthatitallowsyoutocreateavirtualworldinwhich Purchase , Discount
and Refund arejustasreal.Embodyingtheseconceptsintodiscreteclassesseparatesresponsibilitiesandmakestheoverallapplicationeasiertounderstand,test,andchange.
ExperiencedOOprogrammersdeftlycreatevirtualworldsinwhichideasareasrealasphysicalthings.Ifyouarenotyetcomfortabledoingso,starttodaybythinkingoftheclassyou’reabouttoextract,notasaphysicalbottle,butasasymbolicnumberwithanaddedbitofbottle-ishbehavior.
Bearingthatideainmind,considerwhattonamethisclass.Thetwomostobviouschoicesare BottleNumber ,or ContainerNumber .
5.2.2.NamingClassesYou’vebeenintroducedtotheruleaboutnamingmethodsatonehigherlevelofabstractionthantheircurrentimplementation.Extrapolatedtoclasses,thatrulesuggeststhisnewclassshouldbenamed ContainerNumber .However,you’vealsoreadfairlylengthydiscoursesaboutnotanticipatingthefuture,andsincetheexistingrequirementsinvolveonlybottles,youmightleantowards BottleNumber .
BottleNumber islessflexiblebutmorestraightforward.ContainerNumber isjusttheopposite;it’sabitmoregeneral,andsowouldworkforabroaderrangeofvessels. BottleNumber ismoreconcrete. ContainerNumber ismoreabstract.
Thetie-breakerhereisthatthe"namethingsatonehigherlevelofabstraction"ruleappliesmoretomethodsthantoclasses.Itwouldbespeculativetocallthisnewclass ContainerNumber .Theruleaboutnamingcanthusbeamended:whileyoushouldcontinuetonamemethodsafterwhattheymean,classescanbenamedafterwhattheyare.
Havingtworequirementsforbottlesfirmlysuggeststhatthisclassrepresentsabottlenumber,andshouldbenamedassuch.
Asalways,youcanrevisitthisdecisionifthingschangelater.
5.2.3.ExtractingBottleNumberThissectionextractsanewclassnamed BottleNumber fromtheexistingcode.ItdoesnotuseTDD.Instead,itcreatesthenewclassbyfollowingaslightlymodifiedversionofMartinFowler’sExtractClassrefactoringrecipe.
Asyoumightrecall,saferefactoringreliesupontestsrunninggreen,sothefactthatthenew BottleNumber classwillcomeintoexistencebeforeitstestsarrivehasacoupleofconsequences.First,theexisting Bottles testsbecomethesafetynetforthisnewclass.Theywereoriginallywrittenasunittests,butusingthemtoindirectlytest BottleNumber transformsthemintoakindofintegrationtest.Thesetestsmustcontinuetorunaftereverychange.
Next,whileextractingtheclass,codethatisknowntoworkiscopiedfrom Bottles into BottleNumber .It’simportanttoputthisnewclassfullyintousebeforeeditinganyofthecopiedcode.Safetyisbeingprovidedbythe Bottles tests,sotheymustexercisethenewcodeasquicklyaspossible.
Inthepreviouschapters,theprocessofchangingcodewassubdividedintofoursteps.
1. parsethenewcode
2. parseandexecuteit
3. parse,executeanduseitsresult
4. deleteunusedcode
Thesestepsstillapply.Starttheclassextractionbycreatinganempty BottleNumber class,asshownbelow:
Listing5.8:BottleNumberClassDefinition
1 classBottles2 #...3 end4 5 classBottleNumber6 end
Asyougothroughthisrefactoring,remembertosavethecodeaftereverychange,andtorunthetestsaftereverysave.
Next,copythemethodsthatobsessonbottle number intothenewclass.
Listing5.9:ObsessiveMethodsCopiedtoBottleNumber
1 classBottles2 #...3 defcontainer(number)4 ifnumber==15 "bottle"6 else7 "bottles"8 end9 end10 11 defquantity(number)12 #...13 end14 15 defaction(number)16 #...
17 end18 19 defpronoun(number)20 #...21 end22 23 defsuccessor(number)24 #...25 end26 end27 28 classBottleNumber29 defcontainer(number)30 ifnumber==131 "bottle"32 else33 "bottles"34 end35 end36 37 defquantity(number)38 ifnumber==039 "nomore"40 else41 number.to_s42 end43 end44 45 defaction(number)46 ifnumber==047 "Gotothestoreandbuysomemore"
48 else49 "Take#{pronoun(number)}downandpassitaround"50 end51 end52 53 defpronoun(number)54 ifnumber==155 "it"56 else57 "one"58 end59 end60 61 defsuccessor(number)62 ifnumber==063 9964 else65 number-166 end67 end68 end
Rememberthatthe verse methodshouldnotbeextracted.Eventhoughitsargumentisalsonamed number ,inthiscasetheargumentrepresentsaversenumber,notabottlenumber.
Noticethattheaboveexamplecopiedmethodsfrom Bottle toBottleNumber .Themethodsweren’tmoved—theywereduplicated,sonothingabout Bottle hasyetbeenchanged.Thismeansthattheoldcodecontinuestoworkasisandthenewcodeisnotyetbeingexecuted.Runningthetestsatthispointmerely
parsesthenewcode,provingthatit’ssyntacticallycorrect.
Asmentionedearlier,therecipebeingfollowedherewasinspiredbyonefromMartinFowler.The"official"ExtractClassrecipebeginsbylinkingtheoldclasstothenew.Then,oneatatime,therecipemovesfields,andthenmethods,ofinterest.Incontrast,theexampleabovestartswithFowlersfinalstep,andcombinesallofthemethodmoveswithinasinglechange.
Thismayseemlikealargeleap,buthereyoucanbeconfidentthatyou’removingtherightgroupofmethods.ThesemethodswerecreatedbytheFlockingRules,sotheyvisiblyshareacommonpattern.Thiscommonpatternmakesiteasytorecognizethattheybelongtogetherintheextractedclass.Thisvisualsimilarityisatributetotherules,andanillustrationofthevalueofstablelandingpoints(rememberthestreamandtherocks?)Thepriorrefactoringresultedindeeplyconsistentcode,andhere’smoreproofthatconsistentcodemakesthecurrentrefactoringeasy.
The BottleNumber classneedstoknowthevalueof number ,soaddan attr_reader for :number ,andan initialize methodtosetthevariable.Here’sthecode:
Listing5.10:BottleNumberHoldingOntoNumber
1 classBottleNumber2 attr_reader:number3 definitialize(number)4 @number=number5 end6 #...7 end
Online2above, attr_reader isaclassmethod.Invokingitwith
thesymbol :number effectivelydefinesanewinstancemethodonBottleNumber thatactslikethis:
Becauseofthe attr_reader , BottleNumber respondstothe numbermessagebyreturningthevalueheldinthe @number instancevariable.Thisvariableissetwithinthe initialize methodonline4above.That initialize methodgetsinvokedwhen new issenttoBottleNumber .
The BottleNumber classnowcontainsallofthenecessarycode,butasyetthiscodeisonlybeingparsed.Thenextsmallstepistoexecuteabitofthenewclasswithoutusingtheresult.
Thefollowingexampledoesthisbyalteringthe container methodof Bottles toinvokethecontainermethodof BottleNumber :
Listing5.11:ParseandExecuteaBitofNewCode
1 classBottles2 #...3 defcontainer(number)4 BottleNumber.new(number).container(number)5 ifnumber==16 "bottle"7 else8 "bottles"9 end10 end11 #...12 end
defnumber@numberend
Line4aboveexecutesthenewmethod,butthendiscardstheresultinfavorofexistingcode.Thisprovesthatthenewcodecanexecutewithoutblowingup,butdoesnotprovethatitreturnsthecorrectresult.
Itmustnowbeadmittedthattheaddedlineofcodeis,byanystandard,ugly.
Intheabovecode,both new and container requirethe numberargument,soitmustpassedtwice.Youmayfindthisannoyinglyredundant.Inthenewly-created BottleNumber class,the containermethodcouldeasilymakedowithoutanargument.Itcangettherightnumberbysimplysendingthe number messagetoitself.Insteadofthecodeabove,you’dprefer:
However,aspreviouslymentioned,youshouldrefrainfromalteringthecodeinthesecopiedmethodsuntilthenewclassisfullywiredintotheold.Regardlessofhowmuchyouhatepassingtheparametertwice,atthispointyoushouldresisttheurgetomakethechangeshownabove.First,fullyconnectBottleNumber to Bottles .Oncethat’scomplete,youcanreturnandimprovethemethodsin BottleNumber .
So,settingthatunpleasantcodetemporarilyaside,thenextsmallstepinthecurrentrefactoringistousetheresultofthe containermessagewithinthe Bottle class.Theeasiestwaytoaccomplishthisistomoveline4tothebottomofthemethod,likeso:
Listing5.12:Parse,ExecuteandUseResult
1 classBottles2 #...
BottleNumber.new(number).container(number)
BottleNumber.new(number).container
3 defcontainer(number)4 ifnumber==15 "bottle"6 else7 "bottles"8 end9 BottleNumber.new(number).container(number)10 end11 #...12 end
Thetestspass,sonowyoucandeletetheoldimplementationfrom container (lines4-8above).Thisleavesthefollowingcode:
Listing5.13:ResultingContainerMethod
1 classBottles2 #...3 defcontainer(number)4 BottleNumber.new(number).container(number)5 end6 #...7 end
Repeattheaboveprocedureforeachofthemethodscopiedfromthe Bottles class.Thisisanextremelymechanical,wonderfullyboring,anddeeplycomfortingrefactoringprocess.
Here’stheresulting Bottles class:
Listing5.14:ForwardingMessagestoBottleNumber
1 classBottles
2 #...3 defcontainer(number)4 BottleNumber.new(number).container(number)5 end6 7 defquantity(number)8 BottleNumber.new(number).quantity(number)9 end10 11 defaction(number)12 BottleNumber.new(number).action(number)13 end14 15 defpronoun(number)16 BottleNumber.new(number).pronoun(number)17 end18 19 defsuccessor(number)20 BottleNumber.new(number).successor(number)21 end22 end
Thesemethodsin Bottles nowmerelyforwardmessagesalongtoBottleNumber .
5.2.4.RemovingArgumentsNowthattheold Bottles classfullyuses BottleNumber ,theexistingtestsserveasasafetynetforchangestothenewclass.Thismeansthatyoucannowundertakeimprovementsinthenewcode.
Although BottleNumber works,partsofitareannoyingly
redundant.TheproblemisthateventhoughinstancesofBottleNumber knowtheir number ,itsmethodscontinuetorequirenumber asanargument.Toillustrate,herearethetwo containermethods:
Listing5.15:RedundantArguments
1 classBottles2 #...3 defcontainer(number)4 BottleNumber.new(number).container(number)5 end6 #...7 end8 9 classBottleNumber10 attr_reader:number11 definitialize(number)12 @number=number13 end14 15 defcontainer(number)16 ifnumber==117 "bottle"18 else19 "bottles"20 end21 #...22 end
Line4abovegetsanew BottleNumber andasksforits container .Doingsorequirestworeferencesto number .The initialize
method(invokedby new anddefinedonline11)andthecontainer method(line15)bothrequirea number argument.
ThepointofthePrimitiveObsession/ExtractClassrefactoringistocreateasmarterobjecttostandinfortheprimitive.Thissmarterobject,bydefinition,knowsboththevalueoftheprimitiveanditsassociatedbehavior.Becausethenew BottleNumber classholdstherightnumber,themethodsin BottleNumber don’tneedtotakeanargument,andinvokersofthesemethodscouldberelievedoftheirobligationtopassaparameter.
Nowthat BottleNumber isfullyconnectedto Bottles ,it’ssafetostartmakingtheseimprovements.Noticethatifyou’rewillingtosimultaneouslyalterboththesendersandthereceiverseverymessage,it’seasytomakethischange.Forexample,youcouldfixthe container methodbychangingline4abovetoremovetheparameterbeingpassedto container ,whilesimultaneouslydeletingtheargumentfromline15.Ifyoumakebothofthesechangesatonce,andthensaveandrunthetests,thetestswillpass.
Keepinmindthatisamulti-linechange.Someproblemsaresosimplethatit’seasiestjustleapinandmakesuchachange,butothersaresocomplexthatitisn’tfeasibletofixeverythingatonce.Inreal-worldapplications,thesamemethodnameisoftendefinedseveraltimes,andamessagemightgetsentfrommanydifferentplaces.Learningtheartoftransformingcodeonelineatatime,whilekeepingthetestspassingateverypoint,letsyouundertakeenormousrefactoringspiecemeal.Thissmallproblemisagoodplacetopracticethistechnique,inpreparationforlatertacklingbiggerones.
BackinChapter3,youhadtoaddanargumenttoamethodthatwasalreadybeingcalledwithoutone.Thisistheoppositeproblem:hereyouneedtoremoveanargumentfromamethodthat’scurrentlybeinginvokedwithone.Whetherargumentsarebeingaddedorremoved,thetrickisthesame;youmustchange
themethoddefinitiontotemporarilysettheargumenttoadefault.
Thereareaseveralwaystoaccomplishthis.Thefollowingtechniqueisthemostdirect,butrequiresashortrefresheronRubysyntax.
Consider container ,repeatedagainbelow.Thismethodtakesanumber argument.Remember,however,thatthe BottleNumberclassitselfrespondstothe number message.Nowanswerthisquestion:Online4below,does number refertotheargument,ortothemessage?
Listing5.16:BottleNumberContainerRedoux
1 classBottleNumber2 #...3 defcontainer(number)4 ifnumber==15 "bottle"6 else7 "bottles"8 end9 end10 #...11 end
Rubyisperfectlyhappytoallowthesamenametobeusedfordifferentthings,andtoinferwhichyoumeanbasedoncontext.Inthecodeabove,theprogrammerclearlyintendsfor number online4torefertothe number argumentfromline3,andthat’sexactlywhatRubydoes.The number online4isinterpretedasareferencetothemethod’sargumentratherthanasasendofthenumber message.
Armedwiththisknowledge,youcanguessthatremovingtheargumentfromthemethoddefinitionwouldcauseRubytointerpretline4asasendofthe number message.Thisisyourgoal,butunfortunately,the Bottles classisstillsendingcontainer(number) ,sothischangebreaksthetests.
Thetricktoworkingyourwayforwardundergreen,whilemakingonlyone-linechanges,istoalterthenameoftheargumenttosomethingotherthan number ,andsimultaneouslygiveitadefault.Line3belowcontainsthatchange:
Listing5.17:RenamedArgument
1 classBottleNumber2 #...3 defcontainer(delete_me=nil)4 ifnumber==15 "bottle"6 else7 "bottles"8 end9 end10 #...11 end
Above,the number argumentfor container hasbeenrenamedtodelete_me andassignedadefaultof nil .Thatchangeturnsthenumber referenceonline4intoamessagesend,whichallowsthismethodtodependuponamessagesenttoitselfratherthananargumentpassedbysomeoneelse.
Nowthattheargumentisoptional,turnyourattentiontosendersof container .Inthisapplicationthere’sonlytheonein Bottles ,shownhere:
Listing5.18:ForwardWithRedundantArguments
1 classBottles2 #...3 defcontainer(number)4 BottleNumber.new(number).container(number)5 end6 #...7 end
Removingthe number parameterfromthe container messageinvocationonline4resultsinthiscode:
Listing5.19:ForwardWithoutRedundancy
1 classBottles2 #...3 defcontainer(number)4 BottleNumber.new(number).container5 end6 #...7 end
Onceyouhavelocatedandremovedtheparameterfromallofitssenders,the container methoddefinitionnolongerneedstotakeanargument.Youcannowreturnto BottleNumber andremovethedelete_me argumentanddefault,asonline3below:
Listing5.20:BottleNumberContainerMethodWithoutArgument
1 classBottleNumber2 #...3 defcontainer4 ifnumber==1
5 "bottle"6 else7 "bottles"8 end9 end10 #...11 end
Here’sarecapofthestepsforremovinganargumentusingone-linechanges.
1. Alterthemethoddefinitiontochangetheargumentnameandprovideadefault.
Startbychangingtheexistingargumentnametoanythingotherthanwhatitcurrentlyis.Using delete_me willhelpyouremembertodeletetheargumentwhenyou’veupdatedallofthesenders.Thevalueofthedefaultdoesnotmatter,soit’scommontouse nil .Intheexampleabove:
became:
2. Changeeverysenderofthemessagetoremovetheparameter.Intheexample:
became:
defcontainer(number)
defcontainer(delete_me=nil)
BottleNumber.new(number).container(number)
BottleNumber.new(number).container
Finally,deletetheargumentfromthemethoddefinition.So,finally:
became:
Asyoucansee,despitethelengthoftheexplanation,thetechniqueissimple,andinvolvesonlythreesteps.Havingpracticedon container ,theothermethodswilleasilybendtoyourwill.Youcannowfollowthisprocesstoremovethe numberargumentfromtheremainingmethodsin BottleNumber .
Ifyoudothisrefactoringyourself,you’llfindthat quantity andaction workasexpected,butthatwhenyouchange pronoun ,thetestsbeginfail.
5.2.5.TrustingtheProcessRefactoringsthatleadtoerrorscanshakeyourfaithinthevalidityofthecorrespondingrecipes.However,theserecipeshaveproventhemselvesreliableinmanycircumstances,formanypeople,inmanysituations.Ifyouadheretoarecipeandtestsstartfailing,it’slikelythatthere’ssomethingabouttheproblemthatyoudon’tyetunderstand.
Inthiscase,you’vebeenusingthe"removeargumentsviaone-linechanges"process.Itworksfor container , quantity ,andaction butcausestheteststofailwhenappliedto pronoun .
Specifically,ifyougotothe pronoun definitionin BottleNumber :
defcontainer(delete_me=nil)
defcontainer
1 classBottleNumber2 #...
andchange number to delete_me ,andsupplyadefault:
Thengotothe pronoun methodin Bottles :
andremovetheparameterfromtheforwardof pronoun toBottleNumber :
Finally,youreturntothe pronoun methoddefinitioninBottleNumber anddeletetheentireargument:
Thenthetestsbegintofailwith:
Theprocessthatworkedforothermethodsisnowfailingforpronoun .Whilethiserrormightleadyoutodoubtthevalidityofthetechnique,itdoesn’tpointoutaflawintheprocess.Instead,it
3 defpronoun(number)
1 classBottleNumber2 #...3 defpronoun(delete_me=nil)
1 classBottles2 #...3 defpronoun(number)4 BottleNumber.new(number).pronoun(number)
1 classBottles2 #...3 defpronoun(number)4 BottleNumber.new(number).pronoun
1 classBottleNumber2 #...3 defpronoun
ArgumentError:wrongnumberofarguments(given1,expected0)
exposesaslightlymorecomplexbitofcode.
Recallthestepsneededtoremoveparameters:
1. Alterthemethoddefinitiontochangetheargumentname,andprovideadefault.
2. Changeeverysenderofthemessagetoremovetheparameter.
3. Deletetheargumentfromthemethoddefinition.
Thefailureappearedafterstep3.Theerrormessageindicatesthatsomecallerisstillpassingaparameterto pronoun .Thismeansstep2isn’tcomplete;i.e.somesenderhasnotbeenfixed.Thisshouldtriggeryoutoexaminethesourcecodewherethefailureoccurred.Whenyoudoso,you’llseethefollowing:
Itturnsoutthat pronoun isinvokedonlyfromthe action methodof BottleNumber ,wherethemessageissentto self .The pronounmethoddefinedbackin Bottles isnolongerused(asyoucanconfirmbycavalierlydeletingitandrunningthetests).
Insteadofchangingtheunused pronoun methodin Bottles ,step2shouldhaveremovedthe number argumentfromthecalltopronoun inthe action methodof BottleNumber ,leaving:
1 classBottleNumber2 #...3 defaction(number)4 ifnumber==05 "Gotothestoreandbuysomemore"6 else7 "Take#{pronoun(number)}downandpassitaround"8 end9 end
Onceyoumakethatchangeandthencompletethesteps,thecodepassesthetests.
Thelessonhereisthattheprocessworks,andthatencounteringerrorswhilefollowingitsuggeststhatacloserlookatthecodeisinorder.Agreatbenefitoftheserefactoringtechniquesisthatyoucanaccomplishquiteabitwhilethinkingverylittle.Sometimes,however,thoughtjustcan’tbeavoided.Theblessingofthesetechniquesisthatalteringcodeinsuchsmallincrementsseverelyconstrainsthenumberoferrorsanychangecanintroduce.Whenforcedtothink,youcanbeconfidentthatyoureffortswillbenarrowlyfocusedonanopportunetopic.
Nowthat pronoun works,onlythe successor methodremains.Itsuccumbstothisrefactoringwithnosurprises.ThiscompletestheremovalofextraneousargumentstomethodsintheBottleNumber class,andleavesthecodeatthefollowingrestingpoint.
Listing5.21:ForwardMessagestoSmarterNumber
1 classBottles2 3 defsong4 verses(99,0)5 end6
1 classBottleNumber2 #...3 defaction(number)4 ifnumber==05 "Gotothestoreandbuysomemore"6 else7 "Take#{pronoun}downandpassitaround"8 end9 end
7 defverses(starting,ending)8 starting.downto(ending).collect{|i|verse(i)}.join("\n")9 end10 11 defverse(number)12 "#{quantity(number).capitalize}#{container(number)}"+13 "ofbeeronthewall,"+14 "#{quantity(number)}#{container(number)}ofbeer.\n"+15 "#{action(number)},"+16 "#{quantity(successor(number))}#{container(successor(number))}"+17 "ofbeeronthewall.\n"18 end19 20 defcontainer(number)21 BottleNumber.new(number).container22 end23 24 defquantity(number)25 BottleNumber.new(number).quantity26 end27 28 defaction(number)29 BottleNumber.new(number).action30 end31 32 defsuccessor(number)33 BottleNumber.new(number).successor
34 end35 end36 37 classBottleNumber38 attr_reader:number39 definitialize(number)40 @number=number41 end42 43 defcontainer44 ifnumber==145 "bottle"46 else47 "bottles"48 end49 end50 51 defquantity52 ifnumber==053 "nomore"54 else55 number.to_s56 end57 end58 59 defaction60 ifnumber==061 "Gotothestoreandbuysomemore"62 else63 "Take#{pronoun}downandpassitaround"
5.3.AppreciatingImmutabilityTomutateistochange.Stateis"theparticularconditionofsomethingataspecifictime."Avariableis"thatwhichvaries,"or,inmaths,"aquantitywhichadmitsaninfinitenumberofvaluesinthesame
64 end65 end66 67 defpronoun68 ifnumber==169 "it"70 else71 "one"72 end73 end74 75 defsuccessor76 ifnumber==077 9978 else79 number-180 end81 end82 end
Thecompletestheextractionofthe BottleNumber class.Despiteitsmanyconditionals,thecodehasaregular,orderlyaspectthatfeelspleasing,andbodeswellforfuturerefactorings.
It’salmosttimetoreturnyourfocustothe Bottles class,butbeforedoingso,thereareafewbroadideastoconsider.
expression."
Inthephysicalworld,conditionsvaryovertime.Yourcoffeecupwasfull,butnowisempty.You’vebeenexercising,andnowyou’remorefit.TheHimalayasarerising.
It’sthesamecup,you,andmountainrange,buttheirconditionshavechanged.Therealworldispervadedbythisidea—whatexists,willchange.
Humanagreementaboutthenecessityandrightnessofchangeisreflectedinthechoiceofthewordvariableforusewithincomputerprogramminglanguages.Whatpurposeavariableotherthantovary?Mostobject-orientedprogrammerswritecodethatbothexpectsandreliesuponobjectmutation.Objectsareconstructed,used,mutated,andthenusedagain.
Regardlessofhowintuitiveandnaturalitmayseem,mutationisnotanabsoluterequirement.Itisperfectlypossible(asprogrammersoffunctionallanguageswillhappilyinformyou)toconstructapplicationsfromimmutableobjects,i.e.objectsthatdonotchange.Forthoseunusedtothisidea,itcanbedisorientingtoimaginerealityasconstructedbythefunctionalprogrammer.Insteadofrefillingyourexistingcup,youdiscarditinfavorofanewonethatlooksidenticalbutisfullofcoffee.Ratherthanchangingyourselftobemorefit,youswapyourselfforthenew,fitter,you.AstheHimalayasrise,youreplaceyourexistingcopywithabrandnewmountainrangethat’satinybittaller.
Iftheideaofimmutabilityisnewtoyou,theexamplesinthepriorparagraphmayseempositivelyalarming.Thefirstconcernmostfolkshaveisforperformance.Theconsequencesofgettingawholenewcupwhenallyouwantismorecoffeedon’tseemsobad,butreplacinganentiremountainrangetohandleafive-millimeterannualheightchangemayfeelexcessive.
Thenextsectionwilldelveintothoseconsiderations,sodefer
performanceconcernsforamoment.Fornow,ponderthebenefitsofworkingwithobjectsthatdonotchange.Whatvirtuemightimmutabilityprovide,andwhattroublemightitavoid?
Oneofthebestthingsaboutimmutableobjectsisthattheyareeasytounderstandandreasonabout.Theseobjectsneverstartoutonewayandthensecretlymorphintosomethingelse.Youcanbeconfidentthatwhatyouseeatcreationtimeisalwayswhatyougetlater.
Becausetheyareeasytoreasonabout,immutableobjectsarealsoeasytotest.Objectsthatchangeneedtestsfortheaffectedbehavior.Thechangemightbecausedbyacollaboratingobject,ortriggeredbyadistantevent,sotestscouldneedadditionalcollaborators,oractionstriggeredbyapparentlyunrelatedpartsofyourapp.Testsforimmutableobjectsavoidthisextrasetup,whichmakesthetestscheapertowriteandeasiertounderstand.
Anotherkeyvirtueofimmutableobjectsisthattheyarethreadsafe.Someofthemostperniciousbugsinmulti-threadedsystemsinvolvetheinadvertentchangingofsharedstatebydifferentthreads.Thesebugsareoftenrelatedtothetimingofthreadexecution,andsoarenotoriouslydifficulttoreproduce,aswellascostlyandfrustratingtodebug.Thisclassofproblemisentirelyavoidedbyimmutableobjects.Youcan’tbreaksharedstateifsharedstatedoesn’tchange.
Therefore,therearemanygoodreasonstopreferobjectsthatdonotmutate.Youarerestrainedfromcreatingthemonlybythehabitofmutability,andthe(oftenunquestioned)assumptionthatinstantiatingnewobjectswillbeunacceptablymorecostlythanreusingexistingones.
Havingreadthissection,lookbackatthenew BottleNumber classinListing5.21:ForwardMessagestoSmarterNumber.Thequestionofmutabilityappliesdirectlytothisnewclass.Imaginethatyou’reholdingontoaninstanceof BottleNumber whose @number variablecontainsthevalue 99 .Theverseprogressessuchthatitnowneeds
bottlenumber98.Isitbettertomutatethevalueof @number inthecurrentinstanceof BottleNumber ,orshouldthatobjectbediscardedinfavorof BottleNumber.new(98) ?
Ifyouleantowardsmutatingtheexisting BottleNumber ratherthanmakinganother,it’spossiblethatyouarebiasedagainstcreatingnewobjects.Thisbiasisoftenunexamined,andhasitsrootsintheassumptionthatifyouroutinelycreatemanynewobjects,yourapplicationwillbetooslow.
5.4.AssumingFastEnoughThebenefitsofimmutabilityaresogreatthat,ifitwerefree,you’dchooseiteverytime.Immutability’soffsettingcostsaretwofold.First,youmustbecomereconciledtotheidea,whichformanyprogrammersisnosmallthing.Next,achievingimmutabilityrequiresthecreationofmore(sometimesmanymore)newobjects.
Gettinghabituatedtoanewwayofthinkingneedhappenonlyonce,sothiscostisnotanpermanentconcern;drinkingtheimmutabilityKool-Aidtodaysufficesforforever.Theongoingcostsofimmutabilityarethereforemostlyinthecreationofnewobjects,andthat’sthetopicofthissection.
YoumaybefamiliarwithPhilKarlton’sfamoussaying"ThereareonlytwohardthingsinComputerScience:cacheinvalidationandnamingthings."You’vealreadyreadagreatdealaboutnamingthings,andit’sfinallytimetodiscusscaching.
Acache,incomputerscience,isalocalcopyofsomethingstoredelsewhere.Savingalocalcopyoftheresultsofanexpensiveoperation,orcachingit,isassumedtoincreasethespeedofyourapplication,andsolowercosts.
Thepresumptionsintheabovestatementaretwofold.First,itassumesthatcachingwillmakeapplicationsfaster,andnext,itassumesthatcachingwilllowercosts.Thesestatementsare
sometimestrue,butnotalways.
Whenyousendamessageandsavetheresultintoavariable,you’vecreatedasimplecache.Ifthevalueinyourvariablebecomesobsolete,youmustinvalidatethiscache,eitherbydiscardingit,orbyresendingthemessageandsavingthenewresult.
Cachingiseasy.However,figuringoutthatacacheneedstobeupdatedcanbehard.Thecodetodosoisoftencomplicatedandconfusing.Thisadditionalcodemustbetested,andinevitably,whenitturnsoutthatthetestsareinsufficient,debugged.Theextracodeneededtomanageacachecanbesodifficulttowrite,hardtounderstand,andexpensivetorunthatitoffsetstheoriginalbenefits.
Noticethatthecostsofcachingandmutationareinterrelated.Ifthethingyoucachedoesn’tmutate,yourlocalcopyisgoodforever.Ifyoucachesomethingthatchanges,youmustwriteadditionalcodetorecognizethatyourcopyisstale,andtore-runtheinitialoperationtoupdatethecache.
Ifyou’veeverworkedoncodethathandlescomplicatedcacheinvalidation,itwillcomeasnosurprisethattheworditselfcomesfromtheFrenchcacher,whichmeanstoconcealorhide.Outdatedcachescanbeasourceofopaque,expensive,andfrustratingbugs.Thenetcostofcachingcanbecalculatedonlybycomparingthebenefitofincreasesinspeedtothecostofcreatingandmaintainingthecache.Ifyourequirethisspeedincrease,anycostischeap.Ifyoudon’t,everycostistoomuch.
Mutationandcachingcomplicatecode.Thiscomplicationisoftenacceptedasnecessaryandjustifiedbythebeliefthatitwillimproveperformance.However,theunfortunatetruthisthathumansareverybadatpredicting,inadvance,whetheraprogramwillbefastenoughoverall,and,ifnot,whichpartsofitwillbetooslow.
Complicatingcodeinordertosolveperformanceproblems,inadvanceofactualdataaboutwherethoseproblemsare,raisescosts
andveryoftenpaysnothinginreturn.Theseguessesarealmostcertaintobewrong,andmerelyservetoharmreadabilityandimpedechange.
Giventhis,thebestprogrammingstrategyistowritethesimplestcodepossibleandmeasureitsperformanceonceyou’redone.Ifthewholeisnotacceptablyfast,profiletheperformance,andspeeduptheslowestparts.Increasingspeedmayrequirecaching,butmanyproblemscanbefixedbysubstitutingmoreefficientcodeinspecific,narrowplaces.Onceyouunderstandpreciselywhat’swrong,itmaybepossibletofixitwithoutcachingatall.
Yourgoalistooptimizeforeaseofunderstandingwhilemaintainingperformancethat’sfastenough.Don’tsacrificereadabilityinadvanceofhavingsolidperformancedata.Thefirstsolutiontoanyproblemshouldavoidcaching,useimmutableobjects,andtreatobjectcreationasfree.Thisresultsinspeedydevelopmentofsimplecode,whichleavesplentyoftimetoidentifyandcorrecttherealperformanceproblems.
Nowthatthissomewhattheoreticaldiscussioniscomplete,it’stimereturntothe Bottles class,andapplyideastoactualcode.
5.5.CreatingBottleNumbersEvenforthosecomfortablewithobjectcreation,thecodein Bottlesconstructsanotablenumberof BottleNumber s.Examinethemethodsbelow,andcountthenumberoftimesanew BottleNumber iscreatedby verse .
Listing5.22:LotsofNewBottleNumbers
1 classBottles2 #...3 defverse(number)4 "#{quantity(number).capitalize}#
{container(number)}"+5 "ofbeeronthewall,"+6 "#{quantity(number)}#{container(number)}ofbeer.\n"+7 "#{action(number)},"+8 "#{quantity(successor(number))}#{container(successor(number))}"+9 "ofbeeronthewall.\n"10 end11 12 defcontainer(number)13 BottleNumber.new(number).container14 end15 16 defquantity(number)17 BottleNumber.new(number).quantity18 end19 20 defaction(number)21 BottleNumber.new(number).action22 end23 24 defsuccessor(number)25 BottleNumber.new(number).successor26 end27 end
Inthecodeabove,anewinstanceof BottleNumber iscreatedeachtimecontainer , quantity , action ,or successor areinvoked.The versemethodsendsthosemessagesatotalofninetimes.Therefore,overthecourseofthesong,900newinstancesof BottleNumber arecreated
(nineeachin100verses).
Thismayfeelexcessive.
Thisplethoraofobjectcreationistheresultofthepriorrefactoring.Therecipereplacesthebodyofeachoriginalmethodwithcodethatforwardsthemessagetoanewinstanceofthenewly-extractedclass.
Within Bottles , verse istheonlymethodthatsendsthe container ,quantity , action ,or successor messages,sothepresenceoftheseforwardingmethodsmayseemlikeoverkill.Inthissimpleexample,theyprobablyare.Inmorecomplicatedproblems,however,itwouldnotbesurprisingtoperformanExtractClassrefactoringandfindthattheresultingforwardingmessageswereinvokedmanytimes,frommanyothermethodswithintheoriginalclass.Theseforwardingmethodsexisttoprovideasingleplacefortheoriginalclasstocatchthesemessageswhensenttoitself,andfunnelthemalongtothenewclass.
Thepreviousrefactoringrecipemakesnoattempttominimizethenumberofnewobjects,andcreatesasetofforwardingmethodsthatunabashedlycreatenewinstancesoftheextractedclass.Theupshotis900new BottleNumber s.
Thiscodeworks,andifyoufinditdistressing,it’slikelybecauseitfeelswasteful.Therearealternatives.Ifunconstrainedbytherecipe,thereareanumberofwaystoavoidsuchprofligateobjectcreation,andit’sinstructivetoconsiderthem.
Forexample,thefirstthreephrasesofthefirstverseofthesongsendquantity and container twice,and action once.Thiscreatesfiveinstancesof BottleNumber forthe number 99.Ifthefirstinstanceweretobecached,itcouldbere-usedfourtimes.
Thefourthphraseofverse99sends quantity and container once,andsuccessor twice,creatingfourinstancesof BottleNumber on number 98.Cachingthefirstinstancewouldsavethreefurtherobjectcreations
withinthisverse.Additionally,thecachedcopycouldalsobeusedwithinthefirstthreephrasesofthefollowingverse,savingfivemoreobjectcreations,foratotalofeightaltogether.Overthecourseofthesong,thiswouldreducethenumberofnew BottleNumber instancesfrom900to100.
Forthosewhofeeltheneedtobeevenmoreparsimonious,it’spossibletocreateasingleinstanceof BottleNumber andreuseit900times.Toaccomplishthis,onewouldcreatea BottleNumber forthenumber 99,andthen,whentheneedforbottlenumber98arose,changethevalueof number from99to98inthatoneexistingobject.Andjustlikethat,you’veaddedcachingplusmutation.
So,youcanavoidcreatingnew BottleNumber sbycachingexistingones,anddecreasethisnumberfurtherifyou’rewillingtomutatethem.Doingeitherofthesethingsmaylowersomecosts,butwillcertainlyraiseothers.Thesethingsarenotfree.
Asathoughtexercise,takeaminutebeforereadingonandimaginealteringtheexistingcodetouseasingleinstanceof BottleNumber .Ifyoufindthatexerciseeasy,tryanother,thistimepretendingthatcontainer , quantity , action ,and successor aresentfrommultiplemethodswithin Bottles .Pauseamoment,ifyoucareto,andgowritethecode.You’llfindthatthechangesneededtodothisaddcomplexity.Thiscomplexitymaycostmorethanthebenefitgainedbyfasterperformance.
Havingdonethatexperiment,returntotheproblemathand.Inthisexample,theforwardingmethodsareinvokedfromonlyonemethodof Bottles .Thismeansthatit’spossibletoreduceobjectcreationbyaddingasimple,automatically-invalidating,low-costcache.Thefollowingexampleshowsa BottleNumber beingcachedonline4:
Listing5.23:CachingaBottleNumber
1 classBottles2 #...
3 defverse(number)4 bottle_number=BottleNumber.new(number)5 6 "#{quantity(number).capitalize}#{container(number)}"+7 "ofbeeronthewall,"+8 "#{quantity(number)}#{container(number)}ofbeer.\n"+9 "#{action(number)},"+10 "#{quantity(successor(number))}#{container(successor(number))}"+11 "ofbeeronthewall.\n"12 end13 #...14 end
Line4abovecreatesanewinstanceof BottleNumber andcachesitinatemporaryvariable(thisistheTemporaryVariablecodesmell)withinthe verse method.Thiscachereducesobjectcreationwithoutaddingmuchadditionalcomplexity,sothebenefitsoutweighthecosts.
Nowthatthiscachedobjectexists,youcangraduallyaltertheversetemplatetosendmessagestothenewobjectratherthantoself.Thenextexamplebeginsthetransitionwiththesimplestchangepossible.Line4belowasksthisnewobjectforits action :
Listing5.24:AskingtheCachedObjectforItsAction
1 defverse(number)2 bottle_number=BottleNumber.new(number)3 #...4 "#{bottle_number.action},"+5 #...
6 end
Inthecodeabove, action(number) hasbeenreplacedbybottle_number.action .Thissendsthe action messagedirectlytothenew BottleNumber ,entirelybypassingthelocalimplementation.
Asimilarchangecanbemadeinthefirstandsecondphrasesoftheverse template,asshownbelow:
Listing5.25:UsingtheCachedObjectinPhrases1and2
Inlines4and5ofthecodeabove, quantity and container arenowsentdirectlyto bottle_number .This,again,bypassesthelocalimplementationsinfavorofsendingmessagestothecachedobject.
NowthefirstthreephrasesoftheversetemplatesendmessagestoaBottleNumber ratherthanto self .Onlyphrasefourremainstobeupdated.
5.6.RecognizingLiskovViolationsPhrases1through3oftheversetemplaterefertothesamebottlenumber,andsocansharethecurrently-cached BottleNumber instance.Phrase4,however,usesadifferentbottlenumber.Here’sareminderofthecode:
1 defverse(number)2 bottle_number=BottleNumber.new(number)3 4 "#{bottle_number.quantity.capitalize}#{bottle_number.container}"+5 "ofbeeronthewall,"+6 "#{bottle_number.quantity}#{bottle_number.container}ofbeer.\n"+7 "#{bottle_number.action},"+8 #...9 end
Listing5.26:CurrentPhrase4
Theplanistochangephrase4tosendmessagestoinstancesofBottleNumber ratherthanto self .Previously,whenmakingasimilarchangetophrase1and2,
wasreplacedwith
Online4above,phrase4alsoinvokes quantity ,butitpassesadifferentargumentthandoesphrase1:
The quantity methodaboveispassed successor(number) becausephrase4isaboutthenextnumber.Forexample,inaversewherephrase1isaboutnumber99,thenphrase4isaboutnumber98.
Thegoalhereistosendthe quantity messagetoanobjectthatcananswercorrectly,andtheproblemisthatyoudonotyethaveaccesstosuchanobject.
BottleNumber simplement successor ,anditfeelsasif successor shouldreturntheobjectyouneed.Yourobject-orientedintuitionisbangon[13]ifyouexpectthe successor ofa BottleNumber tobeanotherBottleNumber .Ifthisweretrue,youcouldreplace:
1 defverse(number)2 bottle_number=BottleNumber.new(number)3 #...4 "#{quantity(successor(number))}#{container(successor(number))}"+5 "ofbeeronthewall.\n"6 end
quantity(number)
bottle_number.quantity
quantity(successor(number))
with:
Unfortunately,asis,thiscodedoesn’twork.Ifyoumaketheabovechangeandrunthetests,you’llsee:
Theproblemisthat successor stillreturnsanumber,whenlogicallyitshouldnowreturnthesucceeding BottleNumber . BottleNumber sknowquantity ,but Fixnum sdonot.
Backwhen successor wasfirstcreated,itwascorrectforittoreturnanumber.ThisabstractionwasidentifiedbytheFlockingRules,whichcalledforcopyingcodefromtheold verse casestatementintothenew successor method.Thecasestatementoriginallyreturnednumbers,thusthe successor methoddidthesame.Atthatpoint,successor wasanumber.
However,the successor methodhasmovedtoanewclass,andtheconceptoncerepresentedbyanumberisnowrepresentedbyaBottleNumber .Thetypeoftheobjecthaschanged,butthe successormethodstillreturnstheoldtype.Youhaveeveryrighttoexpectanymethodnamed successor toreturnanobjectthatimplementsthesameAPIasthereceiver,butalas,this successor methoddoesnot.
ThisinconsistencyisanotherviolationofthegeneralizedLiskovSubstitutionPrinciple.Amethodnamed successor implicitlypromisesthatthethingitreturnswillbehaveliketheobjecttowhichyousentthemessage.Butthis successor methodlies.Itbreaksitspromise,whichforcesthesendertoknowthatthereturnisuntrustworthy,andtotakestepstohandletheviolation.
quantity(successor(number))
bottle_number.successor.quantity
NoMethodError:undefinedmethod`quantity'for99:Fixnum
Asannoyingasthisis,youareinthemiddleofalteringthe versetemplatetosendmessagestoobjects.Thiscurrentrefactoringisalmostcomplete,anditisoftenbettertofinishhorizontalrefactoringsbeforeundertakingverticaltangents.YoucouldveerfromthepathandfixtheLiskovviolation,butinthespiritofcompletingthecurrentthoughtbeforeundertakinganewtask,staythecourse.You’vealreadydeclaredatemporaryvariabletoholdbottlenumber99.Thecurrentproblemcanbesolvedbydeclaringanothervariabletoholdbottlenumber98,andwritingsomeshamelesscode.Online3below,thefollowingexamplebravelydoesjustthat:
Listing5.27:CachingtheSuccessor
1 defverse(number)2 bottle_number=BottleNumber.new(number)3 next_bottle_number=BottleNumber.new(bottle_number.successor)4 5 "#{bottle_number.quantity.capitalize}#{bottle_number.container}"+6 "ofbeeronthewall,"+7 "#{bottle_number.quantity}#{bottle_number.container}ofbeer.\n"+8 "#{bottle_number.action},"+9 "#{next_bottle_number.quantity}#{next_bottle_number.container}"+10 "ofbeeronthewall.\n"11 end
Line3abovecreatesanew BottleNumber onthesuccessoroftheexisting BottleNumber .Ultimately,you’dliketoimprovethislineofcode,butatpresentitsufficestomovethecurrentrefactoringforward.Nowthat next_bottle_number exists,line9canaskitforits
quantity and container .
Afterthatchange,the verse methodcontainstwodistinctparts.Lines5-10abovedefineatemplatewhichqueriesinstancesof BottleNumberfordetails.Lines2and3createnewinstancesof BottleNumber .Line2seemsreasonable,butline3isawkwardbecausetheLiskovviolationforcesyoutoinvoke successor andthenconvertitsreturnintoaBottleNumber yourself.
Thiscompletesthecachingof BottleNumber sinthe verse method,butthere’sonefinalchangetomake.Nowthat verse talksdirectlytoobjectscachedintemporaryvariables,theforwardingmethodsarenolongerneeded.Deletingthemreducesthecodetothefollowing:
Listing5.28:ObsessionCured
1 classBottles2 3 defsong4 verses(99,0)5 end6 7 defverses(starting,ending)8 starting.downto(ending).collect{|i|verse(i)}.join("\n")9 end10 11 defverse(number)12 bottle_number=BottleNumber.new(number)13 next_bottle_number=BottleNumber.new(bottle_number.successor)14 15 "#{bottle_number.quantity.capitalize}#
{bottle_number.container}"+16 "ofbeeronthewall,"+17 "#{bottle_number.quantity}#{bottle_number.container}ofbeer.\n"+18 "#{bottle_number.action},"+19 "#{next_bottle_number.quantity}#{next_bottle_number.container}"+20 "ofbeeronthewall.\n"21 end22 end23 24 classBottleNumber25 attr_reader:number26 definitialize(number)27 @number=number28 end29 30 defcontainer31 ifnumber==132 "bottle"33 else34 "bottles"35 end36 end37 38 defquantity39 ifnumber==040 "nomore"41 else42 number.to_s43 end
44 end45 46 defaction47 ifnumber==048 "Gotothestoreandbuysomemore"49 else50 "Take#{pronoun}downandpassitaround"51 end52 end53 54 defpronoun55 ifnumber==156 "it"57 else58 "one"59 end60 end61 62 defsuccessor63 ifnumber==064 9965 else66 number-167 end68 end69 end
Thiscompletestheextractionofthe BottleNumber class,resolvesthePrimitiveObsessioncodesmell,andheraldstheendofChapter5.
5.7.Summary
Thischaptercontinuedthequesttomake Bottles opentothesix-packrequirement.Itrecognizedthatmanymethodsin Bottlesobsessedon number ,andundertooktheExtractClassrefactoringtocurethisobsession.TherefactoringcreatedanewclassnamedBottleNumber .
Duringthecourseoftherefactoring,conditionalswereexaminedfromanexperiencedOOpractitioners'pointofview.Thischapteralsoexploredtherewardsofmodelingabstractions,thetrade-offsofcaching,theadvantagesofimmutability,andthebenefitsofdeferringperformancetuning.
MostprogrammersarehappierwiththecurrentcodethantheywerewithShamelessGreen,butthisversionisfarfromperfect.ThetotalFlogscore,forexample,hasgoneupagain.FromFlog’spointofview,afterturningoneconditionalintomanybackinChapter4,you’venowcompoundedyoursinsbyintroducinganewclasswhichaddsnonewbehaviorbutincreasesthelengthofthecode.
Also,therearenounittestsfor BottleNumber .ItreliesentirelyonBottle 'stests.
Thecodestillexudesmanysmells(duplication,conditionals,andtemporaryfield,tonameafew).And,finally,itcommitsaLiskovviolationinthe successor method.
Therefactoringsinthisandthepriorchapterwereundertakeninhopesofmakingthecodeopentothesix-packrequirement,butthishasnotyetsucceeded.You’vebeenactinginfaiththatremovingcodesmellswouldeventuallyleadtoopenness.It’spossiblethatyourfaithisbeingtested.
Despitetheimperfectionslistedabove,therearewaysinwhichthecodeisbetter.Therearenowtwoclasses,buteachhasfocusedresponsibilities.Whileit’struethatthewholeisbigger,eachpartiseasytounderstandandreasonabout.
Thecodeisconsistentandregular,andembodiesanextremelystablelandingpointthatsplendidlyenablesthenextrefactoring.
Withthat,ontoChapter6.
6.ReplacingConditionalswithObjectsThischapterispending.
Thisultimatechaptertransformsthecodetobeopentothesix-packrequirement.Alongthewayitresolvesafewmorecodesmells,delvesintomonkey-patchingandmetaprogramming,andutilizesinheritanceandcomposition.
Thecodeendsupbeingsosimpleandexpressivethatthesix-packrequirementismetbyaddingjustafewlinesofnewcode.It’sawondertobehold.
AppendixA:Prerequisites
A.1.RubyThecodeiscompatiblewithanyRubyversionstartingat1.9.CheckwhichversionofRubyyouhavewiththefollowingcommand:
Ifyoudon’thaveRuby1.9orhigherinstalledfollowtheinstructionsonruby-lang.orgtoinstallit.
A.2.MinitestThecodeexamplesincludeaMinitesttestsuite.TocheckwhichversionsofMinitestyouhave,usethe gemlist command.
IfMinitestisnotinstalled,orifnoneoftheversionslistedareinthe5.xseries,installitwith geminstall .
ruby--version
gemlistminitest
geminstallminitest--version"~>5.4"
AppendixB:InitialExercise
B.1.GettingtheexerciseThecodeinthisbookisonGithub.Thesimplestwaytogettheexerciseistoclonetherepositoryandcheckoutthecorrectbranch,asfollows:
Thedirectorystructurefortheexerciseshouldlooklikethis:
Ifyoudon’thavegitinstalled,createtheexpecteddirectorystructure,andthencopyandpastethecontentsoftherawfileonGitHubinto bottles_test.rb .
Finally,ifyoudon’thaveaninternetconnection,youcanfindthefullcodelistingforthetestsuitebelow,intheTestSuitesection.
B.2.DoingtheexerciseThetestsuiteandexercisearewritteninRuby.Ifyou’reunfamiliarwiththelanguage,ruby-lang.orghasinstallationinstructions,agentletutorial(RubyinTwentyMinutes),and
gitclone--depth=1--branch=exercisehttps://github.com/sandimetz/99bottles.git
├──lib│└──bottles.rb
└──test
└──bottles_test.rb
furtherreferences.
Torunthetestsuite,invokeRubywiththepathtothetestfile.
Thetestsuitecontainsonefailingtest,andmanyskippedtests.Yourgoalistowritecodethatpassesallofthetests.Followthisprotocol:
runthetestsandexaminethefailure
writeonlyenoughcodetopassthefailingtest
unskipthenexttest(thissimulateswritingityourself)
Repeattheaboveuntilnotestsisskipped,andyou’vewrittencodetopasseachone.
Workonthistaskfor30minutes.Thevastmajorityoffolksdonotfinishin30minutes,butit’suseful,forlatercomparisonpurposes,torecordhowfaryougot.Evenifyoucan’tforceyourselftostopatthatpoint,takeabreakat30minutesandsaveyourcode.
ReturntoPreface.
ReturntoChapter1.
B.3.TestSuite
rubytest/bottles_test.rb
1 classBottlesTest<Minitest::Test2 deftest_the_first_verse3 expected=<<-VERSE4 99bottlesofbeeronthewall,99bottlesofbeer.5 Takeonedownandpassitaround,98bottlesofbeeronthe
wall.6 VERSE7 assert_equalexpected,::Bottles.new.verse(99)8 end9
10 deftest_another_verse11 skip12 expected=<<-VERSE13 89bottlesofbeeronthewall,89bottlesofbeer.14 Takeonedownandpassitaround,88bottlesofbeeronthewall.15 VERSE16 assert_equalexpected,::Bottles.new.verse(89)17 end18
19 deftest_verse_220 skip21 expected=<<-VERSE22 2bottlesofbeeronthewall,2bottlesofbeer.23 Takeonedownandpassitaround,1bottleofbeeronthewall.24 VERSE25 assert_equalexpected,::Bottles.new.verse(2)26 end27
28 deftest_verse_129 skip30 expected=<<-VERSE31 1bottleofbeeronthewall,1bottleofbeer.32 Takeitdownandpassitaround,nomorebottlesofbeeronthewall.33 VERSE34 assert_equalexpected,::Bottles.new.verse(1)35 end36
37 deftest_verse_038 skip39 expected=<<-VERSE40 Nomorebottlesofbeeronthewall,nomorebottlesofbeer.41 Gotothestoreandbuysomemore,99bottlesofbeeronthewall.42 VERSE43 assert_equalexpected,::Bottles.new.verse(0)44 end45
46 deftest_a_couple_verses47 skip48 expected=<<-VERSES49 99bottlesofbeeronthewall,99bottlesofbeer.50 Takeonedownandpassitaround,98bottlesofbeeronthewall.51
52 98bottlesofbeeronthewall,98bottlesofbeer.53 Takeonedownandpassitaround,97bottlesofbeeronthewall.54 VERSES55 assert_equalexpected,::Bottles.new.verses(99,98)56 end57
58 deftest_a_few_verses59 skip60 expected=<<-VERSES61 2bottlesofbeeronthewall,2bottlesofbeer.62 Takeonedownandpassitaround,1bottleofbeeronthewall.63
64 1bottleofbeeronthewall,1bottleofbeer.65 Takeitdownandpassitaround,nomorebottlesofbeeronthewall.66
67 Nomorebottlesofbeeronthewall,nomorebottlesofbeer.68 Gotothestoreandbuysomemore,99bottlesofbeeronthewall.69 VERSES70 assert_equalexpected,::Bottles.new.verses(2,0)71 end72
73 deftest_the_whole_song74 skip75 expected=<<-SONG76 99bottlesofbeeronthewall,99bottlesofbeer.77 Takeonedownandpassitaround,98bottlesofbeeronthewall.78
79 98bottlesofbeeronthewall,98bottlesofbeer.80 Takeonedownandpassitaround,97bottlesofbeeronthewall.81
82 97bottlesofbeeronthewall,97bottlesofbeer.83 Takeonedownandpassitaround,96bottlesofbeeronthewall.84
85 96bottlesofbeeronthewall,96bottlesofbeer.86 Takeonedownandpassitaround,95bottlesofbeeronthewall.87
88 95bottlesofbeeronthewall,95bottlesofbeer.89 Takeonedownandpassitaround,94bottlesofbeeronthewall.90
91 94bottlesofbeeronthewall,94bottlesofbeer.92 Takeonedownandpassitaround,93bottlesofbeeronthewall.93
94 93bottlesofbeeronthewall,93bottlesofbeer.95 Takeonedownandpassitaround,92bottlesofbeeronthewall.96
97 92bottlesofbeeronthewall,92bottlesofbeer.98 Takeonedownandpassitaround,91bottlesofbeeronthewall.99
100 91bottlesofbeeronthewall,91bottlesofbeer.101 Takeonedownandpassitaround,90bottlesofbeeronthewall.102
103 90bottlesofbeeronthewall,90bottlesofbeer.104 Takeonedownandpassitaround,89bottlesofbeeronthewall.105
106 89bottlesofbeeronthewall,89bottlesofbeer.107 Takeonedownandpassitaround,88bottlesofbeeronthe
wall.108
109 88bottlesofbeeronthewall,88bottlesofbeer.110 Takeonedownandpassitaround,87bottlesofbeeronthewall.111
112 87bottlesofbeeronthewall,87bottlesofbeer.113 Takeonedownandpassitaround,86bottlesofbeeronthewall.114
115 86bottlesofbeeronthewall,86bottlesofbeer.116 Takeonedownandpassitaround,85bottlesofbeeronthewall.117
118 85bottlesofbeeronthewall,85bottlesofbeer.119 Takeonedownandpassitaround,84bottlesofbeeronthewall.120
121 84bottlesofbeeronthewall,84bottlesofbeer.122 Takeonedownandpassitaround,83bottlesofbeeronthewall.123
124 83bottlesofbeeronthewall,83bottlesofbeer.125 Takeonedownandpassitaround,82bottlesofbeeronthewall.126
127 82bottlesofbeeronthewall,82bottlesofbeer.128 Takeonedownandpassitaround,81bottlesofbeeronthewall.129
130 81bottlesofbeeronthewall,81bottlesofbeer.131 Takeonedownandpassitaround,80bottlesofbeeronthewall.132
133 80bottlesofbeeronthewall,80bottlesofbeer.134 Takeonedownandpassitaround,79bottlesofbeeronthewall.135
136 79bottlesofbeeronthewall,79bottlesofbeer.137 Takeonedownandpassitaround,78bottlesofbeeronthewall.138
139 78bottlesofbeeronthewall,78bottlesofbeer.140 Takeonedownandpassitaround,77bottlesofbeeronthewall.141
142 77bottlesofbeeronthewall,77bottlesofbeer.143 Takeonedownandpassitaround,76bottlesofbeeronthewall.144
145 76bottlesofbeeronthewall,76bottlesofbeer.146 Takeonedownandpassitaround,75bottlesofbeeronthewall.147
148 75bottlesofbeeronthewall,75bottlesofbeer.149 Takeonedownandpassitaround,74bottlesofbeeronthewall.150
151 74bottlesofbeeronthewall,74bottlesofbeer.152 Takeonedownandpassitaround,73bottlesofbeeronthewall.153
154 73bottlesofbeeronthewall,73bottlesofbeer.155 Takeonedownandpassitaround,72bottlesofbeeronthewall.156
157 72bottlesofbeeronthewall,72bottlesofbeer.158 Takeonedownandpassitaround,71bottlesofbeeronthewall.159
160 71bottlesofbeeronthewall,71bottlesofbeer.161 Takeonedownandpassitaround,70bottlesofbeeronthewall.162
163 70bottlesofbeeronthewall,70bottlesofbeer.164 Takeonedownandpassitaround,69bottlesofbeeronthewall.165
166 69bottlesofbeeronthewall,69bottlesofbeer.167 Takeonedownandpassitaround,68bottlesofbeeronthe
wall.168
169 68bottlesofbeeronthewall,68bottlesofbeer.170 Takeonedownandpassitaround,67bottlesofbeeronthewall.171
172 67bottlesofbeeronthewall,67bottlesofbeer.173 Takeonedownandpassitaround,66bottlesofbeeronthewall.174
175 66bottlesofbeeronthewall,66bottlesofbeer.176 Takeonedownandpassitaround,65bottlesofbeeronthewall.177
178 65bottlesofbeeronthewall,65bottlesofbeer.179 Takeonedownandpassitaround,64bottlesofbeeronthewall.180
181 64bottlesofbeeronthewall,64bottlesofbeer.182 Takeonedownandpassitaround,63bottlesofbeeronthewall.183
184 63bottlesofbeeronthewall,63bottlesofbeer.185 Takeonedownandpassitaround,62bottlesofbeeronthewall.186
187 62bottlesofbeeronthewall,62bottlesofbeer.188 Takeonedownandpassitaround,61bottlesofbeeronthewall.189
190 61bottlesofbeeronthewall,61bottlesofbeer.191 Takeonedownandpassitaround,60bottlesofbeeronthewall.192
193 60bottlesofbeeronthewall,60bottlesofbeer.194 Takeonedownandpassitaround,59bottlesofbeeronthewall.195
196 59bottlesofbeeronthewall,59bottlesofbeer.197 Takeonedownandpassitaround,58bottlesofbeeronthewall.198
199 58bottlesofbeeronthewall,58bottlesofbeer.200 Takeonedownandpassitaround,57bottlesofbeeronthewall.201
202 57bottlesofbeeronthewall,57bottlesofbeer.203 Takeonedownandpassitaround,56bottlesofbeeronthewall.204
205 56bottlesofbeeronthewall,56bottlesofbeer.206 Takeonedownandpassitaround,55bottlesofbeeronthewall.207
208 55bottlesofbeeronthewall,55bottlesofbeer.209 Takeonedownandpassitaround,54bottlesofbeeronthewall.210
211 54bottlesofbeeronthewall,54bottlesofbeer.212 Takeonedownandpassitaround,53bottlesofbeeronthewall.213
214 53bottlesofbeeronthewall,53bottlesofbeer.215 Takeonedownandpassitaround,52bottlesofbeeronthewall.216
217 52bottlesofbeeronthewall,52bottlesofbeer.218 Takeonedownandpassitaround,51bottlesofbeeronthewall.219
220 51bottlesofbeeronthewall,51bottlesofbeer.221 Takeonedownandpassitaround,50bottlesofbeeronthewall.222
223 50bottlesofbeeronthewall,50bottlesofbeer.224 Takeonedownandpassitaround,49bottlesofbeeronthewall.225
226 49bottlesofbeeronthewall,49bottlesofbeer.227 Takeonedownandpassitaround,48bottlesofbeeronthe
wall.228
229 48bottlesofbeeronthewall,48bottlesofbeer.230 Takeonedownandpassitaround,47bottlesofbeeronthewall.231
232 47bottlesofbeeronthewall,47bottlesofbeer.233 Takeonedownandpassitaround,46bottlesofbeeronthewall.234
235 46bottlesofbeeronthewall,46bottlesofbeer.236 Takeonedownandpassitaround,45bottlesofbeeronthewall.237
238 45bottlesofbeeronthewall,45bottlesofbeer.239 Takeonedownandpassitaround,44bottlesofbeeronthewall.240
241 44bottlesofbeeronthewall,44bottlesofbeer.242 Takeonedownandpassitaround,43bottlesofbeeronthewall.243
244 43bottlesofbeeronthewall,43bottlesofbeer.245 Takeonedownandpassitaround,42bottlesofbeeronthewall.246
247 42bottlesofbeeronthewall,42bottlesofbeer.248 Takeonedownandpassitaround,41bottlesofbeeronthewall.249
250 41bottlesofbeeronthewall,41bottlesofbeer.251 Takeonedownandpassitaround,40bottlesofbeeronthewall.252
253 40bottlesofbeeronthewall,40bottlesofbeer.254 Takeonedownandpassitaround,39bottlesofbeeronthewall.255
256 39bottlesofbeeronthewall,39bottlesofbeer.257 Takeonedownandpassitaround,38bottlesofbeeronthewall.258
259 38bottlesofbeeronthewall,38bottlesofbeer.260 Takeonedownandpassitaround,37bottlesofbeeronthewall.261
262 37bottlesofbeeronthewall,37bottlesofbeer.263 Takeonedownandpassitaround,36bottlesofbeeronthewall.264
265 36bottlesofbeeronthewall,36bottlesofbeer.266 Takeonedownandpassitaround,35bottlesofbeeronthewall.267
268 35bottlesofbeeronthewall,35bottlesofbeer.269 Takeonedownandpassitaround,34bottlesofbeeronthewall.270
271 34bottlesofbeeronthewall,34bottlesofbeer.272 Takeonedownandpassitaround,33bottlesofbeeronthewall.273
274 33bottlesofbeeronthewall,33bottlesofbeer.275 Takeonedownandpassitaround,32bottlesofbeeronthewall.276
277 32bottlesofbeeronthewall,32bottlesofbeer.278 Takeonedownandpassitaround,31bottlesofbeeronthewall.279
280 31bottlesofbeeronthewall,31bottlesofbeer.281 Takeonedownandpassitaround,30bottlesofbeeronthewall.282
283 30bottlesofbeeronthewall,30bottlesofbeer.284 Takeonedownandpassitaround,29bottlesofbeeronthewall.285
286 29bottlesofbeeronthewall,29bottlesofbeer.287 Takeonedownandpassitaround,28bottlesofbeeronthe
wall.288
289 28bottlesofbeeronthewall,28bottlesofbeer.290 Takeonedownandpassitaround,27bottlesofbeeronthewall.291
292 27bottlesofbeeronthewall,27bottlesofbeer.293 Takeonedownandpassitaround,26bottlesofbeeronthewall.294
295 26bottlesofbeeronthewall,26bottlesofbeer.296 Takeonedownandpassitaround,25bottlesofbeeronthewall.297
298 25bottlesofbeeronthewall,25bottlesofbeer.299 Takeonedownandpassitaround,24bottlesofbeeronthewall.300
301 24bottlesofbeeronthewall,24bottlesofbeer.302 Takeonedownandpassitaround,23bottlesofbeeronthewall.303
304 23bottlesofbeeronthewall,23bottlesofbeer.305 Takeonedownandpassitaround,22bottlesofbeeronthewall.306
307 22bottlesofbeeronthewall,22bottlesofbeer.308 Takeonedownandpassitaround,21bottlesofbeeronthewall.309
310 21bottlesofbeeronthewall,21bottlesofbeer.311 Takeonedownandpassitaround,20bottlesofbeeronthewall.312
313 20bottlesofbeeronthewall,20bottlesofbeer.314 Takeonedownandpassitaround,19bottlesofbeeronthewall.315
316 19bottlesofbeeronthewall,19bottlesofbeer.317 Takeonedownandpassitaround,18bottlesofbeeronthewall.318
319 18bottlesofbeeronthewall,18bottlesofbeer.320 Takeonedownandpassitaround,17bottlesofbeeronthewall.321
322 17bottlesofbeeronthewall,17bottlesofbeer.323 Takeonedownandpassitaround,16bottlesofbeeronthewall.324
325 16bottlesofbeeronthewall,16bottlesofbeer.326 Takeonedownandpassitaround,15bottlesofbeeronthewall.327
328 15bottlesofbeeronthewall,15bottlesofbeer.329 Takeonedownandpassitaround,14bottlesofbeeronthewall.330
331 14bottlesofbeeronthewall,14bottlesofbeer.332 Takeonedownandpassitaround,13bottlesofbeeronthewall.333
334 13bottlesofbeeronthewall,13bottlesofbeer.335 Takeonedownandpassitaround,12bottlesofbeeronthewall.336
337 12bottlesofbeeronthewall,12bottlesofbeer.338 Takeonedownandpassitaround,11bottlesofbeeronthewall.339
340 11bottlesofbeeronthewall,11bottlesofbeer.341 Takeonedownandpassitaround,10bottlesofbeeronthewall.342
343 10bottlesofbeeronthewall,10bottlesofbeer.344 Takeonedownandpassitaround,9bottlesofbeeronthewall.345
346 9bottlesofbeeronthewall,9bottlesofbeer.347 Takeonedownandpassitaround,8bottlesofbeeronthe
wall.348
349 8bottlesofbeeronthewall,8bottlesofbeer.350 Takeonedownandpassitaround,7bottlesofbeeronthewall.351
352 7bottlesofbeeronthewall,7bottlesofbeer.353 Takeonedownandpassitaround,6bottlesofbeeronthewall.354
355 6bottlesofbeeronthewall,6bottlesofbeer.356 Takeonedownandpassitaround,5bottlesofbeeronthewall.357
358 5bottlesofbeeronthewall,5bottlesofbeer.359 Takeonedownandpassitaround,4bottlesofbeeronthewall.360
361 4bottlesofbeeronthewall,4bottlesofbeer.362 Takeonedownandpassitaround,3bottlesofbeeronthewall.363
364 3bottlesofbeeronthewall,3bottlesofbeer.365 Takeonedownandpassitaround,2bottlesofbeeronthewall.366
367 2bottlesofbeeronthewall,2bottlesofbeer.368 Takeonedownandpassitaround,1bottleofbeeronthewall.369
370 1bottleofbeeronthewall,1bottleofbeer.371 Takeitdownandpassitaround,nomorebottlesofbeeronthewall.372
373 Nomorebottlesofbeeronthewall,nomorebottlesofbeer.374 Gotothestoreandbuysomemore,99bottlesofbeeronthewall.375 SONG376 assert_equalexpected,::Bottles.new.song377 end378 end
AcknowledgementsWe’regratefultoeveryone,andwilltellyouallaboutitverysoon.
1.FromthenovelbyJosephHeller,acatch-22isaparadoxicalsituationfromwhichyoucannotescapebecauseofcontradictoryrules.
2.Forthoseunfamiliarwiththefairytale,thisisareferencetoeverythingownedbytheLittle,Small,WeeBearinGoldilocks(Goldenlocks)andtheThreeBears
3.ThisquotewashistoricallythoughttooriginatewithMarkTwainbutisnowwidelyattributedtoCharlesDudleyWarner.TwainandWarnerwereneighborsandtheformerapparentlyhearditfromthelatter.
4.AquotefromRobertMartin’sTransformationPriorityPremiseblogpost.5.Aredherringissomethingthatmisleadsordistractsfromarelevantor
importantissue.6.Ahairshirt,orcilice,isanundergarmentmadeofanimalhair,worntoinduce
discomfortasasignofrepentanceoratonement.7.SeeKentBeckDon’tCrosstheBeams:AvoidingInterferenceBetweenHorizontal
andVerticalRefactorings8.ThankstoAvdiGrimmforthesuggestionofusingrowsandcolumnsinan
imaginaryspreadsheettohelpfindnamesforunderlyingconcepts.9.ThankstoTomStuartforthesuggestionthat,whenyou’restrugglingtonamea
conceptforwhichyouhaveonlyafewexamples,itcanhelptoimagineotherconcretethingsthatmightalsofallintothesamecategory.
10."You’llneverknowlessthanyouknowrightnow"isaquotefromKentBeck.11.Spidey(orspider)senseisatinglingfeelingatthebaseofMarvelComics
superheroSpider-Man'sskullthatalertshimtodanger.12.Aquotefrom1Corinthians13:12oftheKingJamesVersionoftheChristian
Bible.13.MerriamWebsterdefinesbangonas"exactlycorrectorappropriate"
0.3Lastupdated2016-10-0611:48:16EDT