Post on 14-Jul-2020
RefactoringImprovingtheDesignofExistingCode
SecondEdition
MartinFowlerwithcontributionsbyKentBeck
ContentsataGlanceForewordtotheFirstEdition
Preface
Chapter1:Refactoring:AFirstExample
Chapter2:PrinciplesinRefactoring
Chapter3:BadSmellsinCode
Chapter4:BuildingTests
Chapter5:IntroducingtheCatalog
Chapter6:AFirstSetofRefactorings
Chapter7:Encapsulation
Chapter8:MovingFeatures
Chapter9:OrganizingData
Chapter10:SimplifyingConditionalLogic
Chapter11:RefactoringAPIs
Chapter12:DealingwithInheritance
Bibliography
ContentsForewordtotheFirstEdition
Preface
WhatIsRefactoring?
What’sinThisBook?
WhoShouldReadThisBook?
BuildingonaFoundationLaidbyOthers
Acknowledgments
Chapter1:Refactoring:AFirstExample
TheStartingPoint
CommentsontheStartingProgram
TheFirstStepinRefactoring
DecomposingthestatementFunction
Status:LotsofNestedFunctions
SplittingthePhasesofCalculationandFormatting
Status:SeparatedintoTwoFiles(andPhases)
ReorganizingtheCalculationsbyType
Status:CreatingtheDatawiththePolymorphicCalculator
FinalThoughts
Chapter2:PrinciplesinRefactoring
DefiningRefactoring
TheTwoHats
WhyShouldWeRefactor?
WhenShouldWeRefactor?
ProblemswithRefactoring
Refactoring,Architecture,andYagni
RefactoringandtheWiderSoftwareDevelopmentProcess
RefactoringandPerformance
WhereDidRefactoringComeFrom?
AutomatedRefactorings
GoingFurther
Chapter3:BadSmellsinCode
MysteriousName
DuplicatedCode
LongFunction
LongParameterList
GlobalData
MutableData
DivergentChange
ShotgunSurgery
FeatureEnvy
DataClumps
PrimitiveObsession
RepeatedSwitches
Loops
LazyElement
SpeculativeGenerality
TemporaryField
MessageChains
MiddleMan
InsiderTrading
LargeClass
AlternativeClasseswithDifferentInterfaces
DataClass
RefusedBequest
Comments
Chapter4:BuildingTests
TheValueofSelf-TestingCode
SampleCodetoTest
AFirstTest
AddAnotherTest
ModifyingtheFixture
ProbingtheBoundaries
MuchMoreThanThis
Chapter5:IntroducingtheCatalog
FormatoftheRefactorings
TheChoiceofRefactorings
Chapter6:AFirstSetofRefactorings
ExtractFunction
InlineFunction
ExtractVariable
InlineVariable
ChangeFunctionDeclaration
EncapsulateVariable
RenameVariable
IntroduceParameterObject
CombineFunctionsintoClass
CombineFunctionsintoTransform
SplitPhase
Chapter7:Encapsulation
EncapsulateRecord
EncapsulateCollection
ReplacePrimitivewithObject
ReplaceTempwithQuery
ExtractClass
InlineClass
HideDelegate
RemoveMiddleMan
SubstituteAlgorithm
Chapter8:MovingFeatures
MoveFunction
MoveField
MoveStatementsintoFunction
MoveStatementstoCallers
ReplaceInlineCodewithFunctionCall
SlideStatements
SplitLoop
ReplaceLoopwithPipeline
RemoveDeadCode
Chapter9:OrganizingData
SplitVariable
RenameField
ReplaceDerivedVariablewithQuery
ChangeReferencetoValue
ChangeValuetoReference
Chapter10:SimplifyingConditionalLogic
DecomposeConditional
ConsolidateConditionalExpression
ReplaceNestedConditionalwithGuardClauses
ReplaceConditionalwithPolymorphism
IntroduceSpecialCase
IntroduceAssertion
Chapter11:RefactoringAPIs
SeparateQueryfromModifier
ParameterizeFunction
RemoveFlagArgument
PreserveWholeObject
ReplaceParameterwithQuery
ReplaceQuerywithParameter
RemoveSettingMethod
ReplaceConstructorwithFactoryFunction
ReplaceFunctionwithCommand
ReplaceCommandwithFunction
Chapter12:DealingwithInheritance
PullUpMethod
PullUpField
PullUpConstructorBody
PushDownMethod
PushDownField
ReplaceTypeCodewithSubclasses
RemoveSubclass
ExtractSuperclass
CollapseHierarchy
ReplaceSubclasswithDelegate
ReplaceSuperclasswithDelegate
Bibliography
ForewordtotheFirstEdition“Refactoring”wasconceivedinSmalltalkcircles,butitwasn’tlongbeforeitfounditswayintootherprogramminglanguagecamps.Becauserefactoringisintegraltoframeworkdevelopment,thetermcomesupquicklywhen“frameworkers”talkabouttheircraft.Itcomesupwhentheyrefinetheirclasshierarchiesandwhentheyraveabouthowmanylinesofcodetheywereabletodelete.Frameworkersknowthataframeworkwon’tberightthefirsttimearound—itmustevolveastheygainexperience.Theyalsoknowthatthecodewillbereadandmodifiedmorefrequentlythanitwillbewritten.Thekeytokeepingcodereadableandmodifiableisrefactoring—forframeworks,inparticular,butalsoforsoftwareingeneral.
So,what’stheproblem?Simplythis:Refactoringisrisky.Itrequireschangestoworkingcodethatcanintroducesubtlebugs.Refactoring,ifnotdoneproperly,cansetyoubackdays,evenweeks.Andrefactoringbecomesriskierwhenpracticedinformallyoradhoc.Youstartdigginginthecode.Soonyoudiscovernewopportunitiesforchange,andyoudigdeeper.Themoreyoudig,themorestuffyouturnup...andthemorechangesyoumake.Eventuallyyoudigyourselfintoaholeyoucan’tgetoutof.Toavoiddiggingyourowngrave,refactoringmustbedonesystematically.WhenmycoauthorsandIwroteDesignPatterns,wementionedthatdesignpatternsprovidetargetsforrefactorings.However,identifyingthetargetisonlyonepartoftheproblem;transformingyourcodesothatyougetthereisanotherchallenge.
MartinFowlerandthecontributingauthorsmakeaninvaluablecontributiontoobject-orientedsoftwaredevelopmentbysheddinglightontherefactoringprocess.Thisbookexplainstheprinciplesandbestpracticesofrefactoring,andpointsoutwhenandwhereyoushouldstartdigginginyourcodetoimproveit.Atthebook’scoreisacomprehensivecatalogofrefactorings.Eachrefactoringdescribesthemotivationandmechanicsofaprovencodetransformation.Someoftherefactorings,suchasExtractMethodorMoveField,mayseemobvious.
Butdon’tbefooled.Understandingthemechanicsofsuchrefactoringsisthekeytorefactoringinadisciplinedway.Therefactoringsinthisbookwillhelpyouchangeyourcodeonesmallstepatatime,thusreducingtherisksofevolvingyourdesign.Youwillquicklyaddtheserefactoringsandtheirnamestoyour
developmentvocabulary.
Myfirstexperiencewithdisciplined,“onestepatatime”refactoringwaswhenIwaspair-programmingat30,000feetwithKentBeck.Hemadesurethatweappliedrefactoringsfromthisbook’scatalogonestepatatime.Iwasamazedathowwellthispracticeworked.Notonlydidmyconfidenceintheresultingcodeincrease,Ialsofeltlessstressed.Ihighlyrecommendyoutrytheserefactorings:Youandyourcodewillfeelmuchbetterforit.
—ErichGamma,ObjectTechnologyInternational,Inc.
PrefaceOnceuponatime,aconsultantmadeavisittoadevelopmentprojectinordertolookatsomeofthecodethathadbeenwritten.Ashewanderedthroughtheclasshierarchyatthecenterofthesystem,theconsultantfounditrathermessy.Thehigher-levelclassesmadecertainassumptionsabouthowtheclasseswouldwork—assumptionsthatwereembodiedininheritedcode.Thatcodedidn’tsuitallthesubclasses,however,andwasoverriddenquiteheavily.Slightmodificationstothesuperclasswouldhavegreatlyreducedtheneedtooverrideit.Inotherplaces,anintentionofthesuperclasshadnotbeenproperlyunderstood,andbehaviorpresentinthesuperclasswasduplicated.Inyetotherplaces,severalsubclassesdidthesamethingwithcodethatcouldclearlybemovedupthehierarchy.
Theconsultantrecommendedtotheprojectmanagementthatthecodebelookedatandcleanedup—buttheprojectmanagementwasn’tenthusiastic.Thecodeseemedtoworkandtherewereconsiderableschedulepressures.Themanagerssaidtheywouldgetaroundtoitatsomelaterpoint.
Theconsultanthadalsoshownwhatwasgoingontotheprogrammersworkingonthehierarchy.Theprogrammerswerekeenandsawtheproblem.Theyknewthatitwasn’treallytheirfault;sometimes,anewpairofeyesisneededtospottheproblem.Sotheprogrammersspentadayortwocleaningupthehierarchy.Whenfinished,theyhadremovedhalfthecodeinthehierarchywithoutreducingitsfunctionality.Theywerepleasedwiththeresultandfoundthatitbecamequickerandeasierbothtoaddnewclassesandtousetheclassesintherestofthesystem.
Theprojectmanagementwasnotpleased.Schedulesweretightandtherewasalotofworktodo.Thesetwoprogrammershadspenttwodaysdoingworkthataddednothingtothemanyfeaturesthesystemhadtodeliverinafewmonths’time.Theoldcodehadworkedjustfine.Yes,thedesignwasabitmore“pure”andabitmore“clean.”Buttheprojecthadtoshipcodethatworked,notcodethatwouldpleaseanacademic.Theconsultantsuggestedthatasimilarcleanupshouldbedoneonothercentralpartsofthesystem,whichmighthalttheprojectforaweekortwo.Allthiswastomakethecodelookbetter,nottomakeitdoanythingitdidn’talreadydo.
Howdoyoufeelaboutthisstory?Doyouthinktheconsultantwasrighttosuggestfurthercleanup?Ordoyoufollowthatoldengineeringadage,“ifitworks,don’tfixit”?
Imustadmittosomebiashere.Iwasthatconsultant.Sixmonthslater,theprojectfailed,inlargepartbecausethecodewastoocomplextodebugortunetoacceptableperformance.
TheconsultantKentBeckwasbroughtintorestarttheproject—anexercisethatinvolvedrewritingalmostthewholesystemfromscratch.Hedidseveralthingsdifferently,butoneofthemostimportantchangeswastoinsistoncontinuouscleaningupofthecodeusingrefactoring.Theimprovedeffectivenessoftheteam,andtherolerefactoringplayed,iswhatinspiredmetowritethefirsteditionofthisbook—soIcouldpassontheknowledgethatKentandothershaveacquiredbyusingrefactoringtoimprovethequalityofsoftware.
Sincethen,refactoringhasbecomeanacceptedpartofthevocabularyofprogramming.Andtheoriginalbookhasstoodupratherwell.However,eighteenyearsisanoldageforaprogrammingbook,soIfeltitwastimetogobackandreworkit.Doingthishadmerewriteprettymucheverypageinthebook.But,inasense,verylittlehaschanged.Theessenceofrefactoringisthesame;mostofthekeyrefactoringsremainessentiallythesame.ButIdohopethattherewritingwillhelpmorepeoplelearnhowtodorefactoringeffectively.
WhatIsRefactoring?
Refactoringistheprocessofchangingasoftwaresysteminawaythatdoesnotaltertheexternalbehaviorofthecodeyetimprovesitsinternalstructure.Itisadisciplinedwaytocleanupcodethatminimizesthechancesofintroducingbugs.Inessence,whenyourefactor,youareimprovingthedesignofthecodeafterithasbeenwritten.
“Improvingthedesignafterithasbeenwritten.”That’sanoddturnofphrase.Formuchofthehistoryofsoftwaredevelopment,mostpeoplebelievedthatwedesignfirst,andonlywhendonewithdesignshouldwecode.Overtime,thecodewillbemodified,andtheintegrityofthesystem—itsstructureaccordingtothatdesign—graduallyfades.Thecodeslowlysinksfromengineeringtohacking.
Refactoringistheoppositeofthispractice.Withrefactoring,wecantakeabad,evenchaotic,designandreworkitintowell-structuredcode.Eachstepissimple—evensimplistic.Imoveafieldfromoneclasstoanother,pullsomecodeoutofamethodtomakeitintoitsownmethod,orpushsomecodeupordownahierarchy.Yetthecumulativeeffectofthesesmallchangescanradicallyimprovethedesign.Itistheexactreverseofthenotionofsoftwaredecay.
Withrefactoring,thebalanceofworkchanges.Ifoundthatdesign,ratherthanoccurringallupfront,occurscontinuouslyduringdevelopment.AsIbuildthesystem,Ilearnhowtoimprovethedesign.Theresultofthisinteractionisaprogramwhosedesignstaysgoodasdevelopmentcontinues.
What’sinThisBook?
Thisbookisaguidetorefactoring;itiswrittenforaprofessionalprogrammer.Myaimistoshowyouhowtodorefactoringinacontrolledandefficientmanner.Youwilllearntorefactorinsuchawaythatyoudon’tintroducebugsintothecodebutmethodicallyimproveitsstructure.
Traditionally,abookstartswithanintroduction.Iagreewiththatinprinciple,butIfindithardtointroducerefactoringwithageneralizeddiscussionordefinitions—soIstartwithanexample.Chapter1takesasmallprogramwithsomecommondesignflawsandrefactorsitintoaprogramthat’seasiertounderstandandchange.Thiswillshowyouboththeprocessofrefactoringandanumberofusefulrefactorings.Thisisthekeychaptertoreadifyouwanttounderstandwhatrefactoringreallyisabout.
InChapter2,Icovermoreofthegeneralprinciplesofrefactoring,somedefinitions,andthereasonsfordoingrefactoring.Ioutlinesomeofthechallengeswithrefactoring.InChapter3,KentBeckhelpsmedescribehowtofindbadsmellsincodeandhowtocleanthemupwithrefactorings.Testingplaysaveryimportantroleinrefactoring,soChapter4describeshowtobuildtestsintocode.
Theheartofthebook—thecatalogofrefactorings—takesuptherestofitsvolume.Whilethisisbynomeansacomprehensivecatalog,itcoversthekeyrefactoringsthatmostdeveloperswilllikelyneed.ItgrewfromthenotesImadewhenlearningaboutrefactoringinthelate1990s,andIstillusethesenotesnowasIdon’trememberthemall.WhenIwanttodosomething,suchasSplitPhase
(154),thecatalogremindsmehowtodoitinasafe,step-by-stepmanner.Ihopethisisthepartofthebookthatyou’llcomebacktooften.
AWeb-FirstBook
TheWorld-WideWebhasmadeanenormousimpactonoursociety,particularlyaffectinghowwegatherinformation.WhenIwrotethisbook,mostoftheknowledgeaboutsoftwaredevelopmentwastransferredthroughprint.NowIgathermostofmyinformationonline.Thishaspresentedachallengeforauthorslikemyself:Istherestillaroleforbooks,andwhatshouldtheylooklike?
Ibelievetherestillisroleforbookslikethis—buttheyneedtochange.Thevalueofabookisalargebodyofknowledgeputtogetherinacohesivefashion.Inwritingthisbook,Itriedtocovermanydifferentrefactoringsandorganizetheminaconsistentandintegratedmanner.
Butthatintegratedwholeisanabstractliteraryworkthat,whiletraditionallyrepresentedbyapaperbook,neednotbeinthefuture.Mostofthebookindustrystillseesthepaperbookastheprimaryrepresentation,andwhilewe’veenthusiasticallyadoptedebooks,theyarejustelectronicrepresentationsofanoriginalworkbasedonthestructureofapaperbook.
Withthisbook,I’mexploringadifferentapproach.Thecanonicalformofthisbookisitswebsite.Thepaperbookisaselectionofmaterialfromthewebsite,arrangedinamannerthatmakessenseforprint.Itdoesn’tattempttoincludealltherefactoringsonthewebsite,particularlysinceImaywelladdmorerefactoringstothecanonicalwebbookinthefuture.Similarly,theebookisadifferentrepresentationofthewebbookthatmaynotincludethesamesetofrefactoringsastheprintedbook—afterall,ebooksdon’tgetheavyasIaddpagesandtheycanbeeasilyupdatedaftertheyarebought.
Idon’tknowwhetheryou’rereadingthisonthewebsite,inanebook,onpaper,orinsomeotherformIcan’timagineasIwritethis.Idomybesttomakethisausefulwork,whateverwayyouwishtoabsorbit.
JavaScriptExamples
Asinmosttechnicalareasofsoftwaredevelopment,codeexamplesareveryimportanttoillustratetheconcepts.However,therefactoringslookmostlythe
sameindifferentlanguages.Therewillsometimesbeparticularthingsthatalanguageforcesmetopayattentionto,butthecoreelementsoftherefactoringsremainthesame.
IchoseJavaScripttoillustratetheserefactorings,asIfeltthatthislanguagewouldbereadablebythemostamountofpeople.Youshouldn’tfinditdifficult,however,toadapttherefactoringstowhateverlanguagetheyarecurrentlyusing.Itrynottouseanyofthemorecomplicatedbitsofthelanguage,soyoushouldbeabletofollowtherefactoringswithonlyacursoryknowledgeofJavaScript.MyuseofJavaScriptiscertainlynotanendorsementofthelanguage.
AlthoughIuseJavaScriptformyexamples,thatdoesn’tmeanthetechniquesinthisbookareconfinedtoJavaScript.ThefirsteditionofthisbookusedJava,andmanyprogrammersfounditusefuleventhoughtheyneverwroteasingleJavaclass.Ididtoywithillustratingthisgeneralitybyusingadozendifferentlanguagesfortheexamples,butIfeltthatwouldbetooconfusingforthereader.Still,thisbookiswrittenforprogrammersinanylanguage.Outsideoftheexamplesections,I’mnotmakinganyassumptionsaboutthelanguage.Iexpectthereadertoabsorbmygeneralcommentsandapplythemtothelanguagetheyareusing.Indeed,IexpectreaderstotaketheJavaScriptexamplesandadaptthemtotheirlanguage.
Thismeansthat,apartfromdiscussingspecificexamples,whenItalkabout“class”,“module”,“function,”etc.,Iusethosetermsinthegeneralprogrammingmeaning,notasspecifictermsoftheJavaScriptlanguagemodel.
ThefactthatI’musingJavaScriptastheexamplelanguagealsomeansthatItrytoavoidJavaScriptstylesthatwillbelessfamiliartothosewhoaren’tregularJavaScriptprogrammers.Thisisnota“refactoringinJavaScript”book—rather,it’sageneralrefactoringbookthathappenstouseJavaScript.TherearemanyinterestingrefactoringsthatarespecifictoJavaScript(suchasrefactoringfromcallbacks,topromises,toasync/await)buttheyareoutofscopeforthisbook.
WhoShouldReadThisBook?
I’veaimedthisbookataprofessionalprogrammer—someonewhowritessoftwareforaliving.Theexamplesanddiscussionincludealotofcodetoreadandunderstand.TheexamplesareinJavaScript,butshouldbeapplicabletomostlanguages.Iwouldexpectaprogrammertohavesomeexperienceto
appreciatewhat’sgoingonwiththisbook,butIdon’tassumemuchknowledge.
Althoughtheprimarytargetofthisbookisadeveloperseekingtolearnaboutrefactoring,thisbookisalsovaluableforsomeonewhoalreadyunderstandsrefactoring—itcanbeusedasateachingaid.Inthisbook,I’veputalotofeffortintoexplaininghowvariousrefactoringswork,soanexperienceddevelopercanusethismaterialinmentoringtheircolleagues.
Althoughitisfocusedonthecode,refactoringhasalargeimpactonthedesignofsystem.Itisvitalforseniordesignersandarchitectstounderstandtheprinciplesofrefactoringandtousethemintheirprojects.Refactoringisbestintroducedbyarespectedandexperienceddeveloper.Suchadevelopercanbestunderstandtheprinciplesbehindrefactoringandadaptthoseprinciplestothespecificworkplace.ThisisparticularlytruewhenyouareusingalanguageotherthanJavaScript,becauseyou’llhavetoadapttheexamplesI’vegiventootherlanguages.
Here’showtogetthemostfromthisbookwithoutreadingallofit.
Ifyouwanttounderstandwhatrefactoringis,readChapter1—theexampleshouldmaketheprocessclear.
Ifyouwanttounderstandwhyyoushouldrefactor,readthefirsttwochapters.Theywilltellyouwhatrefactoringisandwhyyoushoulddoit.
Ifyouwanttofindwhereyoushouldrefactor,readChapter3.Ittellsyouthesignsthatsuggesttheneedforrefactoring.
Ifyouwanttoactuallydorefactoring,readthefirstfourchapterscompletely,thenskip-readthecatalog.Readenoughofthecatalogtoknow,roughly,whatisinthere.Youdon’thavetounderstandallthedetails.Whenyouactuallyneedtocarryoutarefactoring,readtherefactoringindetailanduseittohelpyou.Thecatalogisareferencesection,soyouprobablywon’twanttoreaditinonego.
Animportantpartofwritingthisbookwasnamingthevariousrefactorings.Terminologyhelpsuscommunicate,sothatwhenonedeveloperadvisesanothertoextractsomecodeintoafunction,ortosplitsomecomputationintoseparatephases,bothunderstandthereferencestoExtractFunction(106)andSplitPhase(154).Thisvocabularyalsohelpsinselectingautomatedrefactorings.
BuildingonaFoundationLaidbyOthers
IneedtosayrightatthebeginningthatIoweabigdebtwiththisbook—adebttothosewhoseworkinthe1990sdevelopedthefieldofrefactoring.Itwaslearningfromtheirexperiencethatinspiredandinformedmetowritethefirsteditionofthisbook,andalthoughmanyyearshavepassed,it’simportantthatIcontinuetoacknowledgethefoundationthattheylaid.Ideally,oneofthemshouldhavewrittenthatfirstedition,butIendedupbeingtheonewiththetimeandenergy.
TwooftheleadingearlyproponentsofrefactoringwereWardCunninghamandKentBeck.Theyuseditasafoundationofdevelopmentintheearlydaysandadaptedtheirdevelopmentprocessestotakeadvantageofit.Inparticular,itwasmycollaborationwithKentthatshowedmetheimportanceofrefactoring—aninspirationthatleddirectlytothisbook.
RalphJohnsonleadsagroupattheUniversityofIllinoisatUrbana-Champaignthatisnotableforitspracticalcontributionstoobjecttechnology.Ralphhaslongbeenachampionofrefactoring,andseveralofhisstudentsdidvitalearlyworkinthisfield.BillOpdykedevelopedthefirstdetailedwrittenworkonrefactoringinhisdoctoralthesis.JohnBrantandDonRobertswentbeyondwritingwords—theycreatedthefirstautomatedrefactoringtool,theRefactoringBrowser,forrefactoringSmalltalkprograms.
Manypeoplehaveadvancedthefieldofrefactoringsincethefirsteditionofthisbook.Inparticular,theworkofthosewhohaveaddedautomatedrefactoringstodevelopmenttoolshavecontributedenormouslytomakingprogrammers’liveseasier.It’seasyformetotakeitforgrantedthatIcanrenameawidelyusedfunctionwithasimplekeysequence—butthateasereliesontheeffortsofIDEteamswhoseworkhelpsusall.
Acknowledgments
Chapter1Refactoring:AFirstExampleHowdoIbegintotalkaboutrefactoring?Thetraditionalwayisbyintroducingthehistoryofthesubject,broadprinciples,andthelike.Whensomebodydoesthatataconference,Igetslightlysleepy.Mymindstartswandering,withalow-prioritybackgroundprocesspollingthespeakeruntiltheygiveanexample.
TheexampleswakemeupbecauseIcanseewhatisgoingon.Withprinciples,itistooeasytomakebroadgeneralizations—andtoohardtofigureouthowtoapplythings.Anexamplehelpsmakethingsclear.
SoI’mgoingtostartthisbookwithanexampleofrefactoring.I’lltalkabouthowrefactoringworksandwillgiveyouasenseoftherefactoringprocess.Icanthendotheusualprinciples-styleintroductioninthenextchapter.
Withanyintroductoryexample,however,Irunintoaproblem.IfIpickalargeprogram,describingitandhowitisrefactoredistoocomplicatedforamortalreadertoworkthrough.(Itriedthiswiththeoriginalbook—andendedupthrowingawaytwoexamples,whichwerestillprettysmallbuttookoverahundredpageseachtodescribe.)However,ifIpickaprogramthatissmallenoughtobecomprehensible,refactoringdoesnotlooklikeitisworthwhile.
I’mthusintheclassicbindofanyonewhowantstodescribetechniquesthatareusefulforreal-worldprograms.Frankly,itisnotworththeefforttodoalltherefactoringthatI’mgoingtoshowyouonthesmallprogramIwillbeusing.ButifthecodeI’mshowingyouispartofalargersystem,thentherefactoringbecomesimportant.Justlookatmyexampleandimagineitinthecontextofamuchlargersystem.
TheStartingPoint
Inthefirsteditionofthisbook,mystartingprogramprintedabillfromavideorentalstore,whichmaynowleadmanyofyoutoask:“What’savideorentalstore?”Ratherthananswerthatquestion,I’vere-skinnedtheexampletosomethingthatisbotholderandstillcurrent.
Imageacompanyoftheatricalplayerswhogoouttovariouseventsperformingplays.Typically,acustomerwillrequestafewplaysandthecompanychargesthembasedonthesizeoftheaudienceandthekindofplaytheyperform.Therearecurrentlytwokindsofplaysthatcompanyperforms:tragediesandcomedies.Aswellasprovidingabillfortheperformance,thecompanygivesitscustomers“volumecredits”whichtheycanusefordiscountsonfutureperformances—thinkofitasacustomerloyaltymechanism.
TheperformersstoredataabouttheirplaysinasimpleJSONfilethatlookssomethinglikethis:
plays.json…
{
"hamlet":{"name":"Hamlet","type":"tragedy"},
"as-like":{"name":"AsYouLikeIt","type":"comedy"},
"othello":{"name":"Othello","type":"tragedy"}
}
ThedatafortheirbillsalsocomesinaJSONfile:
invoices.json…
[
{
"customer":"BigCo",
"performances":[
{
"playID":"hamlet",
"audience":55
},
{
"playID":"as-like",
"audience":35
},
{
"playID":"othello",
"audience":40
}
]
}
]
Thecodethatprintsthebillisthissimplefunction.
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
constplay=plays[perf.playID];
letthisAmount=0;
switch(play.type){
case"tragedy":
thisAmount=40000;
if(perf.audience>30){
thisAmount+=1000*(perf.audience-30);
}
break;
case"comedy":
thisAmount=30000;
if(perf.audience>20){
thisAmount+=10000+500*(perf.audience-20);
}
thisAmount+=300*perf.audience;
break;
default:
thrownewError(`unknowntype:${play.type}`);
}
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
}
Runningthatcodeonthetestdatafilesaboveresultsinthefollowingoutput.
StatementforBigCo
Hamlet:$650.00(55seats)
AsYouLikeIt:$580.00(35seats)
Othello:$500.00(40seats)
Amountowedis$1,730.00
Youearned47credits
CommentsontheStartingProgram
Whatareyourthoughtsonthedesignofthisprogram?ThefirstthingI’dsayisthatit’stolerableasitis—aprogramsoshortdoesn’trequireanydeepstructuretobecomprehensible.ButremembermyearlierpointthatIhavetokeepexamplessmall.Imaginethisprogramonalargerscale—perhapshundredsoflineslong.Atthatsize,asingleinlinefunctionishardtounderstand.
Giventhattheprogramworks,isn’tanystatementaboutitsstructuremerelyanaestheticjudgment,adislikeof“ugly”code?Afterall,thecompilerdoesn’tcarewhetherthecodeisuglyorclean.ButwhenIchangethesystem,thereisahumaninvolved,andhumansdocare.Apoorlydesignedsystemishardtochange—becauseitisdifficulttofigureoutwhattochangeandhowthesechangeswillinteractwiththeexistingcodetogetthebehaviorIwant.Andifitishardtofigureoutwhattochange,thereisagoodchancethatIwillmakemistakesandintroducebugs.
Thus,ifI’mfacedwithmodifyingaprogramwithhundredsoflinesofcode,I’dratheritbestructuredintoasetoffunctionsandotherprogramelementsthatallowmetounderstandmoreeasilywhattheprogramisdoing.Iftheprogramlacksstructure,it’susuallyeasierformetoaddstructuretotheprogramfirst,andthenmakethechangeIneed.
Whenyouhavetoaddafeaturetoaprogrambutthecodeisnotstructuredinaconvenientway,firstrefactortheprogramtomakeiteasytoaddthefeature,thenaddthefeature.
Inthiscase,Ihaveacoupleofchangesthattheuserswouldliketomake.First,theywantastatementprintedinHTML.Considerwhatimpactthischangewouldhave.I’mfacedwithaddingconditionalstatementsaroundeverystatementthataddsastringtotheresult.Thatwilladdahostofcomplexitytothefunction.Facedwiththat,mostpeopleprefertocopythemethodandchangeittoemitHTML.Makingacopymaynotseemtooonerousatask,butitsetsupallsortsofproblemsforthefuture.Anychangestothecharginglogicwouldforcemetoupdatebothmethods—andtoensuretheyareupdatedconsistently.IfI’mwritingaprogramthatwillneverchangeagain,thiskindofcopy-and-
pasteisfine.Butifit’salong-livedprogram,thenduplicationisamenace.
Thisbringsmetoasecondchange.Theplayersarelookingtoperformmorekindsofplays:theyhopetoaddhistory,pastoral,pastoral-comical,historical-pastoral,tragical-historical,tragical-comical-historical-pastoral,sceneindividable,andpoemunlimitedtotheirrepertoire.Theyhaven’texactlydecidedyetwhattheywanttodoandwhen.Thischangewillaffectboththewaytheirplaysarechargedforandthewayvolumecreditsarecalculated.AsanexperienceddeveloperIcanbesurethatwhateverschemetheycomeupwith,theywillchangeitagainwithinsixmonths.Afterall,whenfeaturerequestscome,theycomenotassinglespiesbutinbattalions.
Again,thatstatementmethodiswherethechangesneedtobemadetodealwithchangesinclassificationandchargingrules.ButifIcopystatementtohtmlStatement,I’dneedtoensurethatanychangesareconsistent.Furthermore,astherulesgrowincomplexity,it’sgoingtobehardertofigureoutwheretomakethechangesandhardertodothemwithoutmakingamistake.
Letmestressthatit’sthesechangesthatdrivetheneedtoperformrefactoring.Ifthecodeworksanddoesn’teverneedtochange,it’sperfectlyfinetoleaveitalone.Itwouldbenicetoimproveit,butunlesssomeoneneedstounderstandit,itisn’tcausinganyrealharm.Yetassoonassomeonedoesneedtounderstandhowthatcodeworks,andstrugglestofollowit,thenyouhavetodosomethingaboutit.
TheFirstStepinRefactoring
WheneverIdorefactoring,thefirststepisalwaysthesame.IneedtoensureIhaveasolidsetoftestsforthatsectionofcode.ThetestsareessentialbecauseeventhoughIwillfollowrefactoringsstructuredtoavoidmostoftheopportunitiesforintroducingbugs,I’mstillhumanandstillmakemistakes.Thelargeraprogram,themorelikelyitisthatmychangeswillcausesomethingtobreakinadvertently—inthedigitalage,frailty’snameissoftware.
Sincethestatementreturnsastring,whatIdoiscreateafewinvoices,giveeachinvoiceafewperformancesofvariouskindsofplays,andgeneratethestatementstrings.IthendoastringcomparisonbetweenthenewstringandsomereferencestringsthatIhavehand-checked.IsetupallofthesetestsusingatestingframeworksoIcanrunthemwithjustasimplekeystrokeinmy
developmentenvironment.Theteststakeonlyafewsecondstorun,andasyouwillsee,Irunthemoften.
Animportantpartofthetestsisthewaytheyreporttheirresults.Theyeithergogreen,meaningthatallthestringsareidenticaltothereferencestrings,orred,showingalistoffailures—thelinesthatturnedoutdifferently.Thetestsarethusself-checking.Itisvitaltomaketestsself-checking.IfIdon’t,I’dendupspendingtimehand-checkingvaluesfromthetestagainstvaluesonadeskpad,andthatwouldslowmedown.Moderntestingframeworksprovideallthefeaturesneededtowriteandrunself-checkingtests.
Beforeyoustartrefactoring,makesureyouhaveasolidsuiteoftests.Thesetestsmustbeself-checking.
AsIdotherefactoring,I’llleanonthetests.Ithinkofthemasabugdetectortoprotectmeagainstmyownmistakes.BywritingwhatIwanttwice,inthecodeandinthetest,Ihavetomakethemistakeconsistentlyinbothplacestofoolthedetector.Bydouble-checkingmywork,Ireducethechanceofdoingsomethingwrong.Althoughittakestimetobuildthetests,Iendupsavingthattime,withconsiderableinterest,byspendinglesstimedebugging.ThisissuchanimportantpartofrefactoringthatIdevoteafullchaptertoit(BuildingTests,p.85).
DecomposingthestatementFunction
Whenrefactoringalongfunctionlikethis,Imentallytrytoidentifypointsthatseparatedifferentpartsoftheoverallbehavior.Thefirstchunkthatleapstomyeyeistheswitchstatementinthemiddle.
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
constplay=plays[perf.playID];
letthisAmount=0;
switch(play.type){
case"tragedy":
thisAmount=40000;
if(perf.audience>30){
thisAmount+=1000*(perf.audience-30);
}
break;
case"comedy":
thisAmount=30000;
if(perf.audience>20){
thisAmount+=10000+500*(perf.audience-20);
}
thisAmount+=300*perf.audience;
break;
default:
thrownewError(`unknowntype:${play.type}`);
}
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
}
AsIlookatthischunk,Iconcludethatit’scalculatingthechargeforoneperformance.Thatconclusionisapieceofinsightaboutthecode.ButasWardCunninghamputsit,thisunderstandingisinmyhead—anotoriouslyvolatileformofstorage.Ineedtopersistitbymovingitfrommyheadbackintothecodeitself.Thatway,shouldIcomebacktoitlater,thecodewilltellmewhatit’sdoing—Idon’thavetofigureitoutagain.
Thewaytoputthatunderstandingintocodeistoturnthatchunkofcodeintoitsownfunction,namingitafterwhatitdoes—somethinglikeamountFor(aPerformance).WhenIwanttoturnachunkofcodeintoafunctionlikethis,Ihaveaprocedurefordoingitthatminimizesmychancesofgettingitwrong.Iwrotedownthisprocedureand,tomakeiteasytoreference,nameditExtractFunction(106).
First,IneedtolookinthefragmentforanyvariablesthatwillnolongerbeinscopeonceI’veextractedthecodeintoitsownfunction.Inthiscase,Ihave
three:perf,play,andthisAmount.Thefirsttwoareusedbytheextractedcode,butnotmodified,soIcanpasstheminasparameters.Modifiedvariablesneedmorecare.Here,thereisonlyone,soIcanreturnit.Icanalsobringitsinitializationinsidetheextractedcode.Allofwhichyieldsthis:
functionstatement…
functionamountFor(perf,play){
letthisAmount=0;
switch(play.type){
case"tragedy":
thisAmount=40000;
if(perf.audience>30){
thisAmount+=1000*(perf.audience-30);
}
break;
case"comedy":
thisAmount=30000;
if(perf.audience>20){
thisAmount+=10000+500*(perf.audience-20);
}
thisAmount+=300*perf.audience;
break;
default:
thrownewError(`unknowntype:${play.type}`);
}
returnthisAmount;
}
WhenIuseaheaderlike“functionsomeName…”initalicsforsomecode,thatmeansthatthefollowingcodeiswithinthescopeofthefunction,file,orclassnamedintheheader.ThereisusuallyothercodewithinthatscopethatIwon’tshow,asI’mnotdiscussingitatthemoment.
TheoriginalstatementcodenowcallsthisfunctiontopopulatethisAmount:
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
constplay=plays[perf.playID];
letthisAmount=amountFor(perf,play);
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
OnceI’vemadethischange,IimmediatelycompileandtesttoseeifI’vebrokenanything.It’sanimportanthabittotestaftereveryrefactoring,howeversimple.Mistakesareeasytomake—atleast,Ifindthemeasytomake.TestingaftereachchangemeansthatwhenImakeamistake,Ionlyhaveasmallchangetoconsiderinordertospottheerror,whichmakesitfareasiertofindandfix.Thisistheessenceoftherefactoringprocess:smallchangesandtestingaftereachchange.IfItrytodotoomuch,makingamistakewillforcemeintoatrickydebuggingepisodethatcantakealongtime.Smallchanges,enablingatightfeedbackloop,arethekeytoavoidingthatmess.
IusecompileheretomeandoingwhateverisneededtomaketheJavaScriptexecutable.SinceJavaScriptisdirectlyexecutable,thatmaymeannothing,butinothercasesitmaymeanmovingcodetoanoutputdirectoryand/orusingaprocessorsuchasBabel[bib-babel].
Refactoringchangestheprogramsinsmallsteps,soifyoumakeamistake,itiseasytofindwherethebugis.
ThisbeingJavaScript,IcanextractamountForintoanestedfunctionofstatement.ThisishelpfulasitmeansIdon’thavetopassdatathat’sinsidethescopeofthecontainingfunctiontothenewlyextractedfunction.Thatdoesn’tmakeadifferenceinthiscase,butit’sonelessissuetodealwith.
Inthiscasethetestspassed,somynextstepistocommitthechangetomylocalversioncontrolsystem.Iuseaversioncontrolsystem,suchasgitormercurial,thatallowsmetomakeprivatecommits.Icommitaftereachsuccessfulrefactoring,soIcaneasilygetbacktoaworkingstateshouldImessuplater.I
thensquashchangesintomoresignificantcommitsbeforeIpushthechangestoasharedrepository.
ExtractFunction(106)isacommonrefactoringtoautomate.IfIwasprogramminginJava,IwouldhaveinstinctivelyreachedforthekeysequenceformyIDEtoperformthisrefactoring.AsIwritethis,thereisnosuchrobustsupportforthisrefactoringinJavaScripttools,soIhavetodothismanually.It’snothard,althoughIhavetobecarefulwiththoselocallyscopedvariables.
OnceI’veusedExtractFunction(106),ItakealookatwhatI’veextractedtoseeifthereareanyquickandeasythingsIcandotoclarifytheextractedfunction.ThefirstthingIdoisrenamesomeofthevariablestomakethemclearer,suchaschangingthisAmounttoresult
functionstatement…
functionamountFor(perf,play){
letresult=0;
switch(play.type){
case"tragedy":
result=40000;
if(perf.audience>30){
result+=1000*(perf.audience-30);
}
break;
case"comedy":
result=30000;
if(perf.audience>20){
result+=10000+500*(perf.audience-20);
}
result+=300*perf.audience;
break;
default:
thrownewError(`unknowntype:${play.type}`);
}
returnresult;
}
It’smycodingstandardtoalwayscallthereturnvaluefromafunction“result”.ThatwayIalwaysknowitsrole.Again,Icompile,test,andcommit.ThenImoveontothefirstargument.
functionstatement…
functionamountFor(aPerformance,play){
letresult=0;
switch(play.type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${play.type}`);
}
returnresult;
}
Again,thisisfollowingmycodingstyle.WithadynamicallytypedlanguagesuchasJavaScript,it’susefultokeeptrackoftypes—hence,mydefaultnameforaparameterincludesthetypename.Iuseanindefinitearticlewithitunlessthereissomespecificroleinformationtocaptureinthename.IlearnedthisconventionfromKentBeck[bib-beck-sbpp]andcontinuetofindithelpful.
Anyfoolcanwritecodethatacomputercanunderstand.Goodprogrammerswritecodethathumanscanunderstand.
Isthisrenamingworththeeffort?Absolutely.Goodcodeshouldclearlycommunicatewhatitisdoing,andvariablenamesareakeytoclearcode.Neverbeafraidtochangenamestoimproveclarity.Withgoodfind-and-replacetools,itisusuallynotdifficult;testing,andstatictypinginalanguagethatsupportsit,willhighlightanyoccurrencesyoumiss.Andwithautomatedrefactoringtools,it’strivialtorenameevenwidelyusedfunctions.
Thenextitemtoconsiderforrenamingistheplayparameter,butIhaveadifferentfateforthat.
RemovingtheplayVariable
AsIconsidertheparameterstoamountFor,Ilooktoseewheretheycomefrom.aPerformancecomesfromtheloopvariable,sonaturallychangeswitheachiterationthroughtheloop.Butplayiscomputedfromtheperformance,sothere’snoneedtopassitinasaparameteratall—IcanjustrecalculateitwithinamountFor.WhenI’mbreakingdownalongfunction,Iliketogetridofvariableslikeplay,becausetemporaryvariablescreatealotoflocallyscopednamesthatcomplicateextractions.TherefactoringIwillusehereisReplaceTempwithQuery(176).
Ibeginbyextractingtheright-handsideoftheassignmentintoafunction.
functionstatement…
functionplayFor(aPerformance){
returnplays[aPerformance.playID];
}
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
constplay=playFor(perf);
letthisAmount=amountFor(perf,play);
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Icompile-test-commit,andthenuseInlineVariable(123).
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
constplay=playFor(perf);
letthisAmount=amountFor(perf,playFor(perf));
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Icompile-test-commit.Withthatinlined,IcanthenapplyChangeFunctionDeclaration(124)toamountFortoremovetheplayparameter.Idothisintwosteps.First,IusethenewfunctioninsideamountFor.
functionstatement…
functionamountFor(aPerformance,play){
letresult=0;
switch(playFor(aPerformance).type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${playFor(aPerformance).type}`);
}
returnresult;
}
Icompile-test-commit,andthendeletetheparameter.
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
letthisAmount=amountFor(perf,playFor(perf));
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;
totalAmount+=thisAmount;
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
functionstatement…
functionamountFor(aPerformance,play){
letresult=0;
switch(playFor(aPerformance).type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${playFor(aPerformance).type}`);
}
returnresult;
}
Andcompile-test-commitagain.
Thisrefactoringalarmssomeprogrammers.Previously,thecodetolookuptheplaywasexecutedonceineachloopiteration;now,it’sexecutedthrice.I’lltalkabouttheinterplayofrefactoringandperformancelater,butforthemomentI’lljustobservethatthischangeisunlikelytosignificantlyaffectperformance,andevenifitwere,itismucheasiertoimprovetheperformanceofawell-factoredcodebase.
Thegreatbenefitofremovinglocalvariablesisthatitmakesitmucheasiertodoextractions,sincethereislesslocalscopetodealwith.Indeed,usuallyI’lltakeoutlocalvariablesbeforeIdoanyextractions.
NowthatI’mdonewiththeargumentstoamountFor,Ilookbackatwhereit’scalled.It’sbeingusedtosetatemporaryvariablethat’snotupdatedagain,soIinlinethatvariable.
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
ExtractingVolumeCredits
Here’sthecurrentstateofthestatementfunctionbody.
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
//addvolumecredits
volumeCredits+=Math.max(perf.audience-30,0);
//addextracreditforeverytencomedyattendees
if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
NowIgetthebenefitfromremovingtheplayvariableasitmakesiteasiertoextractthevolumecreditscalculationbyremovingoneofthelocallyscopedvariables.
Istillhavetodealwiththeothertwo.Again,perfiseasytopassin,butvolumeCreditsisabitmoretrickyasitisanaccumulatorupdatedineachpassoftheloop.Somybestbetistoinitializeashadowofitinsidetheextractedfunctionandreturnit.
functionstatement…
functionvolumeCreditsFor(perf){
letvolumeCredits=0;
volumeCredits+=Math.max(perf.audience-30,0);
if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);
returnvolumeCredits;
}
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Iremovetheunnecessary(and,inthiscase,downrightmisleading)comment.
Icompile-test-committhat,andthenrenamethevariablesinsidethenewfunction.
functionstatement…
functionvolumeCreditsFor(aPerformance){
letresult=0;
result+=Math.max(aPerformance.audience-30,0);
if("comedy"===playFor(aPerformance).type)result+=Math.floor(aPerformance.audience/5);
returnresult;
}
I’veshownitinonestep,butasbeforeIdidtherenamesoneatatime,withacompile-test-commitaftereach.
RemovingtheformatVariable
Let’slookatthemainstatementmethodagain:
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
constformat=newIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
AsIsuggestedbefore,temporaryvariablescanbeaproblem.Theyareonlyusefulwithintheirownroutine,andthereforetheyencouragelong,complexroutines.Mynextmove,then,istoreplacesomeofthem.Theeasiestoneisformat.Thisisacaseofassigningafunctiontoatemp,whichIprefertoreplacewithadeclaredfunction.
functionstatement…
functionformat(aNumber){
returnnewIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber);
}
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
//printlineforthisorder
result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${format(totalAmount/100)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Althoughchangingafunctionvariabletoadeclaredfunctionisarefactoring,Ihaven’tnameditandincludeditinthecatalog.TherearemanyrefactoringsthatIdidn’tfeelimportantenoughforthat.Thisoneisbothsimpletodoandrelativelyrare,soIdidn’tthinkitwasworthwhile.
I’mnotkeenonthename—“format”doesn’treallyconveyenoughofwhatit’sdoing.“formatAsUSD”wouldbeabittoolong-windedsinceit’sbeingusedinastringtemplate,particularlywithinthissmallscope.Ithinkthefactthatit’sformattingacurrencyamountisthethingtohighlighthere,soIpickanamethatsuggeststhatandapplyChangeFunctionDeclaration(124)
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
//printlineforthisorder
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
functionstatement…
functionusd(aNumber){
returnnewIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100
}
Namingisbothimportantandtricky.Breakingalargefunctionintosmalleronesonlyaddsvalueifthenamesaregood.Withgoodnames,Idon’thavetoreadthebodyofthefunctiontoseewhatitdoes.Butit’shardtogetnamesrightthefirsttime,soIusethebestnameIcanthinkofforthemoment,anddon’thesitatetorenameitlater.Often,ittakesasecondpassthroughsomecodetorealizewhatthebestnamereallyis.
AsI’mchangingthename,Ialsomovetheduplicateddivisionby100intothefunction.Storingmoneyasintegercentsisacommonapproach—itavoidsthedangersofstoringfractionalmonetaryvaluesasfloatsbutallowsmetousearithmeticoperators.WheneverIwanttodisplaysuchapenny-integernumber,however,Ineedadecimal,somyformattingfunctionshouldtakecareofthedivision.
RemovingTotalVolumeCredits
MynexttargetvariableisvolumeCredits.Thisisatrickiercase,asit’sbuiltupduringtheiterationsoftheloop.Myfirstmove,then,istouseSplitLoop(226)toseparatetheaccumulationofvolumeCredits.
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letvolumeCredits=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
//printlineforthisorder
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
}
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Withthatdone,IcanuseSlideStatements(221)tomovethedeclarationofthe
variablenexttotheloop.
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
//printlineforthisorder
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
letvolumeCredits=0;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
}
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
GatheringtogethereverythingthatupdatesthevolumeCreditsvariablemakesiteasiertodoReplaceTempwithQuery(176).Asbefore,thefirststepistoapplyExtractFunction(106)totheoverallcalculationofthevariable.
functionstatement…
functiontotalVolumeCredits(){
letvolumeCredits=0;
for(letperfofinvoice.performances){
volumeCredits+=volumeCreditsFor(perf);
}
returnvolumeCredits;
}
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
//printlineforthisorder
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
letvolumeCredits=totalVolumeCredits();
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${volumeCredits}credits\n`;
returnresult;
Onceeverythingisextracted,IcanapplyInlineVariable(123):
toplevel…
functionstatement(invoice,plays){
lettotalAmount=0;
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
//printlineforthisorder
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
totalAmount+=amountFor(perf);
}
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
LetmepauseforabittotalkaboutwhatI’vejustdonehere.Firstly,Iknowreaderswillagainbeworryingaboutperformancewiththischange,asmanypeoplearewaryofrepeatingaloop.Butmostofthetime,re-runningalooplikethishasanegligibleeffectonperformance.Ifyoutimedthecodebeforeandafterthisrefactoring,youwouldprobablynotnoticeanysignificantchangeinspeed—andthat’susuallythecase.Mostprogrammers,evenexperiencedones,arepoorjudgesofhowcodeactuallyperforms.Manyofourintuitionsarebrokenbyclevercompilers,moderncachingtechniques,andthelike.Theperformanceofsoftwareusuallydependsonjustafewpartsofthecode,andchangesanywhereelsedon’tmakeanappreciabledifference.
But“mostly”isn’tthesameas“alwaysly.”Sometimesarefactoringwillhaveasignificantperformanceimplication.Eventhen,Iusuallygoaheadanddoit,becauseit’smucheasiertotunetheperformanceofwell-factoredcode.IfIintroduceasignificantperformanceissueduringrefactoring,Ispendtimeonperformance-tuningafterwards.ItmaybethatthisleadstoreversingsomeoftherefactoringIdidearlier—butmostofthetime,duetotherefactoring,Icanapplyamoreeffectiveperformance-tuningenhancementinstead.Iendupwithcodethat’sbothclearerandfaster.
So,myoveralladviceonperformancewithrefactoringis:Mostofthetimeyoushouldignoreit.Ifyourrefactoringintroducesperformanceslow-downs,finishrefactoringfirstanddoperformancetuningafterwards.
ThesecondaspectIwanttocallyourattentiontoishowsmallthestepsweretoremovevolumeCredits.Herearethefoursteps,eachfollowedbycompiling,testing,andcommittingtomylocalsourcecoderepository:
SplitLoop(226)toisolatetheaccumulation
SlideStatements(221)tobringtheinitializingcodenexttotheaccumulation
ExtractFunction(106)tocreateafunctionforcalculatingthetotal
InlineVariable(123)toremovethevariablecompletely
IconfessIdon’talwaystakequiteasshortstepsasthese—butwheneverthingsgetdifficult,myfirstreactionistotakeshortersteps.Inparticular,shouldatestfailduringarefactoring,ifIcan’timmediatelyseeandfixtheproblem,I’llreverttomylastgoodcommitandredowhatIjustdidwithsmallersteps.ThatworksbecauseIcommitsofrequentlyandbecausesmallstepsarethekeytomovingquickly,particularlywhenworkingwithdifficultcode.
IthenrepeatthatsequencetoremovetotalAmount.Istartbysplittingtheloop(compile-test-commit),thenIslidethevariableinitialization(compile-test-commit),andthenIextractthefunction.Thereisawrinklehere:Thebestnameforthefunctionis“totalAmount”,butthat’sthenameofthevariable,andIcan’thavebothatthesametime.SoIgivethenewfunctionarandomnamewhenIextractit(andcompile-test-commit)
functionstatement…
functionappleSauce(){
lettotalAmount=0;
for(letperfofinvoice.performances){
totalAmount+=amountFor(perf);
}
returntotalAmount;
}
toplevel…
functionstatement(invoice,plays){
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
lettotalAmount=appleSauce();
result+=`Amountowedis${usd(totalAmount)}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
ThenIinlinethevariable(compile-test-commit)andrenamethefunctiontosomethingmoresensible(compile-test-commit).
toplevel…
functionstatement(invoice,plays){
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functionstatement…
functiontotalAmount(){
lettotalAmount=0;
for(letperfofinvoice.performances){
totalAmount+=amountFor(perf);
}
returntotalAmount;
}
Ialsotaketheopportunitytochangethenamesinsidemyextractedfunctionstoadheretomyconvention.
functionstatement…
functiontotalAmount(){
letresult=0;
for(letperfofinvoice.performances){
result+=amountFor(perf);
}
returnresult;
}
functiontotalVolumeCredits(){
letresult=0;
for(letperfofinvoice.performances){
result+=volumeCreditsFor(perf);
}
returnresult;
}
Status:LotsofNestedFunctions
Nowisagoodtimetopauseandtakealookattheoverallstateofthecode:
functionstatement(invoice,plays){
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functiontotalAmount(){
letresult=0;
for(letperfofinvoice.performances){
result+=amountFor(perf);
}
returnresult;
}
functiontotalVolumeCredits(){
letresult=0;
for(letperfofinvoice.performances){
result+=volumeCreditsFor(perf);
}
returnresult;
}
functionusd(aNumber){
returnnewIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
functionvolumeCreditsFor(aPerformance){
letresult=0;
result+=Math.max(aPerformance.audience-30,0);
if("comedy"===playFor(aPerformance).type)result+=Math.floor(aPerformance.audience/5);
returnresult;
}
functionplayFor(aPerformance){
returnplays[aPerformance.playID];
}
functionamountFor(aPerformance){
letresult=0;
switch(playFor(aPerformance).type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${playFor(aPerformance).type}`);
}
returnresult;
}
}
Thestructureofthecodeismuchbetternow.Thetop-levelstatementfunctionisnowjustsevenlinesofcode,andallitdoesislayingouttheprintingofthestatement.Allthecalculationlogichasbeenmovedouttoahandfulofsupportingfunctions.Thismakesiteasiertounderstandeachindividualcalculationaswellastheoverallflowofthereport.
SplittingthePhasesofCalculationandFormatting
Sofar,myrefactoringhasfocusedonaddingenoughstructuretothefunctionsothatIcanunderstanditandseeitintermsofitslogicalparts.Thisisoftenthecaseearlyinrefactoring.Breakingdowncomplicatedchunksintosmallpiecesisimportant,asisnamingthingswell.Now,IcanbegintofocusmoreonthefunctionalitychangeIwanttomake—specifically,providinganHTMLversionofthisstatement.Inmanyways,it’snowmucheasiertodo.Withallthecalculationcodesplitout,allIhavetodoiswriteanHTMLversionofthesevenlinesofcodeatthetop.Theproblemisthatthesebroken-outfunctionsarenestedwithinthetextualstatementmethod,andIdon’twanttocopyandpastethemintoanewfunction,howeverwellorganized.IwantthesamecalculationfunctionstobeusedbythetextandHTMLversionsofthestatement.
Therearevariouswaystodothis,butoneofmyfavoritetechniquesisSplitPhase(154).Myaimhereistodividethelogicintotwoparts:onethatcalculatesthedatarequiredforthestatement,theotherthatrendersitintotextorHTML.Thefirstphasecreatesanintermediatedatastructurethatitpassestothesecond.
IstartaSplitPhase(154)byapplyingExtractFunction(106)tothecodethatmakesupthesecondphase.Inthiscase,that’sthestatementprintingcode,whichisinfacttheentirecontentofstatement.This,togetherwithallthenestedfunctions,goesintoitsowntop-levelfunctionwhichIcallrenderPlainText.
functionstatement(invoice,plays){
returnrenderPlainText(invoice,plays);
}
functionrenderPlainText(invoice,plays){
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functiontotalAmount(){…}
functiontotalVolumeCredits(){…}
functionusd(aNumber){…}
functionvolumeCreditsFor(aPerformance){…}
functionplayFor(aPerformance){…}
functionamountFor(aPerformance){…}
Idomyusualcompile-test-commit,thencreateanobjectthatwillactasmyintermediatedatastructurebetweenthetwophases.IpassthisdataobjectinasanargumenttorenderPlainText(compile-test-commit).
functionstatement(invoice,plays){
conststatementData={};
returnrenderPlainText(statementData,invoice,plays);
}
functionrenderPlainText(data,invoice,plays){
letresult=`Statementfor${invoice.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functiontotalAmount(){…}
functiontotalVolumeCredits(){…}
functionusd(aNumber){…}
functionvolumeCreditsFor(aPerformance){…}
functionplayFor(aPerformance){…}
functionamountFor(aPerformance){…}
InowexaminetheotherargumentsusedbyrenderPlainText.Iwanttomovethedatathatcomesfromthemintotheintermediatedatastructure,sothatallthecalculationcodemovesintothestatementfunctionandrenderPlainTextoperatessolelyondatapassedtoitthroughthedataparameter.
Myfirstmoveistotakethecustomerandaddittotheintermediateobject(compile-test-commit).
functionstatement(invoice,plays){
conststatementData={};
statementData.customer=invoice.customer;
returnrenderPlainText(statementData,invoice,plays);
}
}
functionrenderPlainText(data,invoice,plays){
letresult=`Statementfor${data.customer}\n`;
for(letperfofinvoice.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
Similarly,Iaddtheperformances,whichallowsmetodeletetheinvoiceparametertorenderPlainText(compile-test-commit).
functionstatement(invoice,plays){
conststatementData={};
statementData.customer=invoice.customer;
statementData.performances=invoice.performances;
returnrenderPlainText(statementData,invoice,plays);
}
functionrenderPlainText(data,invoice,plays){
letresult=`Statementfor${data.customer}\n`;
for(letperfofdata.performances){
result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
NowI’dliketheplaynametocomefromtheintermediatedata.Todothis,Ineedtoenrichtheperformancerecordwithdatafromtheplay(compile-test-commit).
functionstatement(invoice,plays){
conststatementData={};
statementData.customer=invoice.customer;
statementData.performances=invoice.performances.map(enrichPerformance);
returnrenderPlainText(statementData,plays);
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
returnresult;
}
Atthemoment,I’mjustmakingacopyoftheperformanceobject,butI’llshortlyadddatatothisnewrecord.ItakeacopybecauseIdon’twanttomodifythedatapassedintothefunction.IprefertotreatdataasimmutableasmuchasIcan—mutablestatequicklybecomessomethingrotten.
Theidiomresult=Object.assign({},aPerformance)looksveryoddtopeopleunfamiliartoJavaScript.Itperformsashallowcopy.I’dprefertohaveafunctionforthis,butit’soneofthosecaseswheretheidiomissobakedintoJavaScriptusagethatwritingmyownfunctionwouldlookoutofplaceforJavaScriptprogrammers.
NowIhaveaspotfortheplay,Ineedtoaddit.Todothat,IneedtoapplyMoveFunction(196)toplayForandstatement(compile-test-commit).
functionstatement…
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
result.play=playFor(aPerformance);
returnresult;
}
functionplayFor(aPerformance){
returnplays[aPerformance.playID];
}
IthenreplaceallthereferencestoplayForinrenderPlainTexttousethedatainstead(compile-test-commit).
functionrenderPlainText…
letresult=`Statementfor${data.customer}\n`;
for(letperfofdata.performances){
result+=`${perf.play.name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functionvolumeCreditsFor(aPerformance){
letresult=0;
result+=Math.max(aPerformance.audience-30,0);
if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);
returnresult;
}
functionamountFor(aPerformance){
letresult=0;
switch(aPerformance.play.type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${aPerformance.play.type}`);
}
returnresult;
}
IthenmoveamountForinasimilarway(compile-test-commit).
functionstatement…
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
result.play=playFor(result);
result.amount=amountFor(result);
returnresult;
}
functionamountFor(aPerformance){…}
functionrenderPlainText…
letresult=`Statementfor${data.customer}\n`;
for(letperfofdata.performances){
result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(totalAmount())}\n`;
result+=`Youearned${totalVolumeCredits()}credits\n`;
returnresult;
functiontotalAmount(){
letresult=0;
for(letperfofdata.performances){
result+=perf.amount;
}
returnresult;
}
Next,Imovethevolumecreditscalculation(compile-test-commit).
functionstatement…
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
result.play=playFor(result);
result.amount=amountFor(result);
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
functionvolumeCreditsFor(aPerformance){…}
functionrenderPlainText…
functiontotalVolumeCredits(){
letresult=0;
for(letperfofdata.performances){
result+=perf.volumeCredits;
}
returnresult;
}
Finally,Imovethetwocalculationsofthetotals.
functionstatement…
conststatementData={};
statementData.customer=invoice.customer;
statementData.performances=invoice.performances.map(enrichPerformance);
statementData.totalAmount=totalAmount(statementData);
statementData.totalVolumeCredits=totalVolumeCredits(statementData);
returnrenderPlainText(statementData,plays);
functiontotalAmount(data){…}
functiontotalVolumeCredits(data){…}
functionrenderPlainText…
letresult=`Statementfor${data.customer}\n`;
for(letperfofdata.performances){
result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(data.totalAmount)}\n`;
result+=`Youearned${data.totalVolumeCredits}credits\n`;
returnresult;
AlthoughIcouldhavemodifiedthebodiesofthesetotalsfunctionstousethestatementDatavariable(asit’swithinscope),Iprefertopasstheexplicitparameter.
And,onceI’mdonewithcompile-test-commitafterthemove,Ican’tresistacouplequickshotsofReplaceLoopwithPipeline(230).
functionrenderPlainText…
functiontotalAmount(data){
returndata.performances
.reduce((total,p)=>total+p.amount,0);
}
functiontotalVolumeCredits(data){
returndata.performances
.reduce((total,p)=>total+p.volumeCredits,0);
}
Inowextractallthefirst-phasecodeintoitsownfunction(compile-test-commit).
toplevel…
functionstatement(invoice,plays){
returnrenderPlainText(createStatementData(invoice,plays));
}
functioncreateStatementData(invoice,plays){
conststatementData={};
statementData.customer=invoice.customer;
statementData.performances=invoice.performances.map(enrichPerformance);
statementData.totalAmount=totalAmount(statementData);
statementData.totalVolumeCredits=totalVolumeCredits(statementData);
returnstatementData;
Sinceit’sclearlyseparatenow,Imoveittoitsownfile(andalterthenameofthereturnedresulttomatchmyusualconvention).
statement.js…
importcreateStatementDatafrom'./createStatementData.js';
createStatementData.js…
exportdefaultfunctioncreateStatementData(invoice,plays){
constresult={};
result.customer=invoice.customer;
result.performances=invoice.performances.map(enrichPerformance);
result.totalAmount=totalAmount(result);
result.totalVolumeCredits=totalVolumeCredits(result);
returnresult;
functionenrichPerformance(aPerformance){…}
functionplayFor(aPerformance){…}
functionamountFor(aPerformance){…}
functionvolumeCreditsFor(aPerformance){…}
functiontotalAmount(data){…}
functiontotalVolumeCredits(data){…}
Onefinalswingofcompile-test-commit—andnowit’seasytowriteanHTMLversion.
statement.js…
functionhtmlStatement(invoice,plays){
returnrenderHtml(createStatementData(invoice,plays));
}
functionrenderHtml(data){
letresult=`<h1>Statementfor${data.customer}</h1>\n`;
result+="<table>\n";
result+="<tr><th>play</th><th>seats</th><th>cost</th></tr>";
for(letperfofdata.performances){
result+=`<tr><td>${perf.play.name}</td><td>${perf.audience}</td>`;
result+=`<td>${usd(perf.amount)}</td></tr>\n`;
}
result+="</table>\n";
result+=`<p>Amountowedis<em>${usd(data.totalAmount)}</em></p>\n`;
result+=`<p>Youearned<em>${data.totalVolumeCredits}</em>credits</p>\n`;
returnresult;
}
functionusd(aNumber){…}
(Imovedusdtothetoplevel,sothatrenderHtmlcoulduseit.)
Status:SeparatedintoTwoFiles(andPhases)
Thisisagoodmomenttotakestockagainandthinkaboutwherethecodeisnow.Ihavetwofilesofcode.
statement.js
importcreateStatementDatafrom'./createStatementData.js';
functionstatement(invoice,plays){
returnrenderPlainText(createStatementData(invoice,plays));
}
functionrenderPlainText(data,plays){
letresult=`Statementfor${data.customer}\n`;
for(letperfofdata.performances){
result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;
}
result+=`Amountowedis${usd(data.totalAmount)}\n`;
result+=`Youearned${data.totalVolumeCredits}credits\n`;
returnresult;
}
functionhtmlStatement(invoice,plays){
returnrenderHtml(createStatementData(invoice,plays));
}
functionrenderHtml(data){
letresult=`<h1>Statementfor${data.customer}</h1>\n`;
result+="<table>\n";
result+="<tr><th>play</th><th>seats</th><th>cost</th></tr>";
for(letperfofdata.performances){
result+=`<tr><td>${perf.play.name}</td><td>${perf.audience}</td>`;
result+=`<td>${usd(perf.amount)}</td></tr>\n`;
}
result+="</table>\n";
result+=`<p>Amountowedis<em>${usd(data.totalAmount)}</em></p>\n`;
result+=`<p>Youearned<em>${data.totalVolumeCredits}</em>credits</p>\n`;
returnresult;
}
functionusd(aNumber){
returnnewIntl.NumberFormat("en-US",
{style:"currency",currency:"USD",
minimumFractionDigits:2}).format(aNumber/100);
}
createStatementData.js
exportdefaultfunctioncreateStatementData(invoice,plays){
constresult={};
result.customer=invoice.customer;
result.performances=invoice.performances.map(enrichPerformance);
result.totalAmount=totalAmount(result);
result.totalVolumeCredits=totalVolumeCredits(result);
returnresult;
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
result.play=playFor(result);
result.amount=amountFor(result);
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
functionplayFor(aPerformance){
returnplays[aPerformance.playID]
}
functionamountFor(aPerformance){
letresult=0;
switch(aPerformance.play.type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${aPerformance.play.type}`);
}
returnresult;
}
functionvolumeCreditsFor(aPerformance){
letresult=0;
result+=Math.max(aPerformance.audience-30,0);
if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);
returnresult;
}
functiontotalAmount(data){
returndata.performances
.reduce((total,p)=>total+p.amount,0);
}
functiontotalVolumeCredits(data){
returndata.performances
.reduce((total,p)=>total+p.volumeCredits,0);
}
IhavemorecodethanIdidwhenIstarted:70lines(notcountinghtmlStatement)asopposedto44,mostlyduetotheextrawrappinginvolvedinputtingthingsinfunctions.Ifallelseisequal,morecodeisbad—butrarelyisallelseequal.Theextracodebreaksupthelogicintoidentifiableparts,separatingthecalculationsofthestatementsfromthelayout.Thismodularitymakesiteasierformetounderstandthepartsofthecodeandhowtheyfittogether.Brevityisthesoulofwit,butclarityisthesoulofevolvablesoftware.AddingthismodularityallowstometosupporttheHTMLversionofthecodewithoutanyduplicationofthecalculations.
Whenprogramming,followthecampingrule:Alwaysleavethecodebasehealthierthanwhenyoufoundit.
TherearemorethingsIcoulddotosimplifytheprintinglogic,butthiswilldoforthemoment.IalwayshavetostrikeabalancebetweenalltherefactoringsI
coulddoandaddingnewfeatures.Atthemoment,mostpeopleunder-prioritizerefactoring—buttherestillisabalance.Myruleisavariationonthecampingrule:Alwaysleavethecodebasehealthierthanwhenyoufoundit.Itwillneverbeperfect,butitshouldbebetter.
ReorganizingtheCalculationsbyType
NowI’llturnmyattentiontothenextfeaturechange:supportingmorecategoriesofplays,eachwithitsownchargingandvolumecreditscalculations.Atthemoment,tomakechangeshereIhavetogointothecalculationfunctionsandedittheconditionsinthere.ThethisAmountfunctionhighlightsthecentralrolethetypeofplayhasinthechoiceofcalculations—butconditionallogiclikethistendstodecayasfurthermodificationsaremadeunlessit’sreinforcedbymorestructuralelementsoftheprogramminglanguage.
Therearevariouswaystointroducestructuretomakethisexplicit,butinthiscaseanaturalapproachistypepolymorphism—aprominentfeatureofclassicalobject-orientation.ClassicalOOhaslongbeenacontroversialfeatureintheJavaScriptworld,buttheECMAScript2015versionprovidesasoundsyntaxandstructureforit.Soitmakessensetouseitinarightsituation—likethisone.
Myoverallplanistosetupaninheritancehierarchywithcomedyandtragedysubclassesthatcontainthecalculationlogicforthosecases.Callerscallapolymorphicamountfunctionthatthelanguagewilldispatchtothedifferentcalculationsforthecomediesandtragedies.I’llmakeasimilarstructureforthevolumecreditscalculation.Todothis,Iutilizeacoupleofrefactorings.ThecorerefactoringisReplaceConditionalwithPolymorphism(271),whichchangesahunkofconditionalcodewithpolymorphism.ButbeforeIcandoReplaceConditionalwithPolymorphism(271),Ineedtocreateaninheritancestructureofsomekind.Ineedtocreateaclasstohosttheamountandvolumecreditfunctions.
Ibeginbyreviewingthecalculationcode.(OneofthepleasantconsequencesofthepreviousrefactoringisthatIcannowignoretheformattingcode,solongasIproducethesameoutputdatastructure.Icanfurthersupportthisbyaddingteststhatprobetheintermediatedatastructure.)
createStatementData.js…
exportdefaultfunctioncreateStatementData(invoice,plays){
constresult={};
result.customer=invoice.customer;
result.performances=invoice.performances.map(enrichPerformance);
result.totalAmount=totalAmount(result);
result.totalVolumeCredits=totalVolumeCredits(result);
returnresult;
functionenrichPerformance(aPerformance){
constresult=Object.assign({},aPerformance);
result.play=playFor(result);
result.amount=amountFor(result);
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
functionplayFor(aPerformance){
returnplays[aPerformance.playID]
}
functionamountFor(aPerformance){
letresult=0;
switch(aPerformance.play.type){
case"tragedy":
result=40000;
if(aPerformance.audience>30){
result+=1000*(aPerformance.audience-30);
}
break;
case"comedy":
result=30000;
if(aPerformance.audience>20){
result+=10000+500*(aPerformance.audience-20);
}
result+=300*aPerformance.audience;
break;
default:
thrownewError(`unknowntype:${aPerformance.play.type}`);
}
returnresult;
}
functionvolumeCreditsFor(aPerformance){
letresult=0;
result+=Math.max(aPerformance.audience-30,0);
if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);
returnresult;
}
functiontotalAmount(data){
returndata.performances
.reduce((total,p)=>total+p.amount,0);
}
functiontotalVolumeCredits(data){
returndata.performances
.reduce((total,p)=>total+p.volumeCredits,0);
}
CreatingaPerformanceCalculator
TheenrichPerformancefunctionisthekey,sinceitpopulatestheintermediatedatastructurewiththedataforeachperformance.Currently,itcallstheconditionalfunctionsforamountandvolumecredits.WhatIneedittodoiscallthosefunctionsonahostclass.Sincethatclasshostsfunctionsforcalculatingdataaboutperformances,I’llcallitaperformancecalculator.
functioncreateStatementData…
functionenrichPerformance(aPerformance){
constcalculator=newPerformanceCalculator(aPerformance);
constresult=Object.assign({},aPerformance);
result.play=playFor(result);
result.amount=amountFor(result);
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
toplevel…
classPerformanceCalculator{
constructor(aPerformance){
this.performance=aPerformance;
}
}
Sofar,thisnewobjectisn’tdoinganything.Iwanttomovebehaviorintoit—andI’dliketostartwiththesimplestthingtomove,whichistheplayrecord.Strictly,Idon’tneedtodothis,asit’snotvaryingpolymorphically,butthiswayI’llkeepallthedatatransformsinoneplace,andthatconsistencywillmakethecodeclearer.
Tomakethiswork,IwilluseChangeFunctionDeclaration(124)topasstheperformance’splayintothecalculator.
functioncreateStatementData…
functionenrichPerformance(aPerformance){
constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance)
constresult=Object.assign({},aPerformance);
result.play=calculator.play;
result.amount=amountFor(result);
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
classPerformanceCalculator…
classPerformanceCalculator{
constructor(aPerformance,aPlay){
this.performance=aPerformance;
this.play=aPlay;
}
}
(I’mnotsayingcompile-test-commitallthetimeanymore,asIsuspectyou’regettingtiredofreadingit.ButIstilldoitateveryopportunity.Idosometimesgettiredofdoingit—andgivemistakesthechancetobiteme.ThenIlearnandgetbackintotherhythm.)
MovingFunctionsintotheCalculator
ThenextbitoflogicImoveisrathermoresubstantialforcalculatingtheamountforaperformance.I’vemovedfunctionsaroundcasuallywhilerearrangingnestedfunctions—butthisisadeeperchangeinthecontextofthefunction,soI’llstepthroughtheMoveFunction(196)refactoring.Thefirstpartofthisrefactoringistocopythelogicovertoitsnewcontext—thecalculatorclass.Then,Iadjustthecodetofitintoitsnewhome,changingaPerformancetothis.performanceandplayFor(aPerformance)tothis.play.
classPerformanceCalculator…
getamount(){
letresult=0;
switch(this.play.type){
case"tragedy":
result=40000;
if(this.performance.audience>30){
result+=1000*(this.performance.audience-30);
}
break;
case"comedy":
result=30000;
if(this.performance.audience>20){
result+=10000+500*(this.performance.audience-20);
}
result+=300*this.performance.audience;
break;
default:
thrownewError(`unknowntype:${this.play.type}`);
}
returnresult;
}
Icancompileatthispointtocheckforanycompile-timeerrors.“Compiling”inmydevelopmentenvironmentoccursasIexecutethecode,sowhatIactuallydoisrunBabel[bib-babel].Thatwillbeenoughtocatchanysyntaxerrorsinthenewfunction—butlittlemorethanthat.Evenso,thatcanbeausefulstep.
Oncethenewfunctionfitsitshome,Itaketheoriginalfunctionandturnitintoadelegatingfunctionsoitcallsthenewfunction.
functioncreateStatementData…
functionamountFor(aPerformance){
returnnewPerformanceCalculator(aPerformance,playFor(aPerformance)).amount;
}
NowIcancompile-test-committoensurethecodeisworkingproperlyinitsnewhome.Withthatdone,IuseInlineFunction(115)tocallthenewfunctiondirectly(compile-test-commit).
functioncreateStatementData…
functionenrichPerformance(aPerformance){
constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance));
constresult=Object.assign({},aPerformance);
result.play=calculator.play;
result.amount=calculator.amount;
result.volumeCredits=volumeCreditsFor(result);
returnresult;
}
Irepeatthesameprocesstomovethevolumecreditscalculation.
functioncreateStatementData…
functionenrichPerformance(aPerformance){
constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance));
constresult=Object.assign({},aPerformance);
result.play=calculator.play;
result.amount=calculator.amount;
result.volumeCredits=calculator.volumeCredits;
returnresult;
}
classPerformanceCalculator…
getvolumeCredits(){
letresult=0;
result+=Math.max(this.performance.audience-30,0);
if("comedy"===this.play.type)result+=Math.floor(this.performance
returnresult;
}
MakingthePerformanceCalculatorPolymorphic
NowthatIhavethelogicinaclass,it’stimetoapplythepolymorphism.ThefirststepistouseReplaceTypeCodewithSubclasses(361)tointroducesubclassesinsteadofthetypecode.Forthis,IneedtocreatesubclassesoftheperformancecalculatorandusetheappropriatesubclassincreatePerformanceData.Inordertogettherightsubclass,Ineedtoreplacetheconstructorcallwithafunction,sinceJavaScriptconstructorscan’treturnsubclasses.SoIuseReplaceConstructorwithFactoryFunction(332).
functioncreateStatementData…
functionenrichPerformance(aPerformance){
constcalculator=createPerformanceCalculator(aPerformance,playFor(aPerformance));
constresult=Object.assign({},aPerformance);
result.play=calculator.play;
result.amount=calculator.amount;
result.volumeCredits=calculator.volumeCredits;
returnresult;
}
toplevel…
functioncreatePerformanceCalculator(aPerformance,aPlay){
returnnewPerformanceCalculator(aPerformance,aPlay);
}
Withthatnowafunction,Icancreatesubclassesoftheperformancecalculatorandgetthecreationfunctiontoselectwhichonetoreturn.
toplevel…
functioncreatePerformanceCalculator(aPerformance,aPlay){
switch(aPlay.type){
case"tragedy":returnnewTragedyCalculator(aPerformance,aPlay);
case"comedy":returnnewComedyCalculator(aPerformance,aPlay);
default:
thrownewError(`unknowntype:${aPlay.type}`);
}
}
classTragedyCalculatorextendsPerformanceCalculator{
}
classComedyCalculatorextendsPerformanceCalculator{
}
Thissetsupthestructureforthepolymorphism,soIcannowmoveontoReplaceConditionalwithPolymorphism(271).
Istartwiththecalculationoftheamountfortragedies.
classTragedyCalculator…
getamount(){
letresult=40000;
if(this.performance.audience>30){
result+=1000*(this.performance.audience-30);
}
returnresult;
}
Justhavingthismethodinthesubclassisenoughtooverridethesuperclassconditional.Butifyou’reasparanoidasIam,youmightdothis:
classPerformanceCalculator…
getamount(){
letresult=0;
switch(this.play.type){
case"tragedy":
throw'badthing';
case"comedy":
result=30000;
if(this.performance.audience>20){
result+=10000+500*(this.performance.audience-20);
}
result+=300*this.performance.audience;
break;
default:
thrownewError(`unknowntype:${this.play.type}`);
}
returnresult;
}
Icouldhaveremovedthecasefortragedyandletthedefaultbranchthrowanerror.ButIliketheexplicitthrow—anditwillonlybethereforacouplemoreminutes(whichiswhyIthrewastring,notabettererrorobject).
Afteracompile-test-commitofthat,Imovethecomedycasedowntoo.
classComedyCalculator…
getamount(){
letresult=30000;
if(this.performance.audience>20){
result+=10000+500*(this.performance.audience-20);
}
result+=300*this.performance.audience;
returnresult;
}
Icannowremovethesuperclassamountmethod,asitshouldneverbecalled.Butit’skindertomyfutureselftoleaveatombstone.
classPerformanceCalculator…
getamount(){
thrownewError('subclassresponsibility');
}
Thenextconditionaltoreplaceisthevolumecreditscalculation.Lookingatthediscussionoffuturecategoriesofplays,Inoticethatmostplaysexpecttocheckifaudienceisabove30,withonlysomecategoriesintroducingavariation.Soitmakessensetoleavethemorecommoncaseonthesuperclassasadefault,andletthevariationsoverrideitasnecessary.SoIjustpushdownthecaseforcomedies:
classPerformanceCalculator…
getvolumeCredits(){
returnMath.max(this.performance.audience-30,0);
}
classComedyCalculator…
getvolumeCredits(){
returnsuper.volumeCredits+Math.floor(this.performance.audience/5);
}
Status:CreatingtheDatawiththePolymorphicCalculator
Timetoreflectonwhatintroducingthepolymorphiccalculatordidtothecode.
createStatementData.js
exportdefaultfunctioncreateStatementData(invoice,plays){
constresult={};
result.customer=invoice.customer;
result.performances=invoice.performances.map(enrichPerformance);
result.totalAmount=totalAmount(result);
result.totalVolumeCredits=totalVolumeCredits(result);
returnresult;
functionenrichPerformance(aPerformance){
constcalculator=createPerformanceCalculator(aPerformance,playFor(aPerformance));
constresult=Object.assign({},aPerformance);
result.play=calculator.play;
result.amount=calculator.amount;
result.volumeCredits=calculator.volumeCredits;
returnresult;
}
functionplayFor(aPerformance){
returnplays[aPerformance.playID]
}
functiontotalAmount(data){
returndata.performances
.reduce((total,p)=>total+p.amount,0);
}
functiontotalVolumeCredits(data){
returndata.performances
.reduce((total,p)=>total+p.volumeCredits,0);
}
}
functioncreatePerformanceCalculator(aPerformance,aPlay){
switch(aPlay.type){
case"tragedy":returnnewTragedyCalculator(aPerformance,aPlay);
case"comedy":returnnewComedyCalculator(aPerformance,aPlay);
default:
thrownewError(`unknowntype:${aPlay.type}`);
}
}
classPerformanceCalculator{
constructor(aPerformance,aPlay){
this.performance=aPerformance;
this.play=aPlay;
}
getamount(){
thrownewError('subclassresponsibility');
}
getvolumeCredits(){
returnMath.max(this.performance.audience-30,0);
}
}
classTragedyCalculatorextendsPerformanceCalculator{
getamount(){
letresult=40000;
if(this.performance.audience>30){
result+=1000*(this.performance.audience-30);
}
returnresult;
}
}
classComedyCalculatorextendsPerformanceCalculator{
getamount(){
letresult=30000;
if(this.performance.audience>20){
result+=10000+500*(this.performance.audience-20);
}
result+=300*this.performance.audience;
returnresult;
}
getvolumeCredits(){
returnsuper.volumeCredits+Math.floor(this.performance.audience/5);
}
}
Again,thecodehasincreasedinsizeasI’veintroducedstructure.Thebenefithereisthatthecalculationsforeachkindofplayaregroupedtogether.Ifmostofthechangeswillbetothiscode,itwillbehelpfultohaveitclearlyseparatedlikethis.Addinganewkindofplayrequireswritinganewsubclassandaddingitto
thecreationfunction.
Theexamplegivessomeinsightastowhenusingsubclasseslikethisisuseful.Here,I’vemovedtheconditionallookupfromtwofunctions(amountForandvolumeCreditsFortoasingleconstructorfunctioncreatePerformanceCalculator.Themorefunctionstherearethatdependonthesametypeofpolymorphism,themoreusefulthisapproachbecomes.
AnalternativetowhatI’vedoneherewouldbetohavecreatePerformanceDatareturnthecalculatoritself,insteadofthecalculatorpopulatingtheintermediatedatastructure.OneofthenicefeaturesofJavaScript’sclasssystemisthatwithit,usinggetterslookslikeregulardataaccess.Mychoiceonwhethertoreturntheinstanceorcalculateseparateoutputdatadependsonwhoisusingthedownstreamdatastructure.Inthiscase,Ipreferredtoshowhowtousetheintermediatedatastructuretohidethedecisiontouseapolymorphiccalculator.
FinalThoughts
Thisisasimpleexample,butIhopeitwillgiveyouafeelingforwhatrefactoringislike.I’veusedseveralrefactorings,includingExtractFunction(106),InlineVariable(123),MoveFunction(196),andReplaceConditionalwithPolymorphism(271)
Therewerethreemajorstagestothisrefactoringepisode:decomposingtheoriginalfunctionintoasetofnestedfunctions,usingSplitPhase(154)toseparatethecalculationandprintingcode,andfinallyintroducingapolymorphiccalculatorforthecalculationlogic.Eachoftheseaddedstructuretothecode,enablingmetobettercommunicatewhatthecodewasdoing.
Asisoftenthecasewithrefactoring,theearlystagesweremostlydrivenbytryingtounderstandwhatwasgoingon.Acommonsequenceis:Readthecode,gainsomeinsight,anduserefactoringtomovethatinsightfromyourheadbackintothecode.Theclearercodethenmakesiteasiertounderstandit,leadingtodeeperinsightsandabeneficialpositivefeedbackloop.TherearestillsomeimprovementsIcouldmake,butIfeelI’vedoneenoughtopassmytestofleavingthecodesignificantlybetterthanhowIfoundit.
Thetruetestofgoodcodeisishoweasyitistochangeit.
I’mtalkingaboutimprovingthecode—butprogrammerslovetoargueaboutwhatgoodcodelookslike.Iknowsomepeopleobjecttomypreferenceforsmall,well-namedfunctions.Ifweconsiderthistobeamatterofaesthetics,wherenothingiseithergoodorbadbutthinkingmakesitso,welackanyguidebutpersonaltaste.Ibelieve,however,thatwecangobeyondtasteandsaythatthetruetestofgoodcodeishoweasyitistochangeit.Codeshouldbeobvious:Whensomeoneneedstomakeachange,theyshouldbeabletofindthecodetobechangedeasilyandtomakethechangequicklywithoutintroducinganyerrors.Ahealthycodebasemaximizesourproductivity,allowingustobuildmorefeaturesforourusersbothfasterandmorecheaply.Tokeepcodehealthy,payattentiontowhatisgettingbetweentheprogrammingteamandthatideal,thenrefactortogetclosertotheideal.
Butthemostimportantthingtolearnfromthisexampleistherhythmofrefactoring.WheneverI’veshownpeoplehowIrefactor,theyaresurprisedbyhowsmallmystepsare,eachstepleavingthecodeinaworkingstatethatcompilesandpassesitstests.IwasjustassurprisedmyselfwhenKentBeckshowedmehowtodothisinahotelroominDetroittwodecadesago.Thekeytoeffectiverefactoringisrecognizingthatyougofasterwhenyoutaketinysteps,thecodeisneverbroken,andyoucancomposethosesmallstepsintosubstantialchanges.Rememberthat—andtherestissilence.
Chapter2PrinciplesinRefactoringTheexampleinthepreviouschaptershouldhavegivenyouadecentfeelofwhatrefactoringis.Nowyouhavethat,it’sagoodtimetostepbackandtalkaboutsomeofthebroaderprinciplesinrefactoring.
DefiningRefactoring
Likemanytermsinsoftwaredevelopment,“refactoring”isoftenusedverylooselybypractitioners.Iusethetermmoreprecisely,andfinditusefultouseitinthatmorepreciseform.(ThesedefinitionsarethesameasthoseIgaveinthefirsteditionofthisbook.)Theterm“refactoring”canbeusedeitherasanounoraverb.Thenoun’sdefinitionis:
Refactoring(noun):achangemadetotheinternalstructureofsoftwaretomakeiteasiertounderstandandcheapertomodifywithoutchangingitsobservablebehavior.
ThisdefinitioncorrespondstothenamedrefactoringsI’vementionedintheearlierexamples,suchasExtractFunction(106)andReplaceConditionalwithPolymorphism(271).
Theverb’sdefinitionis:
Refactoring(verb):torestructuresoftwarebyapplyingaseriesofrefactoringswithoutchangingitsobservablebehavior.
SoImightspendacoupleofhoursrefactoring,duringwhichIwouldapplyafewdozenindividualrefactorings.
Overtheyears,manypeopleintheindustryhavetakentouse“refactoring”tomeananykindofcodecleanup—butthedefinitionsabovepointtoaparticularapproachtocleaningupcode.Refactoringisallaboutapplyingsmallbehavior-preservingstepsandmakingabigchangebystringingtogetherasequenceofthesebehavior-preservingsteps.Eachindividualrefactoringiseitherprettysmall
itselforacombinationofsmallsteps.Asaresult,whenI’mrefactoring,mycodedoesn’tspendmuchtimeinabrokenstate,allowingmetostopatanymomentevenifIhaven’tfinished.
Ifsomeonesaystheircodewasbrokenforacoupleofdayswhiletheyarerefactoring,youcanbeprettysuretheywerenotrefactoring
Iuse“restructuring”asageneraltermtomeananykindofreorganizingorcleaningupofacodebase,andseerefactoringasaparticularkindofrestructuring.Refactoringmayseeminefficienttopeoplewhofirstcomeacrossitandwatchmemakinglotsoftinysteps,whenasinglebiggerstepwoulddo.Butthetinystepsallowmetogofasterbecausetheycomposesowell—and,crucially,becauseIdon’tspendanytimedebugging.
Inmydefinitions,Iusethephrase“observablebehavior.”Thisisadeliberatelylooseterm,indicatingthatthecodeshould,overall,dojustthesamethingsitdidbeforeIstarted.Itdoesn’tmeanitwillworkexactlythesame—forexample,ExtractFunction(106)willalterthecallstack,soperformancecharacteristicsmightchange—butnothingshouldchangethattheusershouldcareabout.Inparticular,interfacestomodulesoftenchangeduetosuchrefactoringsasChangeFunctionDeclaration(124)andMoveFunction(196).AnybugsthatInoticeduringrefactoringshouldstillbepresentafterrefactoring(thoughIcanfixlatentbugsthatnobodyhasobservedyet).
Refactoringisverysimilartoperformanceoptimization,asbothinvolvecarryingoutcodemanipulationsthatdon’tchangetheoverallfunctionalityoftheprogram.Thedifferenceisthepurpose:Refactoringisalwaysdonetomakethecode“easiertounderstandandcheapertomodify”.Thismightspeedthingsuporslowthingsdown.Withperformanceoptimization,Ionlycareaboutspeedinguptheprogram,andampreparedtoendupwithcodethatishardertoworkwithifIreallyneedthatimprovedperformance.
TheTwoHats
KentBeckcameupwithametaphorofthetwohats.WhenIuserefactoringtodevelopsoftware,Idividemytimebetweentwodistinctactivities:addingfunctionalityandrefactoring.WhenIaddfunctionality,Ishouldn’tbechangingexistingcode;I’mjustaddingnewcapabilities.Imeasuremyprogressbyaddingtestsandgettingtheteststowork.WhenIrefactor,Imakeapointofnotadding
functionality;Ionlyrestructurethecode.Idon’taddanytests(unlessIfindacaseImissedearlier);IonlychangetestswhenIhavetoaccommodateachangeinaninterface.
AsIdevelopsoftware,Ifindmyselfswappinghatsfrequently.Istartbytryingtoaddanewcapability,thenIrealizethiswouldbemucheasierifthecodewerestructureddifferently.SoIswaphatsandrefactorforawhile.Oncethecodeisbetterstructured,Iswaphatsbackandaddthenewcapability.OnceIgetthenewcapabilityworking,IrealizeIcodeditinawaythat’sawkwardtounderstand,soIswaphatsagainandrefactor.Allthismighttakeonlytenminutes,butduringthistimeI’malwaysawareofwhichhatI’mwearingandthesubtledifferencethatmakestohowIprogram.
WhyShouldWeRefactor?
Idon’twanttoclaimrefactoringisthecureforallsoftwareills.Itisno“silverbullet.”Yetitisavaluabletool—apairofsilverpliersthathelpsyoukeepagoodgriponyourcode.Refactoringisatoolthatcan—andshould—beusedforseveralpurposes.
RefactoringImprovestheDesignofSoftware
Withoutrefactoring,theinternaldesign—thearchitecture—ofsoftwaretendstodecay.Aspeoplechangecodetoachieveshort-termgoals,oftenwithoutafullcomprehensionofthearchitecture,thecodelosesitsstructure.Itbecomesharderformetoseethedesignbyreadingthecode.Lossofthestructureofcodehasacumulativeeffect.Theharderitistoseethedesigninthecode,theharderitisformetopreserveit,andthemorerapidlyitdecays.Regularrefactoringhelpskeepsthecodeinshape.
Poorlydesignedcodeusuallytakesmorecodetodothesamethings,oftenbecausethecodequiteliterallydoesthesamethinginseveralplaces.Thusanimportantaspectofimprovingdesignistoeliminateduplicatecode.It’snotthatreducingtheamountofcodewillmakethesystemrunanyfaster—theeffectonthefootprintoftheprogramsrarelyissignificant.Reducingtheamountofcodedoes,however,makeabigdifferenceinmodificationofthecode.Themorecodethereis,theharderitistomodifycorrectly.There’smorecodeformetounderstand.Ichangethisbitofcodehere,butthesystemdoesn’tdowhatI
expectbecauseIdidn’tchangethatbitovertherethatdoesmuchthesamethinginaslightlydifferentcontext.Byeliminatingduplication,Iensurethatthecodesayseverythingonceandonlyonce,whichistheessenceofgooddesign.
RefactoringMakesSoftwareEasiertoUnderstand
Programmingisinmanywaysaconversationwithacomputer.Iwritecodethattellsthecomputerwhattodo,anditrespondsbydoingexactlywhatItellit.Intime,IclosethegapbetweenwhatIwantittodoandwhatItellittodo.ProgrammingisallaboutsayingexactlywhatIwant.Buttherearelikelytobeotherusersofmysourcecode.Inafewmonths,ahumanwilltrytoreadmycodetomakesomechanges.Thatuser,whoweoftenforget,isactuallythemostimportant.Whocaresifthecomputertakesafewmorecyclestocompilesomething?Yetitdoesmatterifittakesaprogrammeraweektomakeachangethatwouldhavetakenonlyanhourwithproperunderstandingofmycode.
ThetroubleisthatwhenI’mtryingtogettheprogramtowork,I’mnotthinkingaboutthatfuturedeveloper.Ittakesachangeofrhythmtomakethecodeeasiertounderstand.Refactoringhelpsmemakemycodemorereadable.Beforerefactoring,Ihavecodethatworksbutisnotideallystructured.Alittletimespentonrefactoringcanmakethecodebettercommunicateitspurpose—saymoreclearlywhatIwant.
I’mnotnecessarilybeingaltruisticaboutthis.Often,thisfuturedeveloperismyself.Thismakesrefactoringevenmoreimportant.I’maverylazyprogrammer.OneofmyformsoflazinessisthatIneverrememberthingsaboutthecodeIwrite.Indeed,IdeliberatelytrynotrememberanythingIcanlookup,becauseI’mafraidmybrainwillgetfull.ImakeapointoftryingtoputeverythingIshouldrememberintothecodesoIdon’thavetorememberit.ThatwayI’mlessworriedaboutMaudite[bib-maudite]killingoffmybraincells.
RefactoringHelpsMeFindBugs
Helpinunderstandingthecodealsomeanshelpinspottingbugs.IadmitI’mnotterriblygoodatfindingbugs.Somepeoplecanreadalumpofcodeandseebugs;Icannot.However,IfindthatifIrefactorcode,Iworkdeeplyonunderstandingwhatthecodedoes,andIputthatnewunderstandingrightbackintothecode.Byclarifyingthestructureoftheprogram,Iclarifycertain
assumptionsI’vemade—toapointwhereevenIcan’tavoidspottingthebugs.
ItremindsmeofastatementKentBeckoftenmakesabouthimself:“I’mnotagreatprogrammer;I’mjustagoodprogrammerwithgreathabits.”Refactoringhelpsmebemuchmoreeffectiveatwritingrobustcode.
RefactoringHelpsMeProgramFaster
Intheend,alltheearlierpointscomedowntothis:Refactoringhelpsmedevelopcodemorequickly.
Thissoundscounterintuitive.WhenItalkaboutrefactoring,peoplecaneasilyseethatitimprovesquality.Betterinternaldesign,readability,reducingbugs—alltheseimprovequality.Butdoesn’tthetimeIspendonrefactoringreducethespeedofdevelopment?
WhenItalktosoftwaredeveloperswhohavebeenworkingonasystemforawhile,Ioftenhearthattheywereabletomakeprogressrapidlyatfirst,butnowittakesmuchlongertoaddnewfeatures.Everynewfeaturerequiresmoreandmoretimetounderstandhowtofititintotheexistingcodebase,andonceit’sadded,bugsoftencropupthattakeevenlongertofix.Thecodebasestartslookinglikeaseriesofpatchescoveringpatches,andittakesanexerciseinarchaeologytofigureouthowthingswork.Thisburdenslowsdownaddingnewfeatures—tothepointthatdeveloperswishtheycouldstartagainfromablankslate.
Icanvisualizethisstateofaffairswiththefollowingpseudo-graph:
Butsometeamsreportadifferentexperience.Theyfindtheycanaddnewfeaturesfasterbecausetheycanleveragetheexistingthingsbyquicklybuilding
onwhat’salreadythere.
Thedifferencebetweenthesetwoistheinternalqualityofthesoftware.SoftwarewithagoodinternaldesignallowsmetoeasilyfindhowandwhereIneedtomakechangestoaddanewfeature.Goodmodularityallowsmetoonlyhavetounderstandasmallsubsetofthecodebasetomakeachange.Ifthecodeisclear,I’mlesslikelytointroduceabug,andifIdo,thedebuggingeffortismucheasier.Donewell,mycodebaseturnsintoaplatformforbuildingnewfeaturesforitsdomain.
IrefertothiseffectastheDesignStaminaHypothesis(https://martinfowler.com/bliki/DesignStaminaHypothesis.html):Byputtingoureffortintoagoodinternaldesign,weincreasethestaminaofthesoftwareeffort,allowingustogofasterforlonger.Ican’tprovethatthisisthecase,whichiswhyIrefertoitasahypothesis.Butitexplainsmyexperience,togetherwiththeexperienceofhundredsofgreatprogrammersthatI’vegottoknowovermycareer.
Twentyyearsago,theconventionalwisdomwasthattogetthiskindofgooddesign,ithadtobecompletedbeforestartingtoprogram—becauseoncewewrotethecode,wecouldonlyfacedecay.Refactoringchangesthispicture.We
nowknowwecanimprovethedesignofexistingcode—sowecanformandimproveadesignovertime,evenastheneedsoftheprogramchange.Sinceitisverydifficulttodoagooddesignup-front,refactoringbecomesvitaltoachievingthatvirtuouspathofrapidfunctionality.
WhenShouldWeRefactor?
RefactoringissomethingIdoeveryhourIprogram.Ihavenoticedanumberofwaysitfitsintomyworkflow.
TheRuleofThree
Here’saguidelineDonRobertsgaveme:Thefirsttimeyoudosomething,youjustdoit.Thesecondtimeyoudosomethingsimilar,youwinceattheduplication,butyoudotheduplicatethinganyway.Thethirdtimeyoudosomethingsimilar,yourefactor.
Orforthosewholikebaseball:Threestrikes,thenyourefactor.
PreparatoryRefactoring—MakingItEasiertoAddaFeature
ThebesttimetorefactorisjustbeforeIneedtoaddanewfeaturetothecodebase.AsIdothis,Ilookattheexistingcodeand,often,seethatifitwerestructuredalittledifferently,myworkwouldbemucheasier.Perhapsthere’sfunctionthatdoesalmostallthatIneed,buthassomeliteralvaluesthatconflictwithmyneeds.WithoutrefactoringImightcopythefunctionandchangethosevalues.Butthatleadstoduplicatedcode—ifIneedtochangeitinthefuture,I’llhavetochangebothspots(and,worse,findthem).Andcopy-pastewon’thelpmeifIneedtomakeasimilarvariationforanewfeatureinthefuture.Sowithmyrefactoringhaton,IuseParameterizeFunction(308).OnceI’vedonethat,allIneedtodoiscallthefunctionwiththeparametersIneed.
It’slikeIwanttogo100mileseastbutinsteadofjusttraipsingthroughthewoods,I’mgoingtodrive20milesnorthtothehighwayandthenI’mgoingtogo100mileseastatthreetimesthespeedIcouldhaveifIjustwentstraightthere.Whenpeoplearepushingyoutojustgostraightthere,sometimesyouneedtosay,“Wait,Ineedtocheckthemapandfindthequickestroute.”Thepreparatoryrefactoringdoesthatforme.
TODOaddlink
—JessicaKerr
Thesamehappenswhenfixingabug.OnceI’vefoundthecauseoftheproblem,IseethatitwouldbemucheasiertofixshouldIunifythethreebitsofcopiedcodecausingtheerrorintoone.Orperhapsseparatingsomeupdatelogicfromquerieswillmakeiteasiertoavoidthetanglingthat’scausingtheerror.Byrefactoringtoimprovethesituation,Ialsoincreasethechancesthatthebugwillstayfixed,andreducethechancesthatotherswillappearinthesamecrevicesofthecode.
ComprehensionRefactoring:MakingCodeEasiertoUnderstand
BeforeIcanchangesomecode,Ineedtounderstandwhatitdoes.Thiscodemayhavebeenwrittenbymeorbysomeoneelse.WheneverIhavetothinktounderstandwhatthecodeisdoing,IaskmyselfifIcanrefactorthecodetomakethatunderstandingmoreimmediatelyapparent.Imaybelookingatsomeconditionallogicthat’sstructuredawkwardly.Imayhavewantedtousesomeexistingfunctionsbutspentseveralminutesfiguringoutwhattheydidbecausetheywerenamedbadly.
AtthatpointIhavesomeunderstandinginmyhead,butmyheadisn’taverygoodrecordofsuchdetails.AsWardCunninghamputsit,byrefactoringImovetheunderstandingfrommyheadintothecodeitself.Ithentestthatunderstandingbyrunningthesoftwaretoseeifitstillworks.IfImovemyunderstandingintothecode,itwillbepreservedlongerandbevisibletomycolleagues.
Thatdoesn’tjusthelpmeinthefuture—itoftenhelpsmerightnow.Earlyon,Idocomprehensionrefactoringonlittledetails.IrenameacouplevariablesnowthatIunderstandwhattheyare,orIchopalongfunctionintosmallerparts.Then,asthecodegetsclearer,IfindIcanseethingsaboutthedesignthatIcouldnotseebefore.HadInotchangedthecode,Iprobablyneverwouldhaveseenthesethings,becauseI’mjustnotcleverenoughtovisualizeallthesechangesinmyhead.RalphJohnsondescribestheseearlyrefactoringsaswipingthedirtoffawindowsoyoucanseebeyond.WhenI’mstudyingcode,refactoringleadsmetohigherlevelsofunderstandingthatIwouldotherwisemiss.Thosewhodismisscomprehensionrefactoringasuselessfiddlingwiththe
codedon’trealizethatbyforegoingittheyneverseetheopportunitieshiddenbehindtheconfusion.
Litter-PickupRefactoring
AvariationofcomprehensionrefactoringiswhenIunderstandwhatthecodeisdoing,butrealizethatit’sdoingitbadly.Thelogicisunnecessarilyconvoluted,orIseefunctionsthatarenearlyidenticalandcanbereplacedbyasingleparameterizedfunction.There’sabitofatrade-offhere.Idon’twanttospendalotoftimedistractedfromthetaskI’mcurrentlydoing,butIalsodon’twanttoleavethetrashlyingaroundandgettinginthewayoffuturechanges.Ifit’seasytochange,I’lldoitrightaway.Ifit’sabitmoreefforttofix,ImightmakeanoteofitandfixitwhenI’mdonewithmyimmediatetask.
Sometimes,ofcourse,it’sgoingtotakeafewhourstofix,andIhavemoreurgentthingstodo.Eventhen,however,it’susuallyworthwhiletomakeitalittlebitbetter.Astheoldcampingadagesays,alwaysleavethecampsitecleanerthanwhenyoufoundit.IfImakeitalittlebettereachtimeIpassthroughthecode,overtimeitwillgetfixed.ThenicethingaboutrefactoringisthatIdon’tbreakthecodewitheachsmallstep—so,sometimes,ittakesmonthstocompletethejobbutthecodeisneverbrokenevenwhenI’mpartwaythroughit.
PlannedandOpportunisticRefactoring
Theexamplesabove—preparatory,comprehension,litter-pickuprefactoring—areallopportunistic.Idon’tsetasidetimeatthebeginningtospendonrefactoring—instead,Idorefactoringaspartofaddingafeatureorfixingabug.It’spartofmynaturalflowofprogramming.WhetherI’maddingafeatureorfixingabug,refactoringhelpsmedotheimmediatetaskandalsosetsmeuptomakefutureworkeasier.Thisisanimportantpointthat’sfrequentlymissed.Refactoringisn’tanactivitythat’sseparatedfromprogramming—anymorethanyousetasidetimetowriteifstatements.Idon’tputtimeonmyplanstodorefactoring;mostrefactoringhappenswhileI’mdoingotherthings.
Youhavetorefactorwhenyourunintouglycode—butexcellentcodeneedsplentyofrefactoringtoo.
It’salsoacommonerrortoseerefactoringassomethingpeopledotofixpastmistakesorcleanupuglycode.Certainlyyouhavetorefactorwhenyourunintouglycode,butexcellentcodeneedsplentyofrefactoringtoo.WheneverIwritecode,I’mmakingtradeoffs—howmuchtoIneedtoparameterize,wheretodrawthelinesbetweenfunctions?ThetradeoffsImadecorrectlyforyesterday’sfeaturesetmaynolongerbetherightonesforthenewfeaturesI’maddingtoday.TheadvantageisthatcleancodeiseasiertorefactorwhenIneedtochangethosetradeoffstoreflectthenewreality.
foreachdesiredchange,makethechangeeasy(warning:thismaybehard),thenmaketheeasychange
TODOaddlink
—KentBeck
Foralongtime,peoplethoughtofwritingsoftwareasaprocessofaccretion:Toaddnewfeatures,weshouldbemostlyaddingnewcode.Butgooddevelopersknowthat,often,thefastestwaytoaddanewfeatureistochangethecodetomakeiteasytoadd.Softwareshouldthusbeneverthoughtofas“done.”Asnewcapabilitiesareneeded,thesoftwarechangestoreflectthat.Thosechangescanoftenbegreaterintheexistingcodethaninthenewcode.
Allthisdoesn’tmeanthatplannedrefactoringisalwayswrong.Ifateamhasneglectedrefactoring,itoftenneedsdedicatedtimetogettheircodebaseintoabetterstatefornewfeatures,andaweekspentrefactoringnowcanrepayitselfoverthenextcoupleofmonths.Sometimes,evenwithregularrefactoringI’llseeaproblemareagrowtothepointwhenitneedssomeconcertedefforttofix.Butsuchplannedrefactoringepisodesshouldberare.Mostrefactoringeffortshouldbetheunremarkable,opportunistickind.
OnebitofadviceI’veheardistoseparaterefactoringworkandnewfeatureadditionsintoseparateversion-controlcommits.Thebigadvantageofthisisthattheycanbereviewedandapprovedindependently.I’mnotconvincedofthis,however.Toooften,therefactoringsarecloselyinterwovenwithaddingnewfeatures,andit’snotworththetimetoseparatethemout.Thiscanalsoremovethecontextfortherefactoring,makingtherefactoringcommitshardtojustify.Eachteamshouldexperimenttofindwhatworksforthem;justrememberthatseparatingrefactoringcommitsisnotaself-evidentprinciple—it’sonly
worthwhileifitmakeslifeeasier.
Long-TermRefactoring
Mostrefactoringcanbecompletedwithinafewminutes—hoursatmost.Buttherearesomelargerrefactoringeffortsthatcantakeateamweekstocomplete.Perhapstheyneedtoreplaceanexistinglibrarywithanewone.Orpullsomesectionofcodeoutintoacomponentthattheycansharewithanotherteam.Orfixsomenastymessofdependenciesthattheyhadallowedtobuildup.
Eveninsuchcases,I’mreluctanttohaveateamdodedicatedrefactoring.Often,ausefulstrategyistoagreetograduallyworkontheproblemoverthecourseofthenextfewweeks.Wheneveranyonegoesnearanycodethat’sintherefactoringzone,theymoveitalittlewayinthedirectiontheywanttoimprove.Thistakesadvantageofthefactthatrefactoringdoesn’tbreakthecode—eachsmallchangeleaveseverythinginastill-workingstate.Tochangefromonelibrarytoanother,startbyintroducinganewabstractionthatcanactasaninterfacetoeitherlibrary.Oncethecallingcodeusesthisabstraction,it’smucheasiertoswitchonelibraryforanother.(ThistacticiscalledBranchByAbstraction(https://martinfowler.com/bliki/BranchByAbstraction.html))
RefactoringinaCodeReview
Someorganizationsdoregularcodereviews;thosethatdon’twoulddobetteriftheydid.Codereviewshelpspreadknowledgethroughadevelopmentteam.Reviewshelpmoreexperienceddeveloperspassknowledgetothoselessexperienced.Theyhelpmorepeopleunderstandmoreaspectsofalargesoftwaresystem.Theyarealsoveryimportantinwritingclearcode.Mycodemaylookcleartomebutnottomyteam.That’sinevitable—it’shardforpeopletoputthemselvesintheshoesofsomeoneunfamiliarwithwhatevertheyareworkingon.Reviewsalsogivetheopportunityformorepeopletosuggestusefulideas.Icanonlythinkofsomanygoodideasinaweek.Havingotherpeoplecontributemakesmylifeeasier,soIalwayslookforreviews.
I’vefoundthatrefactoringhelpsmereviewsomeoneelse’scode.BeforeIstartedusingrefactoring,Icouldreadthecode,understandittosomedegree,andmakesuggestions.Now,whenIcomeupwithideas,Iconsiderwhethertheycanbeeasilyimplementedthenandtherewithrefactoring.Ifso,Irefactor.
WhenIdoitafewtimes,Icanseemoreclearlywhatthecodelookslikewiththesuggestionsinplace.Idon’thavetoimaginewhatitwouldbelike—Icanseeit.Asaresult,IcancomeupwithasecondlevelofideasthatIwouldneverhaverealizedhadInotrefactored.
Refactoringalsohelpsgetmoreconcreteresultsfromthecodereview.Notonlyaretheresuggestions;manysuggestionsareimplementedthereandthen.Youendupwithmuchmoreofasenseofaccomplishmentfromtheexercise.
HowI’dembedrefactoringintoacodereviewdependsonthenatureofthereview.Thecommonpullrequestmodel,whereareviewerlooksatcodewithouttheoriginalauthor,doesn’tworktoowell.It’sbettertohavetheoriginalauthorofthecodepresentbecausetheauthorcanprovidecontextonthecodeandfullyappreciatethereviewers’intentionsfortheirchanges.I’vehadmybestexperienceswiththisbysittingone-on-onewiththeoriginalauthor,goingthroughthecodeandrefactoringaswego.Thelogicalconclusionofthisstyleispairprogramming:continuouscodereviewembeddedwithintheprocessofprogramming.
WhatDoITellMyManager?
OneofthemostcommonquestionsI’vebeenaskedis,“Howtotellamanageraboutrefactoring?”I’vecertainlyseenplaceswererefactoringhasbecomeadirtyword—withmanagers(andcustomers)believingthatrefactoringiseithercorrectingerrorsmadeearlier,orworkthatdoesn’tyieldvaluablefeatures.Thisisexacerbatedbyteamsschedulingweeksofpurerefactoring—especiallyifwhattheyarereallydoingisnotrefactoringbutlesscarefulrestructuringthatcausesbreakagesinthecodebase.
Toamanagerwhoisgenuinelysavvyabouttechnologyandunderstandsthedesignstaminahypothesis,refactoringisn’thardtojustify.Suchmanagersshouldbeencouragingrefactoringonaregularbasisandbelookingforsignsthatindicateateamisn’tdoingenough.Whileitdoeshappenthatteamsdotoomuchrefactoring,it’smuchrarerthanteamsnotdoingenough.
Ofcourse,manymanagersandcustomerdon’thavethetechnicalawarenesstoknowhowcodebasehealthimpactsproductivity.InthesecasesIgivemymorecontroversialadvice:Don’ttell!
Subversive?Idon’tthinkso.Softwaredevelopersareprofessionals.Ourjobistobuildeffectivesoftwareasrapidlyaswecan.Myexperienceisthatrefactoringisabigaidtobuildingsoftwarequickly.IfIneedtoaddanewfunctionandthedesigndoesnotsuitthechange,Ifindit’squickertorefactorfirstandthenaddthefunction.IfIneedtofixabug,Ineedtounderstandhowthesoftwareworks—andIfindrefactoringisthefastestwaytodothis.Aschedule-drivenmanagerwantsmetodothingsthefastestwayIcan;howIdoitismyresponsibility.I’mbeingpaidformyexpertiseinprogrammingnewcapabilitiesfast,andthefastestwayisbyrefactoring—thereforeIrefactor.
WhenShouldINotRefactor?
ItmaysoundlikeIalwaysrecommendrefactoring—buttherearecaseswhenit’snotworthwhile.
IfIrunacrosscodethatisamess,butIdon’tneedtomodifyit,thenIdon’tneedtorefactorit.SomeuglycodethatIcantreatasanAPImayremainugly.It’sonlywhenIneedtounderstandhowitworksthatrefactoringgivesmeanybenefit.
Anothercaseiswhenit’seasiertorewriteitthantorefactorit.Thisisatrickydecision.Often,Ican’ttellhoweasyitistorefactorsomecodeunlessIspendsometimetryingandthusgetasenseofhowdifficultitis.Thedecisiontorefactororrewriterequiresgoodjudgmentandexperience,andIcan’treallyboilitdownintoapieceofsimpleadvice.
ProblemswithRefactoring
Wheneveranyoneadvocatesforsometechnique,tool,orarchitecture,Ialwayslookforproblems.Fewthingsinlifeareallsunshineandclearskies.Youneedtounderstandthetrade-offstodecidewhenandwheretoapplysomething.Idothinkrefactoringisavaluabletechnique—onethatshouldbeusedmorebymostteams.Butthereareproblemsassociatedwithit,andit’simportanttounderstandhowtheymanifestthemselvesandhowwecanreacttothem.
SlowingDownNewFeatures
Ifyoureadtheprevioussection,youshouldalreadyknowmyresponse.
Althoughmanypeopleseetimespentrefactoringasslowingdownthedevelopmentofnewfeatures,thewholepurposeofrefactoringistospeedthingsup.Butwhilethisistrue,it’salsotruethattheperceptionofrefactoringasslowingthingsdownisstillcommon—andperhapsthebiggestbarriertopeopledoingenoughrefactoring.
Thewholepurposeofrefactoringistomakeusprogramfaster,producingmorevaluewithlesseffort.
Thereisagenuinetrade-offhere.IdorunintosituationswhereIseea(large-scale)refactoringthatreallyneedstobedone,butthenewfeatureIwanttoaddissosmallthatIprefertoadditandleavethelargerrefactoringalone.That’sajudgmentcall—partofmyprofessionalskillsasaprogrammer.Ican’teasilydescribe,letalonequantify,howImakethattrade-off.
I’mveryconsciousthatpreparatoryrefactoringoftenmakesachangeeasier,soIcertainlywilldoitifIseethatitmakesmynewfeatureeasiertoimplement.I’malsomoreinclinedtorefactorifthisisaproblemI’veseenbefore—sometimesittakesmeacoupleoftimesseeingsomeparticularuglinessbeforeIdecidetorefactoritaway.Conversely,I’mmorelikelytonotrefactorifit’spartofthecodeIrarelytouchandthecostoftheinconvenienceisn’tsomethingIfeelveryoften.Sometimes,IdelayarefactoringbecauseI’mnotsurewhatimprovementtodo,althoughatothertimesI’lltrysomethingasanexperimenttoseeifitmakesthingsbetter.
Still,theevidenceIhearfrommycolleaguesintheindustryisthattoolittlerefactoringisfarmoreprevalentthantoomuch.Inotherwords,mostpeopleshouldtrytorefactormoreoften.Youmayhavetroubletellingthedifferenceinproductivitybetweenahealthyandasicklycodebasebecauseyouhaven’thadenoughexperienceofahealthycodebase—ofthepowerthatcomesfromeasilycombiningexistingpartsintonewconfigurationstoquicklyenablecomplicatednewfeatures.
Althoughit’softenmanagersthatarecriticizedforthecounter-productivehabitofsquelchingrefactoringinthenameofspeed,I’veoftenseendevelopersdoittothemselves.Sometimes,theythinktheyshouldn’tberefactoringeventhoughtheirleadershipisactuallyinfavor.Ifyou’reatechleadinateam,it’simportanttoshowteammembersthatyouvalueimprovingthehealthofacodebase.ThatjudgmentImentionedearlieronwhethertorefactorornotissomethingthat
takesyearsofexperiencetobuildup.Thosewithlessexperienceinrefactoringneedlotsofmentoringtoacceleratethemthroughtheprocess.
ButIthinkthemostdangerouswaythatpeoplegettrappediswhentheytrytojustifyrefactoringintermsof“cleancode,”“goodengineeringpractice,”orsimilarmoralreasons.Thepointofrefactoringisn’ttoshowhowsparklyacodebaseis—itispurelyeconomic.Werefactorbecauseitmakesusfaster—fastertoaddfeatures,fastertofixbugs.It’simportanttokeepthatinfrontofyourmindandinfrontofcommunicationwithothers.Theeconomicbenefitsofrefactoringshouldalwaysbethedrivingfactor,andthemorethatisunderstoodbydevelopers,managers,andcustomers,themoreofthe“gooddesign”curvewe’llsee.
CodeOwnership
Manyrefactoringsinvolvemakingchangesthataffectnotjusttheinternalsofamodulebutitsrelationshipswithotherpartsofasystem.IfIwanttorenameafunction,andIcanfindallthecallerstoafunction,IsimplyapplyChangeFunctionDeclaration(124)andchangethedeclarationandthecallersinonechange.Butsometimesthissimplerefactoringisn’tpossible.PerhapsthecallingcodeisownedbyadifferentteamandIdon’thavewriteaccesstotheirrepository.PerhapsthefunctionisadeclaredAPIusedbymycustomers—soIcan’teventellifit’sbeingused,letalonebywhoandhowmuch.Suchfunctionsarepartofapublishedinterface—aninterfacethatisusedbyclientsindependentofthosewhodeclaretheinterface.
CodeownershipboundariesgetinthewayofrefactoringbecauseIcannotmakethekindsofchangesIwantwithoutbreakingmyclients.Thisdon’tpreventrefactoring—Icanstilldoagreatdeal—butitdoesimposelimitations.Whenrenamingafunction,IneedtouseChangeFunctionDeclaration(124)andtoretaintheolddeclarationasapass-throughtothenewone.Thiscomplicatestheinterface—butitisthepriceImustpaytoavoidbreakingmyclients.Imaybeabletomarktheoldinterfaceasdeprecatedand,intime,retireit,butsometimesIhavetoretainthatinterfaceforever.
Duetothesecomplexities,Irecommendagainstfine-grainedstrongcodeownership.Someorganizationslikeanypieceofcodetohaveasingleprogrammerasanowner,andonlyallowthatprogrammertochangeit.I’veseenateamofthreepeopleoperateinsuchawaythateachonepublishedinterfaces
totheothertwo.Thisledtoallsortsofgyrationstomaintaininterfaceswhenitwouldhavebeenmucheasiertogointothecodebaseandmaketheedits.Mypreferenceistoallowteamownershipofcode—sothatanyoneinthesameteamcanmodifytheteam’scode,eveniforiginallywrittenbysomeoneelse.Programmersmayhaveindividualresponsibilityforareasofasystem,butthatshouldimplythattheymonitorchangestotheirareaofresponsibility,notblockthembydefault.
Suchamorepermissiveownershipschemecanevenexistacrossteams.Someteamsencourageanopen-sourcelikemodelwherepeoplefromotherteamscanchangeabranchoftheircodeandsendthecommitintobeapproved.Thisallowsoneteamtochangetheclientsoftheirfunctions—theycandeletetheolddeclarationsoncetheircommitstotheirclientshavebeenaccepted.Thiscanoftenbeagoodcompromisebetweenstrongcodeownershipandchaoticchangesinlargesystems.
Branches
AsIwritethis,acommonapproachinteamsisforeachteammembertoworkonabranchofthecodebaseusingaversioncontrolsystem,anddoconsiderableworkonthatbranchbeforeintegratingwithamainline(oftencalledmasterortrunk)sharedacrosstheteam.Often,thisinvolvesbuildingawholefeatureonabranch,notintegratingintothemainlineuntilthefeatureisreadytobereleasedintoproduction.Fansofthisapproachclaimthatitkeepsthemainlineclearofanyin-processcode,providesaclearversionhistoryoffeatureadditions,andallowsfeaturestoberevertedeasilyshouldtheycauseproblems.
Therearedownsidestofeaturebrancheslikethis.ThelongerIworkonanisolatedbranch,theharderthejobofintegratingmyworkwithmainlineisgoingtobewhenI’mdone.Mostpeoplereducethispainbyfrequentlymergingorre-basingfrommainlinetomybranch.Butthisdoesn’treallysolvetheproblemwhenseveralpeopleareworkingonindividualfeaturebranches.Idistinguishbetweenmergingandintegration.IfImergemainlineintomycode,thisisaonewaymovement—mybranchchangesbutthemainlinedoesn’t.Iuse“integrate”tomeanatwo-wayprocessthatpullschangesfrommainlineintomybranchandthenpushestheresultbackintomainline,changingboth.IfRachelisworkingonherbranchIdon’tseeherchangesuntilsheintegrateswithmainline;atthatpoint,Ihavetomergeherchangesintomyfeaturebranch,whichmaymeanconsiderablework.Thehardpartofthisworkisdealingwithsemantic
changes.Modernversioncontrolsystemscandowonderswithmergingcomplexchangestotheprogramtext,buttheyareblindtothesemanticsofthecode.IfI’vechangedthenameofafunction,myversioncontroltoolmayeasilyintegratemychangeswithRachel’s.Butif,inherbranch,sheaddedacalltoafunctionthatI’verenamedinmine,thecodewillfail.
Theproblemofcomplicatedmergesgetsexponentiallyworseasthelengthoffeaturebranchesincreases.Integratingbranchesthatarefourweeksoldismorethantwiceashardasthosethatareacoupleofweeksold.Manypeople,therefore,argueforkeepingfeaturebranchesshort—perhapsjustacoupleofdays.Others,suchasme,wantthemevenshorterthanthat.ThisisanapproachcalledContinuousIntegration(CI),alsoknownasTrunk-BasedDevelopment.WithCI,eachteammemberintegrateswithmainlineatleastonceperday.Thispreventsanybranchesdivertingtoofarfromeachotherandthusgreatlyreducesthecomplexityofmerges.CIdoesn’tcomeforfree:Itmeansyouusepracticestoensurethemainlineishealthy,learntobreaklargefeaturesintosmallerchunks,andusefeaturetoggles(akafeatureflags)toswitchoffanyin-processfeaturesthatcan’tbebrokendown.
FansofCIlikeitpartlybecauseitreducesthecomplexityofmerges,butthedominantreasontofavorCIisthatit’sfarmorecompatiblewithrefactoring.Refactoringsofteninvolvemakinglotsoflittlechangesalloverthecodebase—whichareparticularlypronetosemanticmergeconflicts(suchasrenamingawidelyusedfunction).Manyofushaveseenfeature-branchingteamsthatfindrefactoringssoexacerbatemergeproblemsthattheystoprefactoring.CIandrefactoringworkwelltogether,whichiswhyKentBeckcombinedtheminExtremeProgramming.
I’mnotsayingthatyoushouldneverusefeaturebranches.Iftheyaresufficientlyshort,theirproblemsaremuchreduced.(Indeed,usersofCIusuallyalsousebranches,butintegratethemwithmainlineeachday.)Featurebranchesmaybetherighttechniqueforopensourceprojectswhereyouhaveinfrequentcommitsfromprogrammerswhoyoudon’tknowwell(andthusdon’ttrust).Butinafull-timedevelopmentteam,thecostthatfeaturebranchesimposeonrefactoringisexcessive.Evenifyoudon’tgotofullCI,Icertainlyurgeyoutointegrateasfrequentlyaspossible.Youshouldalsoconsidertheobjectiveevidence[bib-forsgren]thatteamsthatuseCIaremoreeffectiveinsoftwaredelivery.
Testing
Oneofthekeycharacteristicsofrefactoringisthatitdoesn’tchangetheobservablebehavioroftheprogram.IfIfollowtherefactoringscarefully,Ishouldn’tbreakanything—butwhatifImakeamistake?(Or,knowingme,s/if/when.)Mistakeshappen,buttheyaren’taproblemprovidedIcatchthemquickly.Sinceeachrefactoringisasmallchange,ifIbreakanything,Ionlyhaveasmallchangetolookattofindthefault—andifIstillcan’tspotit,Icanrevertmyversioncontroltothelastworkingversion.
Thekeyhereisbeingabletocatchanerrorquickly.Todothis,realistically,Ineedtobeabletorunacomprehensivetestsuiteonthecode—andrunitquickly,sothatI’mnotdeterredfromrunningitfrequently.Thismeansthatinmostcases,ifIwanttorefactor,Ineedtohaveself-testingcode(https://martinfowler.com/bliki/SelfTestingCode.html).
Tosomereaders,self-testingcodesoundslikearequirementsosteepastobeunrealizable.Butoverthelastcoupleofdecades,I’veseenmanyteamsbuildsoftwarethisway.Ittakesattentionanddedicationtotesting,butthebenefitsmakeitreallyworthwhile.Self-testingcodenotonlyenablesrefactoring—italsomakesitmuchsafertoaddnewfeatures,sinceIcanquicklyfindandkillanybugsIintroduce.Thekeypointhereisthatwhenatestfails,IcanlookatthechangeI’vemadebetweenwhenthetestswerelastrunningcorrectlyandthecurrentcode.Withfrequenttestruns,thatwillbeonlyafewlinesofcode.Byknowingitwasthosefewlinesthatcausedthefailure,Icanmuchmoreeasilyfindthebug.
Thisalsoanswersthosewhoareconcernedthatrefactoringcarriestoomuchriskofintroducingbugs.Withoutself-testingcode,that’sareasonableworry—whichiswhyIputsomuchemphasisonhavingsolidtests.
Thereisanotherwaytodealwiththetestingproblem.IfIuseanenvironmentthathasgoodautomatedrefactorings,Icantrustthoserefactoringsevenwithoutrunningtests.Icanthenrefactor,providingIonlyusethoserefactoringsthataresafelyautomated.Thisremovesalotofnicerefactoringsfrommymenu,butstillleavesmeenoughtodeliversomeusefulbenefits.I’dstillratherhaveself-testingcode,butit’sanoptionthatisusefultohaveinthetoolkit.
Thisalsoinspiresastyleofrefactoringthatonlyusesalimitedsetof
refactoringsthatcanbeprovensafe.Suchrefactoringsrequirecarefullyfollowingthesteps,andarelanguage-specific.Butteamsusingthemhavefoundtheycandousefulrefactoringonlargecodebaseswithpoortestcoverage.Idon’tfocusonthatinthisbook,asit’sanewer,lessdescribedandunderstoodtechniquethatinvolvesdetailed,language-specificactivity.(Itis,however,somethingIhopetalkaboutmoreonmywebsiteinthefuture.Foratasteofit,seeJayBazuzi’sdescription[bib-bazuzi-safe]ofasaferwaytodoExtractFunction(106)inC++.)
Self-testingcodeis,unsurprisingly,closelyassociatedwithContinuousIntegration—itisthemechanismthatweusetocatchsemanticintegrationconflicts.SuchtestingpracticesareanothercomponentofExtremeProgrammingandakeypartofContinuousDelivery.
LegacyCode
MostpeoplewouldregardabiglegacyasaGoodThing—butthat’soneofthecaseswhereprogrammers’viewisdifferent.Legacycodeisoftencomplex,frequentlycomeswithpoortests,and,aboveall,iswrittenbySomeoneElse(shudder).
Refactoringcanbeafantastictooltohelpunderstandalegacysystem.Functionswithmisleadingnamescanberenamedsotheymakesense,awkwardprogrammingconstructssmoothedout,andtheprogramturnedfromaroughrocktoapolishedgem.Butthedragonguardingthishappytaleisthecommonlackoftests.Ifyouhaveabiglegacysystemwithnotests,youcan’tsafelyrefactoritintoclarity.
Theobviousanswertothisproblemisthatyouaddtests.Butwhilethissoundsasimple,iflaborious,procedure,it’softenmuchmoretrickyinpractice.Usually,asystemisonlyeasytoputundertestifitwasdesignedwithtestinginmind—inwhichcaseitwouldhavethetestsandIwouldn’tbeworryingaboutit.
There’snosimpleroutetodealingwiththis.ThebestadviceIcangiveistogetacopyofWorkingEffectivelywithLegacyCode[bib-feathers-welc]andfollowitsguidance.Don’tbeworriedbytheageofthebook—itsadviceisjustastruemorethanadecadelater.Tosummarizecrudely,itadvisesyoutogetthesystemundertestbyfindingseamsintheprogramwhereyoucaninserttests.Creatingtheseseamsinvolvesrefactoring—whichismuchmoredangeroussinceit’s
donewithouttests,butisanecessaryrisktomakeprogress.Thisisasituationwheresafe,automatedrefactoringscanbeagodsend.Ifallthissoundsdifficult,that’sbecauseitis.Sadly,there’snoshortcuttogettingoutofaholethisdeep—whichiswhyI’msuchastrongproponentofwritingself-testingcodefromthestart.
EvenwhenIdohavetests,Idon’tadvocatetryingtorefactoracomplicatedlegacymessintobeautifulcodeallatonce.WhatIprefertodoistackleitinrelevantpieces.EachtimeIpassthroughasectionofthecode,Itrytomakeitalittlebitbetter—again,likeleavingacampsitecleanerthanwhenIfoundit.Ifthisisalargesystem,I’lldomorerefactoringinareasIvisitfrequently—whichistherightthingtodobecause,ifIneedtovisitcodefrequently,I’llgetabiggerpayoffbymakingiteasiertounderstand.
Databases
WhenIwrotethefirsteditionofthisbook,Isaidthatrefactoringdatabaseswasaproblemarea.But,withinayearofthebook’spublication,thatwasnolongerthecase.MycolleaguePramodSadalagedevelopedanapproachtoevolutionarydatabasedesign[bib-evo-db]anddatabaserefactoring[bib-refact-db]thatisnowwidelyused.Theessenceofthetechniqueistocombinethestructuralchangestoadatabase’sschemaandaccesscodewithdatamigrationscriptsthatcaneasilycomposetohandlelargechanges.
Considerasimpleexampleofrenamingafield(column).AsinChangeFunctionDeclaration(124),Ineedtofindtheoriginaldeclarationofthestructureandallthecallersofthisstructureandchangetheminasinglechange.Thecomplication,however,isthatIalsohavetotransformanydatathatusestheoldfieldtousethenewone.Iwriteasmallhunkofcodethatcarriesoutthistransformandstoreitinversioncontrol,togetherwiththecodethatchangesanydeclaredstructureandaccessroutines.Then,wheneverIneedtomigratebetweentwoversionsofthedatabase,Irunallthemigrationscriptsthatexistbetweenmycurrentcopyofthedatabaseandmydesiredversion.
Aswithregularrefactoring,thekeyhereisthateachindividualchangeissmallyetcapturesacompletechange,sothesystemstillrunsafterapplyingthemigration.Keepingthemsmallmeanstheyareeasytowrite,butIcanstringmanyofthemintoasequencethatcanmakeasignificantchangetothedatabase’sstructureandthedatastoredinit.
Onedifferencefromregularrefactoringsisthatdatabasechangesoftenarebestseparatedovermultiplereleasestoproduction.Thismakesiteasytoreverseanychangethatcausesaprobleminproduction.So,whenrenamingafield,myfirstcommitwouldaddthenewdatabasefieldbutnotuseit.Imaythensetuptheupdatessotheyupdatebotholdandnewfieldsatonce.Icanthengraduallymovethereadersovertothenewfield.Onlyoncetheyhaveallmovedtothenewfield,andI’vegivenalittletimeforanybugstoshowthemselves,wouldIremovethenow-unusedoldfield.Thisapproachtodatabasechangesisanexampleofageneralapproachofparallelchange(https://martinfowler.com/bliki/ParallelChange.html)(alsocalledexpand-contract).
Refactoring,Architecture,andYagni
Refactoringhasprofoundlychangedhowpeoplethinkaboutsoftwarearchitecture.Earlyinmycareer,Iwastaughtthatsoftwaredesignandarchitecturewassomethingtobeworkedon,andmostlycompleted,beforeanyonestartedwritingcode.Oncethecodewaswritten,itsarchitecturewasfixedandcouldonlydecayduetocarelessness.
Refactoringchangesthisperspective.Itallowsmetosignificantlyalterthearchitectureofsoftwarethat’sbeenrunninginproductionforyears.Refactoringcanimprovethedesignofexistingcode,asthisbook’ssubtitleimplies.ButasIindicatedearlier,changinglegacycodeisoftenchallenging,especiallywhenitlacksdecenttests.
Therealimpactofrefactoringonarchitectureisinhowitcanbeusedtoformawell-designedcodebasethatcanrespondgracefullytochangingneeds.Thebiggestissuewithfinishingarchitecturebeforecodingisthatsuchanapproachassumestherequirementsforthesoftwarecanbeunderstoodearlyon.Butexperienceshowsthatthisisoften,evenusually,anunachievablegoal.Repeatedly,Isawpeopleonlyunderstandwhattheyreallyneededfromsoftwareoncethey’dhadachancetouseit,andsawtheimpactitmadetotheirwork.
Onewayofdealingwithfuturechangesistoputflexibilitymechanismsintothesoftware.AsIwritesomefunction,Icanseethatithasageneralapplicability.TohandlethedifferentcircumstancesthatIanticipateittobeusedin,IcanseeadozenparametersIcouldaddtothatfunction.Theseparametersareflexibility
mechanisms—and,likemostmechanisms,theyarenotafreelunch.Addingallthoseparameterscomplicatesthefunctionfortheonecaseit’susedrightnow.IfImissaparameter,alltheparameterizationIhaveaddedmakesitharderformetoaddmore.IfindIoftengetmyflexibilitymechanismswrong—eitherbecausethechangingneedsdidn’tworkoutthewayIexpectedormymechanismdesignwasfaulty.OnceItakeallthatintoaccount,mostofthetimemyflexibilitymechanismsactuallyslowdownmyabilitytoreacttochange.
Withrefactoring,Icanuseadifferentstrategy.InsteadofspeculatingonwhatflexibilityIwillneedinthefutureandwhatmechanismswillbestenablethat,Ibuildsoftwarethatsolvesonlythecurrentlyunderstoodneeds,butImakethissoftwareexcellentlydesignedforthoseneeds.Asmyunderstandingoftheusers’needschanges,Iuserefactoringtoadaptthearchitecturetothosenewdemands.Icanhappilyincludemechanismsthatdon’tincreasecomplexity(suchassmall,well-namedfunctions)butanyflexibilitythatcomplicatesthesoftwarehastoproveitselfbeforeIincludeit.IfIdon’thavedifferentvaluesforaparameterfromthecallers,Idon’taddittotheparameterlist.ShouldthetimecomethatIneedtoaddit,thenParameterizeFunction(308)isaneasyrefactoringtoapply.Ioftenfinditusefultoestimatehowharditwouldbetouserefactoringlatertosupportananticipatedchange.OnlyifIcanseethatitwouldbesubstantiallyhardertorefactorlaterdoIconsideraddingaflexibilitymechanismnow.
Thisapproachtodesigngoesundervariousnames:simpledesign,incrementaldesign,oryagni[bib-yagni](originallyanacronymfor“youaren’tgoingtoneedit”).Yagnidoesn’timplythatarchitecturalthinkingdisappears,althoughitissometimesnaivelyappliedthatway.Ithinkofyagniasadifferentstyleofincorporatingarchitectureanddesignintothedevelopmentprocess—astylethatisn’tcrediblewithoutthefoundationofrefactoring.
Adoptingyagnidoesn’tmeanIneglectallup-frontarchitecturalthinking.Therearestillcaseswhererefactoringchangesaredifficultandsomepreparatorythinkingcansavetime.Butthebalancehasshiftedalongway—I’mmuchmoreinclinedtodealwithissueslaterwhenIunderstandthembetter.Allthishasledtoagrowingdisciplineofevolutionaryarchitecture[bib-evo-arch]wherearchitectsexplorethepatternsandpracticesthattakeadvantageofourabilitytoiterateoverarchitecturaldecisions.
RefactoringandtheWiderSoftwareDevelopmentProcess
Ifyou’vereadtheearliersectiononproblems,onelessonyou’veprobablydrawnisthattheeffectivenessofrefactoringistiedtoothersoftwarepracticesthatateamuses.Indeed,refactoring’searlyadoptionwasaspartofExtremeProgramming[bib-xp](XP),aprocesswhichwasnotableforputtingtogetherasetofrelativelyunusualandinterdependentpractices—suchascontinuousintegration,self-testingcode,andrefactoring(thelattertwowovenintotest-drivendevelopment).
ExtremeProgrammingwasoneofthefirstagilesoftwaremethods[bib-agile]and,forseveralyears,ledtheriseofagiletechniques.Enoughprojectsnowuseagilemethodsthatagilethinkingisgenerallyregardedasmainstream—butinrealitymost“agile”projectsonlyusethename.Toreallyoperateinanagileway,ateamhastobecapableandenthusiasticrefactorers—andforthat,manyaspectsoftheirprocesshavetoalignwithmakingrefactoringaregularpartoftheirwork.
Thefirstfoundationforrefactoringisself-testingcode.Bythis,ImeanthatthereisasuiteofautomatedteststhatIcanrunandbeconfidentthat,ifImadeanerrorinmyprogramming,sometestwillfail.ThisissuchanimportantfoundationforrefactoringthatI’llspendachaptertalkingmoreaboutthis.
Torefactoronateam,it’simportantthateachmembercanrefactorwhentheyneedtowithoutinterferingwithothers’work.ThisiswhyIencourageContinuousIntegration.WithCI,eachmember’srefactoringeffortsarequicklysharedwiththeircolleagues.Nooneendsupbuildingnewworkoninterfacesthatarebeingremoved,andiftherefactoringisgoingtocauseaproblemwithsomeoneelse’swork,weknowaboutthisquickly.Self-testingcodeisalsoakeyelementofContinuousIntegration,sothereisastrongsynergybetweenthethreepracticesofself-testingcode,continuousintegration,andrefactoring.
Withthistrioofpracticesinplace,weenabletheYagnidesignapproachthatItalkedaboutintheprevioussection.Refactoringandyagnipositivelyreinforceeachother:Notjustisrefactoring(anditspre-requisites)afoundationforyagni—yagnimakesiteasiertodorefactoring.Thisisbecauseit’seasiertochangeasimplesystemthanonethathaslotsofspeculativeflexibilityincluded.Balancethesepractices,andyoucangetintoavirtuouscirclewithacodebasethatrespondsrapidlytochangingneedsandisreliable.
Withthesecorepracticesinplace,wehavethefoundationtotakeadvantageof
theotherelementsoftheagilemindset.ContinuousDeliverykeepsoursoftwareinanalways-releasablestate.Thisiswhatallowsmanyweborganizationstoreleaseupdatesmanytimesaday—butevenifwedon’tneedthat,itreducesriskandallowsustoscheduleourreleasestosatisfybusinessneedsratherthantechnologicalconstraints.Withafirmtechnicalfoundation,wecandrasticallyreducethetimeittakestogetagoodideaintoproductioncode,allowingustobetterserveourcustomers.Furthermore,thesepracticesincreasethereliabilityofoursoftware,withlessbugstospendtimefixing.
Statedlikethis,itallsoundsrathersimple—butinpracticeitisn’t.Softwaredevelopment,whatevertheapproach,isatrickybusiness,withcomplexinteractionsbetweenpeopleandmachines.TheapproachIdescribehereisaprovenwaytohandlethiscomplexity,butlikeanyapproach,itrequirespracticeandskill.
RefactoringandPerformance
Acommonconcernwithrefactoringistheeffectithasontheperformanceofaprogram.Tomakethesoftwareeasiertounderstand,Ioftenmakechangesthatwillcausetheprogramtorunslower.Thisisanimportantissue.Idon’tbelongtotheschoolofthoughtthatignoresperformanceinfavorofdesignpurityorinhopesoffasterhardware.Softwarehasbeenrejectedforbeingtooslow,andfastermachinesmerelymovethegoalposts.Refactoringcancertainlymakesoftwaregomoreslowly—butitalsomakesthesoftwaremoreamenabletoperformancetuning.Thesecrettofastsoftware,inallbuthardreal-timecontexts,istowritetunablesoftwarefirstandthentuneitforsufficientspeed.
I’veseenthreegeneralapproachestowritingfastsoftware.Themostseriousoftheseistimebudgeting,oftenusedinhardreal-timesystems.Asyoudecomposethedesign,yougiveeachcomponentabudgetforresources—timeandfootprint.Thatcomponentmustnotexceeditsbudget,althoughamechanismforexchangingbudgetedresourcesisallowed.Timebudgetingfocusesattentiononhardperformancetimes.Itisessentialforsystems,suchasheartpacemakers,inwhichlatedataisalwaysbaddata.Thistechniqueisinappropriateforotherkindsofsystems,suchasthecorporateinformationsystemswithwhichIusuallywork.
Thesecondapproachistheconstantattentionapproach.Here,every
programmer,allthetime,doeswhatevershecantokeepperformancehigh.Thisisacommonapproachthatisintuitivelyattractive—butitdoesnotworkverywell.Changesthatimproveperformanceusuallymaketheprogramhardertoworkwith.Thisslowsdevelopment.Thiswouldbeacostworthpayingiftheresultingsoftwarewerequicker—butusuallyitisnot.Theperformanceimprovementsarespreadallaroundtheprogram;eachimprovementismadewithanarrowperspectiveoftheprogram’sbehavior,andoftenwithamisunderstandingofhowacompiler,runtime,andhardwarebehaves.
ItTakesAwhiletoCreateNothing
TheChryslerComprehensiveCompensationpayprocesswasrunningtooslowly.Althoughwewerestillindevelopment,itbegantobotherus,becauseitwasslowingdownthetests.
KentBeck,MartinFowler,andIdecidedwe’dfixitup.WhileIwaitedforustogettogether,Iwasspeculating,onthebasisofmyextensiveknowledgeofthesystem,aboutwhatwasprobablyslowingitdown.Ithoughtofseveralpossibilitiesandchattedwithfolksaboutthechangesthatwereprobablynecessary.Wecameupwithsomereallygoodideasaboutwhatwouldmakethesystemgofaster.
ThenwemeasuredperformanceusingKent’sprofiler.NoneofthepossibilitiesIhadthoughtofhadanythingtodowiththeproblem.Instead,wefoundthatthesystemwasspendinghalfitstimecreatinginstancesofdate.Evenmoreinterestingwasthatalltheinstanceshadthesamecoupleofvalues.
Whenwelookedatthedate-creationlogic,wesawsomeopportunitiesforoptimizinghowthesedateswerecreated.Theywereallgoingthroughastringconversioneventhoughnoexternalinputswereinvolved.Thecodewasjustusingstringconversionforconvenienceoftyping.Maybewecouldoptimizethat.
Thenwelookedathowthesedateswerebeingused.Itturnedoutthatthehugebulkofthemwereallcreatinginstancesofdaterange,anobjectwithafromdateandatodate.Lookingaroundlittlemore,werealizedthatmostofthesedaterangeswereempty!
Asweworkedwithdaterange,weusedtheconventionthatanydaterangethatendedbeforeitstartedwasempty.It’sagoodconventionandfitsinwellwith
howtheclassworks.Soonafterwestartedusingthisconvention,werealizedthatjustcreatingadaterangethatstartsafteritendswasn’tclearcode,soweextractedthatbehaviorintoafactorymethodforemptydateranges.
Wehadmadethatchangetomakethecodeclearer,butwereceivedanunexpectedpayoff.Wecreatedaconstantemptydaterangeandadjustedthefactorymethodtoreturnthatobjectinsteadofcreatingiteverytime.Thatchangedoubledthespeedofthesystem,enoughfortheteststobebearable.Ittookusaboutfiveminutes.
Ihadspeculatedwithvariousmembersoftheteam(KentandMartindenyparticipatinginthespeculation)onwhatwaslikelywrongwithcodeweknewverywell.Wehadevensketchedsomedesignsforimprovementswithoutfirstmeasuringwhatwasgoingon.
Wewerecompletelywrong.Asidefromhavingareallyinterestingconversation,weweredoingnogoodatall.
Thelessonis:Evenifyouknowexactlywhatisgoingoninyoursystem,measureperformance,don’tspeculate.You’lllearnsomething,andninetimesoutoften,itwon’tbethatyouwereright!
—RonJeffries
Theinterestingthingaboutperformanceisthatinmostprograms,mostoftheirtimeisspentinasmallfractionofthecode.IfIoptimizeallthecodeequally,I’llendupwith90percentofmyworkwastedbecauseit’soptimizingcodethatisn’trunmuch.Thetimespentmakingtheprogramfast—thetimelostbecauseoflackofclarity—isallwastedtime.
Thethirdapproachtoperformanceimprovementtakesadvantageofthis90-percentstatistic.Inthisapproach,Ibuildmyprograminawell-factoredmannerwithoutpayingattentiontoperformanceuntilIbeginadeliberateperformanceoptimizationexercise.Duringthisperformanceoptimization,Ifollowaspecificprocesstotunetheprogram.
Ibeginbyrunningtheprogramunderaprofilerthatmonitorstheprogramandtellsmewhereitisconsumingtimeandspace.ThiswayIcanfindthatsmallpartoftheprogramwheretheperformancehotspotslie.IthenfocusonthoseperformancehotspotsusingthesameoptimizationsIwoulduseintheconstant-
attentionapproach.ButsinceI’mfocusingmyattentiononahotspot,I’mgettingmuchmoreeffectwithlesswork.Evenso,Iremaincautious.Asinrefactoring,Imakethechangesinsmallsteps.AftereachstepIcompile,test,andreruntheprofiler.IfIhaven’timprovedperformance,Ibackoutthechange.IcontinuetheprocessoffindingandremovinghotspotsuntilIgettheperformancethatsatisfiesmyusers.
Havingawell-factoredprogramhelpswiththisstyleofoptimizationintwoways.First,itgivesmetimetospendonperformancetuning.Withwell-factoredcode,Icanaddfunctionalitymorequickly.Thisgivesmemoretimetofocusonperformance.(ProfilingensuresIspendthattimeontherightplace.)Second,withawell-factoredprogramIhavefinergranularityformyperformanceanalysis.Myprofilerleadsmetosmallerpartsofthecode,whichareeasiertotune.Withclearercode,Ihaveabetterunderstandingofmyoptionsandofwhatkindoftuningwillwork.
I’vefoundthatrefactoringhelpsmewritefastsoftware.ItslowsthesoftwareintheshorttermwhileI’mrefactoring,butmakesiteasiertotuneduringoptimization.Iendupwellahead.
WhereDidRefactoringComeFrom?
I’venotsucceededinpinningdownthebirthoftheterm“refactoring.”Goodprogrammershavealwaysspentatleastsometimecleaninguptheircode.Theydothisbecausetheyhavelearnedthatcleancodeiseasiertochangethancomplexandmessycode,andgoodprogrammersknowthattheyrarelywritecleancodethefirsttimearound.
Refactoringgoesbeyondthis.Inthisbook,I’madvocatingrefactoringasakeyelementinthewholeprocessofsoftwaredevelopment.TwoofthefirstpeopletorecognizetheimportanceofrefactoringwereWardCunninghamandKentBeck,whoworkedwithSmalltalkfromthe1980sonward.Smalltalkisanenvironmentthateventhenwasparticularlyhospitabletorefactoring.Itisaverydynamicenvironmentthatallowsyoutoquicklywritehighlyfunctionalsoftware.Smalltalkhadaveryshortcompile-link-executecycleforitstime,whichmadeiteasytochangethingsquicklyatatimewhereovernightcompilecycleswerenotunknown.Itisalsoobject-orientedandthusprovidespowerfultoolsforminimizingtheimpactofchangebehindwell-definedinterfaces.WardandKent
exploredsoftwaredevelopmentapproachesgearedtothiskindofenvironment,andtheirworkdevelopedintoExtremeProgramming.Theyrealizedthatrefactoringwasimportantinimprovingtheirproductivityand,eversince,havebeenworkingwithrefactoring,applyingittoserioussoftwareprojectsandrefiningit.
WardandKent’sideaswereastronginfluenceontheSmalltalkcommunity,andthenotionofrefactoringbecameanimportantelementintheSmalltalkculture.AnotherleadingfigureintheSmalltalkcommunityisRalphJohnson,aprofessorattheUniversityofIllinoisatUrbana-Champaign,whoisfamousasoneoftheauthorsoftheGangofFour[bib-gof]bookondesignpatterns.OneofRalph’sbiggestinterestsisindevelopingsoftwareframeworks.Heexploredhowrefactoringcanhelpdevelopanefficientandflexibleframework.
BillOpdykewasoneofRalph’sdoctoralstudentsandwasparticularlyinterestedinframeworks.HesawthepotentialvalueofrefactoringandsawthatitcouldbeappliedtomuchmorethanSmalltalk.Hisbackgroundwasintelephoneswitchdevelopment,inwhichagreatdealofcomplexityaccruesovertimeandchangesaredifficulttomake.Bill’sdoctoralresearchlookedatrefactoringfromatoolbuilder’sperspective.BillwasinterestedinrefactoringsthatwouldbeusefulforC++frameworkdevelopment;heresearchedthenecessarysemantics-preservingrefactoringsandshowedhowtoprovetheyweresemantics-preservingandhowatoolcouldimplementtheseideas.Bill’sdoctoralthesis[bib-opdyke]wasthefirstsubstantialworkonrefactoring.
IremembermeetingBillattheOOPSLAconferencein1992.Wesatinacaféandhetoldmeabouthisresearch.Irememberthinking,“Interesting,butnotreallythatimportant.”Boy,wasIwrong!
JohnBrantandDonRobertstooktherefactoringtoolideasmuchfurthertoproducetheRefactoringBrowser,thefirstrefactoringtool,appropriatelyfortheSmalltalkenvironment.
Andme?I’dalwaysbeeninclinedtocleancode,butI’dneverconsideredittobethatimportant.Then,IworkedonaprojectwithKentandsawthewayheusedrefactoring.Isawthedifferenceitmadeinproductivityandquality.Thatexperienceconvincedmethatrefactoringwasaveryimportanttechnique.Iwasfrustrated,however,becausetherewasnobookthatIcouldgivetoaworkingprogrammer,andnoneoftheexpertsabovehadanyplanstowritesuchabook.
So,withtheirhelp,Idid—whichledtothefirsteditionofthisbook.
Fortunately,theconceptofrefactoringcaughtonintheindustry.Thebooksoldwell,andrefactoringenteredthevocabularyofmostprogrammers.Moretoolsappeared,especiallyforJava.Onedownsideofthispopularityhasbeenpeopleusing“refactoring”loosely,tomeananykindofrestructuring.Despitethis,however,ithasbecomeamainstreampractice.
AutomatedRefactorings
Perhapsthebiggestchangetorefactoringinthelastdecadeorsoistheavailabilityoftoolsthatsupportautomatedrefactoring.IfIwanttorenameamethodinJavaandI’musingIntelliJIDEA[bib-intellij]orEclipse[bib-eclipse](tomentionjusttwo),Icandoitbypickinganitemoffthemenu.Thetoolcompletestherefactoringforme—andI’musuallysufficientlyconfidentinitsworkthatIdon’tbotherrunningthetestsuite.
ThefirsttoolthatdidthiswastheSmalltalkRefactoringBrowser,writtenbyJohnBrandtandDonRoberts.TheideatookoffintheJavacommunityveryrapidlyatthebeginningofthecentury.WhenJetBrainslaunchedtheirIntelliJIDEAIDE,automatedrefactoringwasoneofthecompellingfeatures.IBMfollowedsuitshortlyafterwardswithrefactoringtoolsinVisualAgeforJava.VisualAgedidn’thaveabigimpact,butmuchofitscapabilitieswerereimplementedinEclipse,includingtherefactoringsupport.
RefactoringalsocametoC#,initiallyviaJetBrains’sResharper,aplug-inforVisualStudio.Lateron,theVisualStudioteamaddedsomerefactoringcapabilities.
It’snowprettycommontofindsomekindofrefactoringsupportineditorsandtools,althoughtheactualcapabilitiesvaryafairbit.Someofthisvariationisduetothetool,someiscausedbythelimitationsofwhatyoucandowithautomatedrefactoringindifferentlanguages.I’mnotgoingtoanalyzethecapabilitiesofdifferenttoolshere,butIthinkitisworthtalkingabitaboutsomeoftheunderlyingprinciples.
Acrudewaytoautomatearefactoringistodotextmanipulation,suchasasearch/replacetochangeaname,orsomesimplereorganizingofcodeforExtractVariable(119).Thisisaverycrudeapproachthatcertainlycan’tbe
trustedwithoutre-runningtests.Itcan,however,beahandyfirststep.I’llusesuchmacrosinEmacstospeedupmyrefactoringworkwhenIdon’thavemoresophisticatedrefactoringsavailabletome.
Todorefactoringproperly,thetoolhastooperateonthesyntaxtreeofthecode,notonthetext.Manipulatingthesyntaxtreeismuchmorereliabletopreservewhatthecodeisdoing.Thisiswhyatthemoment,mostrefactoringcapabilitiesarepartofpowerfulIDEs—theyusethesyntaxtreenotjustforrefactoringbutalsoforcodenavigation,linting,andthelike.Thiscollaborationbetweentextandsyntaxtreeiswhattakesthembeyondtexteditors.
Refactoringisn’tjustunderstandingandupdatingthesyntaxtree.Thetoolalsoneedstofigureouthowtore-renderthecodeintotextbackintheeditorview.Allinall,implementingdecentrefactoringisachallengingprogrammingexercise—onethatI’mmostlyunawareofasIgailyusethetools.
Manyrefactoringsaremademuchsaferwhenappliedinalanguagewithstatictyping.ConsiderthesimpleChangeFunctionDeclaration(124).ImighthaveaddClientmethodsonmySalesmanclassandonmyServerclass.Iwanttorenametheoneonmysalesman,butitisdifferentinintentfromtheoneonmyserver,whichIdon’twanttorename.Withoutstatictyping,thetoolwillfinditdifficulttotellwhetheranycalltoaddClientisintendedforthesalesman.Intherefactoringbrowser,itwouldgeneratealistofcallsitesandIwouldmanuallydecidewhichonestochange.Thismakesitanon-saferefactoringthatforcesmetorerunthetests.Suchatoolisstillhelpful—buttheequivalentoperationinJavacanbecompletelysafeandautomatic.Sincethetoolcanresolvethemethodtothecorrectclasswithstatictyping,Icanbeconfidentthatthetoolchangesonlythemethodsitoughtto.
Toolsoftengofurther.IfIrenameavariable,Icanbepromptedforchangestocommentsthatusethatname.IfIuseExtractFunction(106),thetoolspotssomecodethatduplicatesthenewfunction’sbodyandofferstoreplaceitwithacall.ProgrammingwithpowerfulrefactoringslikethisisacompellingreasontouseanIDEratherthanstickwithafamiliartexteditor.PersonallyI’mabiguserofEmacs,butwhenworkinginJavaIpreferIntelliJIDEAorEclipse—inlargepartduetotherefactoringsupport.
Whilesophisticatedrefactoringtoolsarealmostmagicalintheirabilitytosafelyrefactorcode,therearesomeedgecaseswheretheyslipup.Lessmaturetools
strugglewithreflectivecalls,suchasMethod.invokeinJava(althoughmorematuretoolshandlethisquitewell).Soevenwithmostly-saferefactorings,it’swisetorunthetestsuiteeverysooftentoensurenothinghasgonepear-shaped.UsuallyI’mrefactoringwithamixofautomatedandmanualrefactorings,soIrunmytestsoftenenough.
ThepowerofusingthesyntaxtreetoanalyzeandrefactorprogramsisacompellingadvantageforIDEsoversimpletexteditors,butmanyprogrammersprefertheflexibilityoftheirfavoritetexteditorandwouldliketohaveboth.Atechnologythat’scurrentlygainingmomentumisLanguageServers[bib-lang-server]:softwarethatwillformasyntaxtreeandpresentanAPItotexteditors.Suchlanguageserverscansupportmanytexteditorsandprovidecommandstodosophisticatedcodeanalysisandrefactoringoperations.
GoingFurther
Itseemsalittlestrangetobetalkingaboutfurtherreadinginonlythesecondchapter,butthisisasgoodaspotasanytopointoutthereismorematerialoutthereonrefactoringthatgoesbeyondthebasicsinthisbook.
Thisbookhastaughtrefactoringtomanypeople,butIhavefocusedmoreonarefactoringreferencethanontakingreadersthroughthelearningprocess.Ifyouarelookingforsuchabook,IsuggestBillWake’sRefactoringWorkbook[bib-wake-workbook]thatcontainsmanyexercisestopracticerefactoring.
Manyofthosewhopioneeredrefactoringwerealsoactiveinthesoftwarepatternscommunity.JoshKerievskytiedthesetwoworldscloselytogetherwithRefactoringtoPatterns[bib-r2p],whichlooksatthemostvaluablepatternsfromthehugelyinfluential“GangofFour”book[bib-gof]andshowshowtouserefactoringtoevolvetowardsthem.
Thisbooksconcentratesonrefactoringingeneral-purposeprogramming,butrefactoringalsoappliesinspecializedareas.TwothathavegotusefulattentionareDatabaseRefactoring[bib-refact-db](byScottAmblerandPramodSadalage)andRefactoringHTML[bib-refact-html](byElliotteRustyHarold).
Althoughitdoesn’thaverefactoringinthetitle,alsoworthincludingisMichaelFeathers’sWorkingEffectivelywithLegacyCode[bib-feathers-welc],whichisprimarilyabookabouthowtothinkaboutrefactoringanoldercodebasewith
poortestcoverage.
Althoughthisbook(anditspredecessor)areintendedforprogrammerswithanylanguage,thereisaplaceforlanguage-specificrefactoringbooks.Twoofmyformercolleagues,JayFieldsandShaneHarvey,didthisfortheRubyprogramminglanguage[bib-refact-ruby].
Formoreup-to-datematerial,lookupthewebrepresentationofthisbook,aswellasthemainrefactoringwebsite:refactoring.com[bib-ref.com].
Chapter3BadSmellsinCodebyKentBeckandMartinFowler
Ifitstinks,changeit.—GrandmaBeck,discussingchild-rearingphilosophy
Bynowyouhaveagoodideaofhowrefactoringworks.Butjustbecauseyouknowhowdoesn’tmeanyouknowwhen.Decidingwhentostartrefactoring—andwhentostop—isjustasimportanttorefactoringasknowinghowtooperatethemechanicsofit.
Nowcomesthedilemma.Itiseasytoexplainhowtodeleteaninstancevariableorcreateahierarchy.Thesearesimplematters.Tryingtoexplainwhenyoushoulddothesethingsisnotsocut-and-dried.Insteadofappealingtosomevaguenotionofprogrammingaesthetics(which,frankly,iswhatweconsultantsusuallydo),Iwantedsomethingabitmoresolid.
WhenIwaswritingthefirsteditionofthisbook,IwasmullingoverthisissuewhenIvisitedKentBeckinZurich.Perhapshewasundertheinfluenceoftheodorsofhisnewborndaughteratthetime,buthehadcomeupwiththenotionofdescribingthe“when”ofrefactoringintermsofsmells.
“Smells,”yousay,“andthatissupposedtobebetterthanvagueaesthetics?”Well,yes.Wehavelookedatlotsofcode,writtenforprojectsthatspanthegamutfromwildlysuccessfultonearlydead.Indoingso,wehavelearnedtolookforcertainstructuresinthecodethatsuggest—sometimes,screamfor—thepossibilityofrefactoring.(Weareswitchingoverto“we”inthischaptertoreflectthefactthatKentandIwrotethischapterjointly.Youcantellthedifferencebecausethefunnyjokesaremineandtheothersarehis.)
Onethingwewon’ttrytogiveyouisprecisecriteriaforwhenarefactoringisoverdue.Inourexperience,nosetofmetricsrivalsinformedhumanintuition.Whatwewilldoisgiveyouindicationsthatthereistroublethatcanbesolvedbyarefactoring.Youwillhavetodevelopyourownsenseofhowmanyinstancevariablesorhowmanylinesofcodeinamethodaretoomany.
Usethischapterandthetableontheinsidebackcoverasawaytogiveyouinspirationwhenyou’renotsurewhatrefactoringstodo.Readthechapter(orskimthetable)andtrytoidentifywhatitisyou’resmelling,thengototherefactoringswesuggesttoseewhethertheywillhelpyou.Youmaynotfindtheexactsmellyoucandetect,buthopefullyitshouldpointyouintherightdirection.
MysteriousName
Puzzlingoversometexttounderstandwhat’sgoingonisagreatthingifyou’rereadingadetectivenovel,butnotwhenyou’rereadingcode.WemayfantasizeaboutbeingInternationalMenofMystery,butourcodeneedstobemundaneandclear.Oneofthemostimportantpartsofclearcodeisgoodnames,soweputalotofthoughtintonamingfunctions,modules,variables,classes,sotheyclearlycommunicatewhattheydoandhowtousethem.
Sadly,however,namingisoneofthetwohardthings[bib-2hard]inprogramming.So,perhapsthemostcommonrefactoringswedoaretherenames:ChangeFunctionDeclaration(124)(torenameafunction),RenameVariable(137),andRenameField(244).Peopleareoftenafraidtorenamethings,thinkingit’snotworththetrouble,butagoodnamecansavehoursofpuzzledincomprehensioninthefueture.
Renamingisnotjustanexerciseinchangingnames.Whenyoucan’tthinkofagoodnameforsomething,it’softenasignofadeeperdesignmalaise.Puzzlingoveratrickynamehasoftenledustosignificantsimplificationstoourcode.
DuplicatedCode
Ifyouseethesamecodestructureinmorethanoneplace,youcanbesurethatyourprogramwillbebetterifyoufindawaytounifythem.Duplicationmeansthateverytimeyoureadthesecopies,youneedtoreadthemcarefullytoseeifthere’sanydifference.Ifyouneedtochangetheduplicatedcode,youhavetofindandcatcheachduplication.
Thesimplestduplicatedcodeproblemiswhenyouhavethesameexpressionintwomethodsofthesameclass.ThenallyouhavetodoisExtractFunction(106)andinvokethecodefrombothplaces.Ifyouhavecodethat’ssimilar,but
notquiteidentical,seeifyoucanuseSlideStatements(221)toarrangethecodesothesimilaritemsarealltogetherforeasyextraction.Iftheduplicatefragmentsareinsubclassesofacommonbaseclass,youcanusePullUpMethod(348)toavoidcallingonefromanother.
LongFunction
Inourexperience,theprogramsthatlivebestandlongestarethosewithshortfunctions.Programmersnewtosuchacodebaseoftenfeelthatnocomputationevertakesplace—thattheprogramisanendlesssequenceofdelegation.Whenyouhavelivedwithsuchaprogramforafewyears,however,youlearnjusthowvaluableallthoselittlefunctionsare.Allofthepayoffsofindirection—explanation,sharing,andchoosing—aresupportedbysmallfunctions
Sincetheearlydaysofprogramming,peoplehaverealizedthatthelongerafunctionis,themoredifficultitistounderstand.Olderlanguagescarriedanoverheadinsubroutinecalls,whichdeterredpeoplefromsmallfunctions.Modernlanguageshaveprettymucheliminatedthatoverheadforin-processcalls.Thereisstilloverheadforthereaderofthecodebecauseyouhavetoswitchcontexttoseewhatthefunctiondoes.Developmentenvironmentsthatallowyoutoquicklyjumpbetweenafunctioncallanditsdeclaration,ortoseebothfunctionsatonce,helpeliminatethisstep,buttherealkeytomakingiteasytounderstandsmallfunctionsisgoodnaming.Ifyouhaveagoodnameforafunction,youmostlydon’tneedtolookatitsbody.
Theneteffectisthatyoushouldbemuchmoreaggressiveaboutdecomposingfunctions.Aheuristicwefollowisthatwheneverwefeeltheneedtocommentsomething,wewriteafunctioninstead.Suchafunctioncontainsthecodethatwewantedtocommentbutisnamedaftertheintentionofthecoderatherthanthewayitworks.Wemaydothisonagroupoflinesorevenonasinglelineofcode.Wedothisevenifthemethodcallislongerthanthecodeitreplaces—providedthemethodnameexplainsthepurposeofthecode.Thekeyhereisnotfunctionlengthbutthesemanticdistancebetweenwhatthemethoddoesandhowitdoesit.
Ninety-ninepercentofthetime,allyouhavetodotoshortenafunctionisExtractFunction(106).Findpartsofthefunctionthatseemtogonicelytogetherandmakeanewone.
Ifyouhaveafunctionwithlotsofparametersandtemporaryvariables,theygetinthewayofextracting.IfyoutrytouseExtractFunction(106),youenduppassingsomanyparameterstotheextractedmethodthattheresultisscarcelymorereadablethantheoriginal.YoucanoftenuseReplaceTempwithQuery(176)toeliminatethetemps.LonglistsofparameterscanbeslimmeddownwithIntroduceParameterObject(140)andPreserveWholeObject(317).
Ifyou’vetriedthatandyoustillhavetoomanytempsandparameters,it’stimetogetouttheheavyartillery:ReplaceFunctionwithCommand(335).
Howdoyouidentifytheclumpsofcodetoextract?Agoodtechniqueistolookforcomments.Theyoftensignalthiskindofsemanticdistance.Ablockofcodewithacommentthattellsyouwhatitisdoingcanbereplacedbyamethodwhosenameisbasedonthecomment.Evenasinglelineisworthextractingifitneedsexplanation.
Conditionalsandloopsalsogivesignsforextractions.UseDecomposeConditional(260)todealwithconditionalexpressions.AbigswitchstatementshouldhaveitslegsturnedintosinglefunctioncallswithExtractFunction(106).Ifthere’smorethanoneswitchstatementswitchingonthesamecondition,youshouldapplyReplaceConditionalwithPolymorphism(271).
Withloops,extracttheloopandthecodewithintheloopintoitsownmethod.Ifyoufindithardtogiveanextractedloopaname,thatmaybebecauseit’sdoingtwodifferentthings—inwhichcasedon’tbeafraidtouseSplitLoop(226)tobreakouttheseparatetasks.
LongParameterList
Inourearlyprogrammingdays,weweretaughttopassinasparameterseverythingneededbyafunction.Thiswasunderstandablebecausethealternativewasglobaldata,andglobaldataquicklybecomesevil.Butlongparameterlistsareoftenconfusingintheirownright.
Ifyoucanobtainoneparameterbyaskinganotherparameterforit,youcanuseReplaceParameterwithQuery(322)toremovethesecondparameter.Ratherthanpullinglotsofdataoutofanexistingdatastructure,youcanusePreserveWholeObject(317)topasstheoriginaldatastructureinstead.Ifseveralparametersalwaysfittogether,combinethemwithIntroduceParameterObject
(140).Ifaparameterisusedasaflagtodispatchdifferentbehavior,useRemoveFlagArgument(312).
Classesareagreatwaytoreduceparameterlistsizes.Theyareparticularlyusefulwhenmultiplefunctionsshareseveralparametervalues.Then,youcanuseCombineFunctionsintoClass(144)tocapturethosecommonvaluesasfields.Ifweputonourfunctionalprogramminghats,we’dsaythiscreatesasetofpartiallyappliedfunctions.
GlobalData
Sinceourearliestdaysofwritingsoftware,wewerewarnedoftheperilsofglobaldata—howitwasinventedbydemonsfromthefourthplaneofhell,whichistherestingplaceofanyprogrammerwhodarestouseit.And,althoughwearesomewhatskepticalaboutfireandbrimstone,it’sstilloneofthemostpungentodorswearelikelytoruninto.Theproblemwithglobaldataisthatitcanbemodifiedfromanywhereinthecodebase,andthere’snomechanismtodiscoverwhichbitofcodetouchedit.Timeandagain,thisleadstobugsthatbreedfromaformofspookyactionfromadistance—andit’sveryhardtofindoutwheretheerrantbitofprogramis.Themostobviousformofglobaldataisglobalvariables,butwealsoseethisproblemwithclassvariablesandsingletons.
OurkeydefensehereisEncapsulateVariable(132),whichisalwaysourfirstmovewhenconfrontedwithdatathatisopentocontaminationbyanypartofaprogram.Atleastwhenyouhaveitwrappedbyafunction,youcanstartseeingwhereit’smodifiedandstarttocontrolitsaccess.Then,it’sgoodtolimititsscopeasmuchaspossiblebymovingitwithinaclassormodulewhereonlythatmodule’scodecanseeit.
Globaldataisespeciallynastywhenit’smutable.Globaldatathatyoucanguaranteeneverchangesaftertheprogramstartsisrelativelysafe—ifyouhavealanguagethatcanenforcethatguarantee.
GlobaldataillustratesParacelsus’smaxim:Thedifferencebetweenapoisonandsomethingbenignisthedose.Youcangetawaywithsmalldosesofglobaldata,butitgetsexponentiallyhardertodealwiththemoreyouhave.Evenwithlittlebits,weliketokeepitencapsulated—that’sthekeytocopingwithchangesasthesoftwareevolves.
MutableData
Changestodatacanoftenleadtounexpectedconsequencesandtrickybugs.Icanupdatesomedatahere,notrealizingthatanotherpartofthesoftwareexpectssomethingdifferentandnowfails—afailurethat’sparticularlyhardtospotifitonlyhappensunderrareconditions.Forthisreason,anentireschoolofsoftwaredevelopment—functionalprogramming—isbasedonthenotionthatdatashouldneverchangeandthatupdatingadatastructureshouldalwaysreturnanewcopyofthestructurewiththechange,leavingtheolddatapristine.
Thesekindsoflanguages,however,arestillarelativelysmallpartofprogramming;manyofusworkinlanguagesthatallowvariablestovary.Butthisdoesn’tmeanweshouldignoretheadvantagesofimmutability—therearestillmanythingswecandotolimittherisksonunrestricteddataupdates.
YoucanuseEncapsulateVariable(132)toensurethatallupdatesoccurthroughnarrowfunctionsthatcanbeeasiertomonitorandevolve.Ifavariableisbeingupdatedtostoredifferentthings,useSplitVariable(240)bothtokeepthemseparateandavoidtheriskyupdate.TryasmuchaspossibletomovelogicoutofcodethatprocessestheupdatebyusingSlideStatements(221)andExtractFunction(106)toseparatetheside-effect-freecodefromanythingthatperformstheupdate.InAPIs,useSeparateQueryfromModifier(304)toensurecallersdon’tneedtocallcodethathassideeffectsunlesstheyreallyneedto.WeliketouseRemoveSettingMethod(329)assoonaswecan—sometimes,justtryingtofindclientsofasetterhelpsspotopportunitiestoreducethescopeofavariable.
Mutabledatathatcanbecalculatedelsewhereisparticularlypungent.It’snotjustarichsourceofconfusion,bugs,andmisseddinnersathome—it’salsounnecessary.WesprayitwithaconcentratedsolutionofvinegarandReplaceDerivedVariablewithQuery(248).
Mutabledataisn’tabigproblemwhenit’savariablewhosescopeisjustacoupleoflines—butitsriskincreasesasitsscopegrows.UseCombineFunctionsintoClass(144)orCombineFunctionsintoTransform(149)tolimithowmuchcodeneedstoupdateavariable.Ifavariablecontainssomedatawithinternalstructure,it’susuallybettertoreplacetheentirestructureratherthanmodifyitinplace,usingChangeReferencetoValue(252).
DivergentChange
Westructureoursoftwaretomakechangeeasier;afterall,softwareismeanttobesoft.Whenwemakeachange,wewanttobeabletojumptoasingleclearpointinthesystemandmakethechange.Whenyoucan’tdothis,youaresmellingoneoftwocloselyrelatedpungencies.
Divergentchangeoccurswhenonemoduleisoftenchangedindifferentwaysfordifferentreasons.Ifyoulookatamoduleandsay,“Well,IwillhavetochangethesethreefunctionseverytimeIgetanewdatabase;Ihavetochangethesefourfunctionseverytimethereisanewfinancialinstrument,”thisisanindicationofdivergentchange.Thedatabaseinteractionandfinancialprocessingproblemsareseparatecontexts,andwecanmakeourprogramminglifebetterbymovingsuchcontextsintoseparatemodules.Thatway,whenwehaveachangetoonecontext,weonlyhavetounderstandthatonecontextandignoretheother.Wealwaysfoundthistobeimportant,butnow,withourbrainsshrinkingwithage,itbecomesallthemoreimperative.Ofcourse,youoftendiscoverthisonlyafteryou’veaddedafewdatabasesorfinancialinstruments;contextboundariesareusuallyunclearintheearlydaysofaprogramandcontinuetoshiftasasoftwaresystem’scapabilitieschange.
Ifthetwoaspectsnaturallyformasequence—forexample,yougetdatafromthedatabaseandthenapplyyourfinancialprocessingonit—thenSplitPhase(154)separatesthetwowithacleardatastructurebetweenthem.Ifthere’smoreback-and-forthinthecalls,thencreateappropriatemodulesanduseMoveFunction(196)todividetheprocessingup.Iffunctionsmixthetwotypesofprocessingwithinthemselves,useExtractFunction(106)toseparatethembeforemoving.Ifthemodulesareclasses,thenExtractClass(180)helpsformalizehowtodothesplit.
ShotgunSurgery
Shotgunsurgeryissimilartodivergentchangebutistheopposite.Youwhiffthiswhen,everytimeyoumakeachange,youhavetomakealotoflittleeditstoalotofdifferentclasses.Whenthechangesareallovertheplace,theyarehardtofind,andit’seasytomissanimportantchange.
Inthiscase,youwanttouseMoveFunction(196)andMoveField(205)toput
allthechangesintoasinglemodule.Ifyouhaveabunchoffunctionsoperatingonsimilardata,useCombineFunctionsintoClass(144).Ifyouhavefunctionsthataretransformingorenrichingadatastructure,useCombineFunctionsintoTransform(149).SplitPhase(154)isoftenusefulhereifthecommonfunctionscancombinetheiroutputforaconsumingphaseoflogic.
Ausefultacticforshotgunsurgeryistouseinliningrefactorings,suchasInlineFunction(115)orInlineClass(184),topulltogetherpoorlyseparatedlogic.You’llendupwithaLongMethodoraLargeClass,butcanthenuseextractionstobreakitupintomoresensiblepieces.Eventhoughweareinordinatelyfondofsmallfunctionsandclassesinourcode,wearen’tafraidofcreatingsomethinglargeasanintermediatesteptoreorganization.
FeatureEnvy
Whenwemodularizeaprogram,wearetryingtoseparatethecodeintozonestomaximizetheinteractioninsideazoneandminimizeinteractionbetweenzones.AclassiccaseofFeatureEnvyoccurswhenafunctioninonemodulespendsmoretimecommunicatingwithfunctionsordatainsideanothermodulethanitdoeswithinitsownmodule.We’velostcountofthetimeswe’veseenafunctioninvokinghalf-a-dozengettermethodsonanotherobjecttocalculatesomevalue.Fortunately,thecureforthatcaseisobvious:Thefunctionclearlywantstobewiththedata,souseMoveFunction(196)togetitthere.Sometimes,onlyapartofafunctionsuffersfromenvy,inwhichcaseuseExtractFunction(106)onthejealousbit,andMoveFunction(196)togiveitadreamhome.
Ofcoursenotallcasesarecut-and-dried.Often,afunctionusesfeaturesofseveralmodules,sowhichoneshoulditlivewith?Theheuristicweuseistodeterminewhichmodulehasmostofthedataandputthefunctionwiththatdata.ThisstepisoftenmadeeasierifyouuseExtractFunction(106)tobreakthefunctionintopiecesthatgointodifferentplaces.
Ofcourse,thereareseveralsophisticatedpatternsthatbreakthisrule.FromtheGangofFour[bib-gof],StrategyandVisitorimmediatelyleaptomind.KentBeck’sSelfDelegation[bib-beck-sbpp]isanother.Usethesetocombatthedivergentchangesmell.Thefundamentalruleofthumbistoputthingstogetherthatchangetogether.Dataandthebehaviorthatreferencesthatdatausuallychangetogether—butthereareexceptions.Whentheexceptionsoccur,wemove
thebehaviortokeepchangesinoneplace.StrategyandVisitorallowyoutochangebehavioreasilybecausetheyisolatethesmallamountofbehaviorthatneedstobeoverridden,atthecostoffurtherindirection.
DataClumps
Dataitemstendtobelikechildren:Theyenjoyhangingaroundtogether.Often,you’llseethesamethreeorfourdataitemstogetherinlotsofplaces:asfieldsinacoupleofclasses,asparametersinmanymethodsignatures.Bunchesofdatathathangaroundtogetherreallyoughttofindahometogether.Thefirststepistolookforwheretheclumpsappearasfields.UseExtractClass(180)onthefieldstoturntheclumpsintoanobject.ThenturnyourattentiontomethodsignaturesusingIntroduceParameterObject(140)orPreserveWholeObject(317)toslimthemdown.Theimmediatebenefitisthatyoucanshrinkalotofparameterlistsandsimplifymethodcalling.Don’tworryaboutdataclumpsthatuseonlysomeofthefieldsofthenewobject.Aslongasyouarereplacingtwoormorefieldswiththenewobject,you’llcomeoutahead.
Agoodtestistoconsiderdeletingoneofthedatavalues.Ifyoudidthis,wouldtheothersmakeanysense?Iftheydon’t,it’sasuresignthatyouhaveanobjectthat’sdyingtobeborn.
You’llnoticethatweadvocatecreatingaclasshere,notasimplerecordstructure.Wedothisbecauseusingaclassgivesyoutheopportunitytomakeaniceperfume.Youcannowlookforcasesoffeatureenvy,whichwillsuggestbehaviorthatcanbemovedintoyournewclasses.We’veoftenseenthisasapowerfuldynamicthatcreatesusefulclassesandcanremovealotofduplicationandacceleratefuturedevelopment,allowingthedatatobecomeproductivemembersofsociety.
PrimitiveObsession
Mostprogrammingenvironmentsarebuiltonawidelyusedsetofprimitivetypes:integers,floatingpointnumbers,andstrings.Librariesmayaddsomeadditionalsmallobjectssuchasdates.Wefindmanyprogrammersarecuriouslyreluctanttocreatetheirownfundamentaltypeswhichareusefulfortheirdomain—suchasmoney,coordinates,orranges.Wethusseecalculationsthattreatmonetaryamountsasplainnumbers,orcalculationsofphysicalquantitiesthat
ignoreunits(addinginchestomillimeters),orlotsofcodedoingif(a<upper&&a>lower).
Stringsareparticularlycommonpetridishesforthiskindofodor:Atelephonenumberismorethanjustacollectionofcharacters.Ifnothingelse,apropertypecanoftenincludeconsistentdisplaylogicforwhenitneedstobedisplayedinauserinterface.Representingsuchtypesasstringsissuchacommonstenchthatpeoplecallthem“stringlytyped”variables.
YoucanmoveoutoftheprimitivecaveintothecentrallyheatedworldofmeaningfultypesbyusingReplacePrimitivewithObject(172).Iftheprimitiveisatypecodecontrollingconditionalbehavior,useReplaceTypeCodewithSubclasses(361)followedbyReplaceConditionalwithPolymorphism(271).
GroupsofprimitivesthatcommonlyappeartogetheraredataclumpsandshouldbecivilizedwithExtractClass(180)andIntroduceParameterObject(140).
RepeatedSwitches
Talktoatrueobject-orientedevangelistandthey’llsoongetontotheevilsofswitchstatements.They’llarguethatanyswitchstatementyouseeisbeggingforReplaceConditionalwithPolymorphism(271).We’veevenheardsomepeoplearguethatallconditionallogicshouldbereplacedwithpolymorphism,tossingmostifsintothedustbinofhistory.
Eveninourmorewild-eyedyouth,wewereneverunconditionallyopposedtotheconditional.Indeed,thefirsteditionofthisbookhadasmellentitled“switchstatements.”Thesmellwastherebecauseinthelate90’swefoundpolymorphismsadlyunderappreciated,andsawbenefitingettingpeopletoswitchover.
Thesedaysthereismorepolymorphismabout,anditisn’tthesimpleredflagthatitoftenwasfifteenyearsago.Furthermore,manylanguagessupportmoresophisticatedformsofswitchstatementsthatusemorethansomeprimitivecodeastheirbase.Sowenowfocusontherepeatedswitch,wherethesameconditionalswitchinglogic(eitherinaswitch/casestatementorinacascadeofif/elsestatements)popsupindifferentplaces.Theproblemwithsuchduplicateswitchesisthat,wheneveryouaddaclause,youhavetofindalltheswitchesandupdatethem.Againstthedarkforcesofsuchrepetition,polymorphism
providesanelegantweaponforamorecivilizedcodebase.
Loops
Loopshavebeenacorepartofprogrammingsincetheearliestlanguages.Butwefeeltheyarenomorerelevanttodaythanbell-bottomsandflockwallpaper.Wedisdainedthematthetimeofthefirstedition—butJava,likemostotherlanguagesatthetime,didn’tprovideabetteralternative.Thesedays,however,first-classfunctionsarewidelysupported,sowecanuseReplaceLoopwithPipeline(230)toretirethoseanachronisms.Wefindthatpipelineoperations,suchasfilterandmap,helpusquicklyseetheelementsthatareincludedintheprocessingandwhatisdonewiththem.
LazyElement
Welikeusingprogramelementstoaddstructure—providingopportunitiesforvariation,reuse,orjusthavingmorehelpfulnames.Butsometimesthestructureisn’tneeded.Itmaybeafunctionthat’snamedthesameasitsbodycodereads,oraclassthatisessentiallyonesimplefunction.Sometimes,thisreflectsafunctionthatwasexpectedtogrowandbepopularlater,butneverrealizeditsdreams.Sometimes,it’saclassthatusedtopayitsway,buthasbeendownsizedwithrefactoring.Eitherway,suchprogramelementsneedtodiewithdignity.UsuallythismeansusingInlineFunction(115)orInlineClass(184).Withinheritance,youcanuseCollapseHierarchy(378).
SpeculativeGenerality
BrianFootesuggestedthisnameforasmelltowhichweareverysensitive.Yougetitwhenpeoplesay,“Oh,Ithinkwe’llneedtheabilitytodothiskindofthingsomeday”andthusaddallsortsofhooksandspecialcasestohandlethingsthataren’trequired.Theresultisoftenhardertounderstandandmaintain.Ifallthismachinerywerebeingused,itwouldbeworthit.Butifitisn’t,itisn’t.Themachineryjustgetsintheway,sogetridofit.
Ifyouhaveabstractclassesthataren’tdoingmuch,useCollapseHierarchy(378).UnnecessarydelegationcanberemovedwithInlineFunction(115)andInlineClass(184).Functionswithunusedparametersshouldbesubjectto
ChangeFunctionDeclaration(124)toremovethoseparameters.YoushouldalsoapplyChangeFunctionDeclaration(124)toremoveanyunneededparameters,whichoftengettossedinforfuturevariationsthatnevercometopass.
Speculativegeneralitycanbespottedwhentheonlyusersofafunctionorclassaretestcases.Ifyoufindsuchananimal,deletethetestcaseandapplyRemoveDeadCode(236).
TemporaryField
Sometimesyouseeaclassinwhichafieldissetonlyincertaincircumstances.Suchcodeisdifficulttounderstand,becauseyouexpectanobjecttoneedallofitsfields.Tryingtounderstandwhyafieldistherewhenitdoesn’tseemtobeusedcandriveyounuts.
UseExtractClass(180)tocreateahomeforthepoororphanvariables.UseMoveFunction(196)toputallthecodethatconcernsthefieldsintothisnewclass.YoumayalsobeabletoeliminateconditionalcodebyusingIntroduceSpecialCase(287)tocreateanalternativeclassforwhenthevariablesaren’tvalid.
MessageChains
Youseemessagechainswhenaclientasksoneobjectforanotherobject,whichtheclientthenasksforyetanotherobject,whichtheclientthenasksforyetanotheranotherobject,andsoon.YoumayseetheseasalonglineofgetThismethods,orasasequenceoftemps.Navigatingthiswaymeanstheclientiscoupledtothestructureofthenavigation.Anychangetotheintermediaterelationshipscausestheclienttohavetochange.
ThemovetousehereisHideDelegate(187).Youcandothisatvariouspointsinthechain.Inprinciple,youcandothistoeveryobjectinthechain,butdoingthisoftenturnseveryintermediateobjectintoamiddleman.Often,abetteralternativeistoseewhattheresultingobjectisusedfor.SeewhetheryoucanuseExtractFunction(106)totakeapieceofthecodethatusesitandthenMoveFunction(196)topushitdownthechain.Ifseveralclientsofoneoftheobjectsinthechainwanttonavigatetherestoftheway,addamethodtodothat.
Somepeopleconsideranymethodchaintobeaterriblething.Weareknownforourcalm,reasonedmoderation.Well,atleastinthiscaseweare.
MiddleMan
Oneoftheprimefeaturesofobjectsisencapsulation—hidinginternaldetailsfromtherestoftheworld.Encapsulationoftencomeswithdelegation.Youaskadirectorwhethersheisfreeforameeting;shedelegatesthemessagetoherdiaryandgivesyouananswer.Allwellandgood.Thereisnoneedtoknowwhetherthedirectorusesadiary,anelectronicgizmo,orasecretarytokeeptrackofherappointments.
However,thiscangotoofar.Youlookataclass’sinterfaceandfindhalfthemethodsaredelegatingtothisotherclass.Afterawhile,itistimetouseRemoveMiddleMan(190)andtalktotheobjectthatreallyknowswhat’sgoingon.Ifonlyafewmethodsaren’tdoingmuch,useInlineFunction(115)toinlinethemintothecaller.Ifthereisadditionalbehavior,youcanuseReplaceSuperclasswithDelegate(397)orReplaceSubclasswithDelegate(379)tofoldthemiddlemanintotherealobject.Thatallowsyoutoextendbehaviorwithoutchasingallthatdelegation.
InsiderTrading
Softwarepeoplelikestrongwallsbetweentheirmodulesandcomplainbitterlyabouthowtradingdataaroundtoomuchincreasescoupling.Tomakethingswork,sometradehastooccur,butweneedtoreduceittoaminimumandkeepitallaboveboard.
ModulesthatwhispertoeachotherbythecoffeemachineneedtobeseparatedbyusingMoveFunction(196)andMoveField(205)toreducetheneedtochat.Ifmoduleshavecommoninterests,trytocreateathirdmoduletokeepthatcommonalityinawell-regulatedvehicle,oruseHideDelegate(187)tomakeanothermoduleactasanintermediary.
Inheritancecanoftenleadtocollusion.Subclassesarealwaysgoingtoknowmoreabouttheirparentsthantheirparentswouldlikethemtoknow.Ifit’stimetoleavehome,applyReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397)..
LargeClass
Whenaclassistryingtodotoomuch,itoftenshowsupastoomanyfields.Whenaclasshastoomanyfields,duplicatedcodecannotbefarbehind.
YoucanExtractClass(180)tobundleanumberofthevariables.Choosevariablestogotogetherinthecomponentthatmakessenseforeach.Forexample,“depositAmount”and“depositCurrency”arelikelytobelongtogetherinacomponent.Moregenerally,commonprefixesorsuffixesforsomesubsetofthevariablesinaclasssuggesttheopportunityforacomponent.Ifthecomponentmakessensewithinheritance,you’llfindExtractSuperclass(373)orReplaceTypeCodewithSubclasses(361)(whichessentiallyisextractingasubclass)areofteneasier.
Sometimesaclassdoesnotuseallofitsfieldsallofthetime.Ifso,youmaybeabletotheseextractionsmanytimes.
Aswithaclasswithtoomanyinstancevariables,aclasswithtoomuchcodeisaprimebreedinggroundforduplicatedcode,chaos,anddeath.Thesimplestsolution(havewementionedthatwelikesimplesolutions?)istoeliminateredundancyintheclassitself.Ifyouhavefivehundred-linemethodswithlotsofcodeincommon,youmaybeabletoturnthemintofiveten-linemethodswithanothertentwo-linemethodsextractedfromtheoriginal.
Theclientsofsuchaclassareoftenthebestclueforsplittinguptheclass.Lookatwhetherclientsuseasubsetofthefeaturesoftheclass.Eachsubsetisapossibleseparateclass.Onceyou’veidentifiedausefulsubset,useExtractClass(180),ExtractSuperclass(373),orReplaceTypeCodewithSubclasses(361)tobreakitout.
AlternativeClasseswithDifferentInterfaces
Oneofthegreatbenefitsofusingclassesisthesupportforsubstitution,allowingoneclasstoswapinforanotherintimesofneed.Butthisonlyworksiftheirinterfacesarethesame.UseChangeFunctionDeclaration(124)tomakefunctionsmatchup.Often,thisdoesn’tgofarenough;keepusingMoveFunction(196)tomovebehaviorintoclassesuntiltheprotocolsmatch.Ifthisleadstoduplication,youmaybeabletouseExtractSuperclass(373)toatone.
DataClass
Theseareclassesthathavefields,gettingandsettingmethodsforthefields,andnothingelse.Suchclassesaredumbdataholdersandareoftenbeingmanipulatedinfartoomuchdetailbyotherclasses.Insomestages,theseclassesmayhavepublicfields.Ifso,youshouldimmediatelyapplyEncapsulateRecord(160)beforeanyonenotices.UseRemoveSettingMethod(329)onanyfieldthatshouldnotbechanged.
Lookforwherethesegettingandsettingmethodsareusedbyotherclasses.TrytouseMoveFunction(196)tomovebehaviorintothedataclass.Ifyoucan’tmoveawholefunction,useExtractFunction(106)tocreateafunctionthatcanbemoved.
Dataclassesareoftenasignofbehaviorinthewrongplace,whichmeanscanmakebigprogressbymovingitfromtheclientintothedataclassitself.Butthereareexceptions,andoneofthebestexceptionsisarecordthat’sbeingusedasaresultrecordfromadistinctfunctioninvocation.Agoodexampleofthisistheintermediatedatastructureafteryou’veappliedSplitPhase(154).Akeycharacteristicofsucharesultrecordisthatit’simmutable(atleastinpractice).Immutablefieldsdon’tneedtobeencapsulatedandinformationderivedfromimmutabledatacanberepresentedasfieldsratherthangettingmethods.
RefusedBequest
Subclassesgettoinheritthemethodsanddataoftheirparents.Butwhatiftheydon’twantorneedwhattheyaregiven?Theyaregivenallthesegreatgiftsandpickjustafewtoplaywith.
Thetraditionalstoryisthatthismeansthehierarchyiswrong.YouneedtocreateanewsiblingclassandusePushDownMethod(357)andPushDownField(359)topushalltheunusedcodetothesibling.Thatwaytheparentholdsonlywhatiscommon.Often,you’llhearadvicethatallsuperclassesshouldbeabstract.
You’llguessfromoursnideuseof“traditional”thatwearen’tgoingtoadvisethis—atleastnotallthetime.Wedosubclassingtoreuseabitofbehaviorallthetime,andwefinditaperfectlygoodwayofdoingbusiness.Thereisasmell—
wecan’tdenyit—butusuallyitisn’tastrongsmell.So,wesaythatiftherefusedbequestiscausingconfusionandproblems,followthetraditionaladvice.However,don’tfeelyouhavetodoitallthetime.Ninetimesoutoftenthissmellistoofainttobeworthcleaning.
Thesmellofrefusedbequestismuchstrongerifthesubclassisreusingbehaviorbutdoesnotwanttosupporttheinterfaceofthesuperclass.Wedon’tmindrefusingimplementations—butrefusinginterfacegetsusonourhighhorses.Inthiscase,however,don’tfiddlewiththehierarchy;youwanttogutitbyapplyingReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397).
Comments
Don’tworry,wearen’tsayingthatpeopleshouldn’twritecomments.Inourolfac-toryanalogy,commentsaren’tabadsmell;indeedtheyareasweetsmell.Thereasonwementioncommentshereisthatcommentsareoftenusedasadeodorant.It’ssurprisinghowoftenyoulookatthicklycommentedcodeandnoticethatthecommentsaretherebecausethecodeisbad.
Commentsleadustobadcodethathasalltherottenwhiffswe’vediscussedintherestofthischapter.Ourfirstactionistoremovethebadsmellsbyrefactoring.Whenwe’refinished,weoftenfindthatthecommentsaresuperfluous.
Ifyouneedacommenttoexplainwhatablockofcodedoes,tryExtractFunction(106).Ifthemethodisalreadyextractedbutyoustillneedacommenttoexplainwhatitdoes,useChangeFunctionDeclaration(124)torenameit.Ifyouneedtostatesomerulesabouttherequiredstateofthesystem,useIntroduceAssertion(299).
Whenyoufeeltheneedtowriteacomment,firsttrytorefactorthecodesothatanycommentbecomessuperfluous.
Agoodtimetouseacommentiswhenyoudon’tknowwhattodo.Inadditiontodescribingwhatisgoingon,commentscanindicateareasinwhichyouaren’tsure.Acommentcanalsoexplainwhyyoudidsomething.Thiskindofinformationhelpsfuturemodifiers,especiallyforgetfulones.
Chapter4BuildingTestsRefactoringisavaluabletool,butitcan’tcomealone.Todorefactoringproperly,Ineedasolidsuiteofteststospotmyinevitablemistakes.Evenwithautomatedrefactoringtools,manyofmyrefactoringswillstillneedcheckingviaatestsuite.
Idon’tfindthistobeadisadvantage.Evenwithoutrefactoring,writinggoodtestsincreasesmyeffectivenessasaprogrammer.Thiswasasurpriseformeandiscounter-intuitiveformostprogrammers—soit’sworthexplainingwhy.
TheValueofSelf-TestingCode
Ifyoulookathowmostprogrammersspendtheirtime,you’llfindthatwritingcodeisactuallyquiteasmallfraction.Sometimeisspentfiguringoutwhatoughttobegoingon,sometimeisspentdesigning,butmosttimeisspentdebugging.I’msureeveryreadercanrememberlonghoursofdebugging—often,wellintothenight.Everyprogrammercantellastoryofabugthattookawholeday(ormore)tofind.Fixingthebugisusuallyprettyquick,butfindingitisanightmare.Andthen,whenyoudofixabug,there’salwaysachancethatanotheronewillappearandthatyoumightnotevennoticeittillmuchlater.Andyou’llspendagesfindingthatbug.
Theeventthatstartedmeontheroadtoself-testingcodewasatalkatOOPSLAin1992.Someone(Ithinkitwas“Bedarra”DaveThomas)saidoffhandedly,“Classesshouldcontaintheirowntests.”SoIdecidedtoincorporatetestsintothecodebasetogetherwiththeproductioncode.AsIwasalsodoingiterativedevelopment,ItriedaddingtestsasIcompletedeachiteration.TheprojectonwhichIwasworkingatthattimewasquitesmall,soweputoutiterationseveryweekorso.Runningthetestsbecamefairlystraightforward—butalthoughitwaseasy,itwasstillprettyboring.ThiswasbecauseeverytestproducedoutputtotheconsolethatIhadtocheck.NowI’maprettylazypersonandampreparedtoworkquitehardinordertoavoidwork.Irealizedthat,insteadoflookingatthescreentoseeifitprintedoutsomeinformationfromthemodel,Icouldgetthecomputertomakethattest.AllIhadtodowasputtheoutputIexpectedin
thetestcodeanddoacomparison.NowIcouldrunthetestsandtheywouldjustprint“OK”tothescreenifallwaswell.Thesoftwarewasnowself-testing.
Makesurealltestsarefullyautomaticandthattheychecktheirownresults.
Nowitwaseasytoruntests—aseasyascompiling.SoIstartedtoruntestseverytimeIcompiled.Soon,Ibegantonoticemyproductivityhadshotupward.IrealizedthatIwasn’tspendingsomuchtimedebugging.IfIaddedabugthatwascaughtbyaprevioustest,itwouldshowupassoonasIranthattest.Thetesthadworkedbefore,soIwouldknowthatthebugwasintheworkIhaddonesinceIlasttested.AndIranthetestsfrequently—whichmeansonlyafewminuteshadelapsed.IthusknewthatthesourceofthebugwasthecodeIhadjustwritten.Asitwasasmallamountofcodethatwasstillfreshinmymind,thebugwaseasytofind.Bugsthatwouldhaveotherwisetakenanhourormoretofindnowtookacoupleofminutesatmost.Notonlywasmysoftwareself-testing,butbyrunningthetestsfrequentlyIhadapowerfulbugdetector.
AsInoticedthis,Ibecamemoreaggressiveaboutdoingthetests.Insteadofwaitingfortheendofanincrement,Iwouldaddthetestsimmediatelyafterwritingabitoffunction.EverydayIwouldaddacoupleofnewfeaturesandtheteststotestthem.Ihardlyeverspentmorethanafewminuteshuntingforaregressionbug.
Asuiteoftestsisapowerfulbugdetectorthatdecapitatesthetimeittakestofindbugs.
Toolsforwritingandorganizingthesetestshavedevelopedagreatdealsincemyexperiments.WhileflyingfromSwitzerlandtoAtlantaforOOPSLA1997,KentBeckpairedwithErichGammatoporthisunittestingframeworkfromSmalltalktoJava.Theresultingframework,calledJUnit,hasbeenenormouslyinfluentialforprogramtesting,inspiringahugevarietyofsimilartools[bib-xunit]inlotsofdifferentlanguages.
Admittedly,itisnotsoeasytopersuadeotherstofollowthisroute.Writingthetestsmeansalotofextracodetowrite.Unlessyouhaveactuallyexperiencedhowitspeedsprogramming,self-testingdoesnotseemtomakesense.Thisisnothelpedbythefactthatmanypeoplehaveneverlearnedtowritetestsoreventothinkabouttests.Whentestsaremanual,theyaregut-wrenchinglyboring.Butwhentheyareautomatic,testscanactuallybequitefuntowrite.
Infact,oneofthemostusefultimestowritetestsisbeforeIstartprogramming.WhenIneedtoaddafeature,Ibeginbywritingthetest.Thisisn’tasbackwardasitsounds.Bywritingthetest,I’maskingmyselfwhatneedstobedonetoaddthefunction.Writingthetestalsoconcentratesmeontheinterfaceratherthantheimplementation(alwaysagoodthing).ItalsomeansIhaveaclearpointatwhichI’mdonecoding—whenthetestworks.
KentBeckbakedthishabitofwritingthetestfirstintoatechniquecalledTest-DrivenDevelopment(TDD)[bib-tdd].TheTest-DrivenDevelopmentapproachtoprogrammingreliesonshortcyclesofwritinga(failing)test,writingthecodetomakethattestwork,andrefactoringtoensuretheresultisascleanaspossible.Thistest-code-refactorcycleshouldoccurmanytimesperhour,andcanbeaveryproductiveandcalmingwaytowritecode.I’mnotgoingtodiscussitfurtherhere,butIdouseandwarmlyrecommendit.
That’senoughofthepolemic.AlthoughIbelieveeveryonewouldbenefitbywritingself-testingcode,itisnotthepointofthisbook.Thisbookisaboutrefactoring.Refactoringrequirestests.Ifyouwanttorefactor,youhavetowritetests.ThischaptergivesyouastartindoingthisforJavaScript.Thisisnotatestingbook,soI’mnotgoingtogointomuchdetail.I’vefound,however,thatwithtestingaremarkablysmallamountofworkcanhavesurprisinglybigbenefits.
Aswitheverythingelseinthisbook,Idescribethetestingapproachusingexamples.WhenIdevelopcode,IwritethetestsasIgo.Butsometimes,Ineedtorefactorsomecodewithouttests—thenIhavetomakethecodeself-testingbeforeIbegin.
SampleCodetoTest
Here’ssomecodetolookatandtest.Thecodesupportsasimpleapplicationthatallowsausertoexamineandmanipulateaproductionplan.The(crude)UIlookslikethis:
Theproductionplanhasademandandpriceforeachprovince.Eachprovincehasproducers,eachofwhichcanproduceacertainnumberofunitsataparticularprice.TheUIalsoshowshowmuchrevenueeachproducerwouldearniftheysellalltheirproduction.Atthebottom,thescreenshowstheshortfallinproduction(thedemandminusthetotalproduction)andtheprofitforthisplan.TheUIallowstheusertomanipulatethedemand,price,andtheindividualproducer’sproductionandcoststoseetheeffectontheproductionshortfallandprofits.Wheneverauserchangesanynumberinthedisplay,alltheothersupdateimmediately.
I’mshowingauserinterfacehere,soyoucansensehowthesoftwareisused,butI’monlygoingtoconcentrateonthebusinesslogicpartofthesoftware—thatis,theclassesthatcalculatetheprofitandtheshortfall,notthecodethatgeneratestheHTMLandhooksupthefieldchangestotheunderlyingbusinesslogic.Thischapterisjustanintroductiontotheworldofself-testingcode,soitmakessenseformetostartwiththeeasiestcase—whichiscodethatdoesn’tinvolveuserinterface,persistence,orexternalserviceinteraction.Suchseparation,however,isagoodideainanycase:Oncethiskindofbusinesslogicgetsatallcomplicated,IwillseparateitfromtheUImechanicssoIcanmoreeasilyreasonaboutitandtestit.
Thisbusinesslogiccodeinvolvestwoclasses:onethatrepresentsasingleproducer,andtheotherthatrepresentsawholeprovince.Theprovince’sconstructortakesaJavaScriptobject—onewecouldimaginebeingsuppliedbyaJSONdocument.
classProvince…
constructor(doc){
this._name=doc.name;
this._producers=[];
this._totalProduction=0;
this._demand=doc.demand;
this._price=doc.price;
doc.producers.forEach(d=>this.addProducer(newProducer(this,d)));
}
addProducer(arg){
this._producers.push(arg);
this._totalProduction+=arg.production;
}
toplevel…
functionsampleProvinceData(){
return{
name:"Asia",
producers:[
{name:"Byzantium",cost:10,production:9},
{name:"Attalia",cost:12,production:10},
{name:"Sinope",cost:10,production:6},
],
demand:30,
price:20
};
}
Ithasaccessorsforthevariousdatavalues:
classProvince…
getname(){returnthis._name;}
getproducers(){returnthis._producers.slice();}
gettotalProduction(){returnthis._totalProduction;}
settotalProduction(arg){this._totalProduction=arg;}
getdemand(){returnthis._demand;}
setdemand(arg){this._demand=parseInt(arg);}
getprice(){returnthis._price;}
setprice(arg){this._price=parseInt(arg);}
ThesetterswillbecalledwithstringsfromtheUIthatcontainthenumbers,soIneedtoparsethenumberstousethemreliablyincalculations.
Theproducerclassismostlyasimpledataholder:
classProducer…
constructor(aProvince,data){
this._province=aProvince;
this._cost=data.cost;
this._name=data.name;
this._production=data.production||0;
}
getname(){returnthis._name;}
getcost(){returnthis._cost;}
setcost(arg){this._cost=parseInt(arg);}
getproduction(){returnthis._production;}
setproduction(amountStr){
constamount=parseInt(amountStr);
constnewProduction=Number.isNaN(amount)?0:amount;
this._province.totalProduction+=newProduction-this._production;
this._production=newProduction;
}
Thewaythatsetproductionupdatesthederiveddataintheprovinceisugly,andwheneverIseethatIwanttorefactortoremoveit.ButIhavetowritetestsbeforethatIcanrefactorit.
Thecalculationfortheshortfallissimple.
classProvince…
getshortfall(){
returnthis._demand-this.totalProduction;
}
Thatfortheprofitisabitmoreinvolved
classProvince…
getprofit(){
returnthis.demandValue-this.demandCost;
}
getdemandCost(){
letremainingDemand=this.demand;
letresult=0;
this.producers
.sort((a,b)=>a.cost-b.cost)
.forEach(p=>{
constcontribution=Math.min(remainingDemand,p.production);
remainingDemand-=contribution;
result+=contribution*p.cost;
});
returnresult;
}
getdemandValue(){
returnthis.satisfiedDemand*this.price;
}
getsatisfiedDemand(){
returnMath.min(this._demand,this.totalProduction);
}
AFirstTest
Totestthiscode,I’llneedsomesortoftestingframework.Therearemanyoutthere,evenjustforJavaScript.TheoneI’lluseisMocha[bib-mocha],whichisreasonablycommonandwell-regarded.Iwon’tgointoafullexplanationofhowtousetheframework,justshowsomeexampletestswithit.Youshouldbeabletoadapt,easilyenough,adifferentframeworktobuildsimilartests.
Hereisasimpletestfortheshortfallcalculation:
describe('province',function(){
it('shortfall',function(){
constasia=newProvince(sampleProvinceData());
assert.equal(asia.shortfall,5);
});
});
TheMochaframeworkdividesupthetestcodeintoblocks,eachgroupingtogetherasuiteoftests.Eachtestappearsinanitblock.Forthissimplecase,thetesthastwosteps.Thefirststepsetsupsomefixture—dataandobjectsthatareneededforthetest:inthiscase,aloadedprovinceobject.Thesecondlineverifiessomecharacteristicofthatfixture—inthiscase,thattheshortfallistheamountthatshouldbeexpectedgiventheinitialdata.
Differentdevelopersusethedescriptivestringsinthedescribeanditblocksdifferently.Somewouldwriteasentencethatexplainswhatthetestistesting,butothersprefertoleavethemempty,arguingthatthedescriptivesentenceisjustduplicatingthecodeinthesamewayacommentdoes.IliketoputinjustenoughtoidentifywhichtestiswhichwhenIgetfailures.
IfIrunthistestinaNodeJSconsole,theoutputlookslikethis:
!
1passing(61ms)
Notethesimplicityofthefeedback—justasummaryofhowmanytestsarerunandhowmanyhavepassed.
Alwaysmakesureatestwillfailwhenitshould.
WhenIwriteatestagainstexistingcodelikethis,it’snicetoseethatalliswell—butI’mnaturallyskeptical.Particularly,onceIhavealotoftestsrunning,I’malwaysnervousthatatestisn’treallyexercisingthecodethewayIthinkitis,andthuswon’tcatchabugwhenIneeditto.SoIliketoseeeverytestfailatleastoncewhenIwriteit.Myfavoritewayofdoingthatistotemporarilyinjectafaultintothecode,forexample:
classProvince…
getshortfall(){
returnthis._demand-this.totalProduction*2;
}
Here’swhattheconsolenowlookslike:
!
0passing(72ms)
1failing
1)provinceshortfall:
AssertionError:expected-20toequal5
atContext.<anonymous>(src/tester.js:10:12)
Theframeworkindicateswhichtestfailedandgivessomeinformationaboutthenatureofthefailure—inthiscase,whatvaluewasexpectedandwhatvalue
actuallyturnedup.Ithereforenoticeatoncethatsomethingfailed—andIcanimmediatelyseewhichtestsfailed,givingmeaclueastowhatwentwrong(and,inthiscase,confirmingthefailurewaswhereIinjectedit).
Runtestsfrequently.Runthoseexercisingthecodeyou’reworkingonatleasteveryfewminutes;runalltestsatleastdaily.
Inarealsystem,Imighthavethousandsoftests.Agoodtestframeworkallowsmetorunthemeasilyandtoquicklyseeifanyhavefailed.Thissimplefeedbackisessentialtoself-testingcode.WhenIwork,I’llberunningtestsveryfrequently—checkingprogresswithnewcodeorcheckingformistakeswithrefactoring.
TheMochaframeworkcanusedifferentlibraries,whichitcallsassertionlibraries,toverifythefixtureforatest.BeingJavaScript,thereareaquadzillionofthemoutthere,someofwhichmaystillbecurrentwhenyou’rereadingthis.TheoneI’musingatthemomentisChai[bib-chai].Chaiallowsmetowritemyvalidationseitherusingan“assert”style:
describe('province',function(){
it('shortfall',function(){
constasia=newProvince(sampleProvinceData());
assert.equal(asia.shortfall,5);
});
});
oran“expect”style:
describe('province',function(){
it('shortfall',function(){
constasia=newProvince(sampleProvinceData());
expect(asia.shortfall).equal(5);
});
});
Iusuallyprefertheassertstyle,butatthemomentImostlyusetheexpectstylewhileworkinginJavaScript.
Differentenvironmentsprovidedifferentwaystoruntests.WhenI’mprogramminginJava,IuseanIDEthatgivesmeagraphicaltestrunner.Itsprogressbarisgreenaslongasallthetestspass,andturnsredshouldanyofthemfail.Mycolleaguesoftenusethephrases“greenbar”and“redbar”to
describethestateoftests.Imightsay,“Neverrefactoronaredbar,”meaningyoushouldn’tberefactoringifyourtestsuitehasafailingtest.Or,Imightsay,“Reverttogreen”tosayyoushouldundorecentchangesandgobacktothelaststatewhereyouhadall-passingtestsuite(usuallybygoingbacktoarecentversion-controlcheckpoint).
Graphicaltestrunnersarenice,butnotessential.IusuallyhavemytestssettorunfromasinglekeyinEmacs,andobservethetextfeedbackinmycompilationwindow.ThekeypointisthatIcanquicklyseeifmytestsareallOK.
AddAnotherTest
NowI’llcontinueaddingmoretests.ThestyleIfollowistolookatallthethingstheclassshoulddoandtesteachoneofthemforanyconditionsthatmightcausetheclasstofail.Thisisnotthesameastestingeverypublicmethod,whichiswhatsomeprogrammersadvocate.Testingshouldberisk-driven;remember,I’mtryingtofindbugs,noworinthefuture.ThereforeIdon’ttestaccessorsthatjustreadandwriteafield:TheyaresosimplethatI’mnotlikelytofindabugthere.
Thisisimportantbecausetryingtowritetoomanytestsusuallyleadstonotwritingenough.IgetmanybenefitsfromtestingevenifIdoonlyalittletesting.MyfocusistotesttheareasthatI’mmostworriedaboutgoingwrong.ThatwayIgetthemostbenefitformytestingeffort.
Itisbettertowriteandrunincompleteteststhannottoruncompletetests.
SoI’llstartbyhittingtheothermainoutputforthiscode—theprofitcalculation.Again,I’lljustdoabasictestforprofitonmyinitialfixture.
describe('province',function(){
it('shortfall',function(){
constasia=newProvince(sampleProvinceData());
expect(asia.shortfall).equal(5);
});
it('profit',function(){
constasia=newProvince(sampleProvinceData());
expect(asia.profit).equal(230);
});
});
Thatshowsthefinalresult,butthewayIgotitwasbyfirstsettingtheexpected
valuetoaplaceholder,thenreplacingitwithwhatevertheprogramproduced(230).Icouldhavecalculateditbyhandmyself,butsincethecodeissupposedtobeworkingcorrectly,I’lljusttrustitfornow.OnceIhavethatnewtestworkingcorrectly,Ibreakitbyalteringtheprofitcalculationwithaspurious*2.Isatisfymyselfthatthetestfailsasitshould,thenrevertmyinjectedfault.Thispattern—writewithaplaceholderfortheexpectedvalue,replacetheplaceholderwiththecode’sactualvalue,injectafault,revertthefault—isacommononeIusewhenaddingteststoexistingcode.
Thereissomeduplicationbetweenthesetests—bothofthemsetupthefixturewiththesamefirstline.JustasI’msuspiciousofduplicatedcodeinregularcode,I’msuspiciousofitintestcode,sowilllooktoremoveitbyfactoringtoacommonplace.Oneoptionistoraisetheconstanttotheouterscope.
describe('province',function(){
constasia=newProvince(sampleProvinceData());//DON'TDOTHIS
it('shortfall',function(){
expect(asia.shortfall).equal(5);
});
it('profit',function(){
expect(asia.profit).equal(230);
});
});
Butasthecommentindicates,Ineverdothis.Itwillworkforthemoment,butitintroducesapetridishthat’sprimedforoneofthenastiestbugsintesting—asharedfixturewhichcausesteststointeract.TheconstkeywordinJavaScriptonlymeansthereferencetoasiaisconstant,notthecontentofthatobject.Shouldafuturetestchangethatcommonobject,I’llendupwithintermittenttestfailuresduetotestsinteractingthroughthesharedfixture,yieldingdifferentresultsdependingonwhatorderthetestsarerunin.That’sanon-determinismintheteststhatcanleadtolonganddifficultdebuggingatbest,andacollapseofconfidenceinthetestsatworst.Instead,Iprefertodothis:
describe('province',function(){
letasia;
beforeEach(function(){
asia=newProvince(sampleProvinceData());
});
it('shortfall',function(){
expect(asia.shortfall).equal(5);
});
it('profit',function(){
expect(asia.profit).equal(230);
});
});
ThebeforeEachclauseisrunbeforeeachtestruns,clearingoutasiaandsettingittoafreshvalueeachtime.ThiswayIbuildafreshfixturebeforeeachtestisrun,whichkeepsthetestsisolatedandpreventsthenon-determinismthatcausessomuchtrouble.
WhenIgivethisadvice,somepeopleareconcernedthatbuildingafreshfixtureeverytimewillslowdownthetests.Mostofthetime,itwon’tbenoticeable.Ifitisaproblem,I’dconsiderasharedfixture,butthenIwillneedtobereallycarefulthatnotesteverchangesit.IcanalsouseasharedfixtureifI’msureitistrulyimmutable.Butmyreflexistouseafreshfixturebecausethedebuggingcostofmakingamistakewithasharedfixturehasbitmetooofteninthepast.
GivenIrunthesetupcodeinbeforeEachwitheverytest,whynotleavethesetupcodeinsidetheindividualitblocks?Ilikemyteststoalloperateonacommonbitoffixture,soIcanbecomefamiliarwiththatstandardfixtureandseethevariouscharacteristicstotestonit.ThepresenceofthebeforeEachblocksignalstothereaderthatI’musingastandardfixture.Youcanthenlookatallthetestswithinthescopeofthatdescribeblockandknowtheyalltakethesamebasedataasastartingpoint.
ModifyingtheFixture
Sofar,thetestsI’vewrittenshowhowIprobethepropertiesofthefixtureonceI’veloadedit.Butinuse,thatfixturewillberegularlyupdatedbytheuserastheychangevalues.
Mostoftheupdatesaresimplesetters,andIdon’tusuallybothertotestthoseasthere’slittlechancetheywillbethesourceofabug.ButthereissomecomplicatedbehavioraroundProducer’sproductionsetter,soIthinkthat’sworthatest.
describe(’province’…
it('changeproduction',function(){
asia.producers[0].production=20;
expect(asia.shortfall).equal(-6);
expect(asia.profit).equal(292);
});
Thisisacommonpattern.Itaketheinitialstandardfixturethat’ssetupbythebeforeEachblock,Iexercisethatfixtureforthetest,thenIverifythefixturehasdonewhatIthinkitshouldhavedone.Ifyoureadmuchabouttesting,you’llhearthesephasesdescribedvariouslyassetup-exercise-verify,given-when-then,orarrange-act-assert.Sometimesyou’llseeallthestepspresentwithinthetestitself,inothercasesthecommonearlyphasescanbepushedoutintostandardsetuproutinessuchasbeforeEach.
(Thereisanimplicitfourthphasethat’susuallynotmentioned:teardown.Teardownremovesthefixturebetweentestssothatdifferenttestsdon’tinteractwitheachother.BydoingallmysetupinbeforeEach,Iallowthetestframeworktoimplicitlyteardownmyfixturebetweentests,soIcantaketheteardownphaseforgranted.Mostwritersontestsglossoverteardown—reasonablyso,sincemostofthetimeweignoreit.Butoccasionally,itcanbeimportanttohaveanexplicitteardownoperation,particularlyifwehaveafixturethatwehavetosharebetweentestsbecauseit’sslowtocreate.)
Inthistest,I’mverifyingtwodifferentcharacteristicsinasingleitclause.Asageneralrule,it’swisetohaveonlyasingleverifystatementineachitclause.Thisisbecausethetestwillfailonthefirstverificationfailure—whichcanoftenhideusefulinformationwhenyou’refiguringoutwhyatestisbroken.Inthiscase,IfeelthetwoarecloselyenoughconnectedthatI’mhappytohavetheminthesametest.ShouldIwishtoseparatethemintoseparateitclauses,Icandothatlater.
ProbingtheBoundaries
Sofarmytestshavefocusedonregularusage,oftenreferredtoas“happypath”conditionswhereeverythingisgoingOKandthingsareusedasexpected.Butit’salsogoodtothrowtestsattheboundariesoftheseconditions—toseewhathappenswhenthingsmightgowrong.
WheneverIhaveacollectionofsomething,suchasproducersinthisexample,Iliketoseewhathappenswhenit’sempty.
describe('noproducers',function(){
letnoProducers;
beforeEach(function(){
constdata={
name:"Noproudcers",
producers:[],
demand:30,
price:20
};
noProducers=newProvince(data);
});
it('shortfall',function(){
expect(noProducers.shortfall).equal(30);
});
it('profit',function(){
expect(noProducers.profit).equal(0);
});
Withnumbers,zerosaregoodthingstoprobe.
describe(’province’…
it('zerodemand',function(){
asia.demand=0;
expect(asia.shortfall).equal(-25);
expect(asia.profit).equal(0);
});
Asarenegatives
describe(’province’…
it('negativedemand',function(){
asia.demand=-1;
expect(asia.shortfall).equal(-26);
expect(asia.profit).equal(-10);
});
Atthispoint,Imaystarttowonderifanegativedemandresultinginanegativeprofitreallymakesanysenseforthedomain.Shouldn’ttheminimumdemandbezero?Inwhichcase,perhaps,thesettershouldreactdifferentlytoanegativeargument—raisinganerrororsettingthevaluetozeroanyway.Thesearegoodquestionstoask,andwritingtestslikethishelpsmethinkabouthowthecodeoughttoreacttoboundarycases.
Thinkoftheboundaryconditionsunderwhichthingsmightgowrongandconcentrateyourteststhere.
ThesetterstakeastringfromthefieldsintheUI,whichareconstrainedtoonlyacceptnumbers—buttheycanstillbeblank,soIshouldhaveteststhatensurethecoderespondstotheblanksthewayIwantitto.
describe(’province’…
it('emptystringdemand',function(){
asia.demand="";
expect(asia.shortfall).NaN;
expect(asia.profit).NaN;
});
NoticehowI’mplayingthepartofanenemytomycode.I’mactivelythinkingabouthowIcanbreakit.Ifindthatstateofmindtobebothproductiveandfun.Itindulgesthemean-spiritedpartofmypsyche.
Thisoneisinteresting:
describe('stringforproducers',function(){
it('',function(){
constdata={
name:"Stringproducers",
producers:"",
demand:30,
price:20
};
constprov=newProvince(data);
expect(prov.shortfall).equal(0);
});
Thisdoesn’tproduceasimplefailurereportingthattheshortfallisn’t0.Here’stheconsoleoutput:
!
9passing(74ms)
1failing
1)stringforproducers:
TypeError:doc.producers.forEachisnotafunction
atnewProvince(src/main.js:22:19)
atContext.<anonymous>(src/tester.js:86:18)
Mochatreatsthisasafailure—butmanytestingframeworksdistinguishbetweenthissituation,whichtheycallanerror,andaregularfailure.Afailureindicatesa
verifystepwheretheactualvalueisoutsidetheboundsexpectedbytheverifystatement.Butthiserrorisadifferentanimal—it’sanexceptionraisedduringanearlierphase(inthiscase,thesetup).Thislookslikeanexceptionthattheauthorsofthecodehadn’tanticipated,sowegetanerrorsadlyfamiliartoJavaScriptprogrammers(“…isnotafunction”).
Howshouldthecoderespondtosuchacase?Oneapproachistoaddsomehandlingthatwouldgiveabettererrorresponse—eitherraisingamoremeaningfulerrormessage,orjustsettingproducerstoanemptyarray(withperhapsalogmessage).Buttheremayalsobevalidreasonstoleaveitasitis.Perhapstheinputobjectisproducedbyatrustedsource—suchasanotherpartofthesamecodebase.Puttinginlotsofvalidationchecksbetweenmodulesinthesamecodebasecanresultinduplicatechecksthatcausemoretroublethantheyareworth,especiallyiftheyduplicatevalidationdoneelsewhere.Butifthatinputobjectiscominginfromanexternalsource,suchasaJSON-encodedrequest,thenvalidationchecksareneeded,andshouldbetested.Ineithercase,writingtestslikethisraisesthesekindsofquestions.
IfI’mwritingtestslikethisbeforerefactoring,Iwouldprobablydiscardthistest.Refactoringshouldpreserveobservablebehavior;anerrorlikethisisoutsidetheboundsofobservable,soIneednotbeconcernedifmyrefactoringchangesthecode’sresponsetothiscondition.
Ifthiserrorcouldleadtobaddatarunningaroundtheprogram,causingafailurethatwillbehardtodebug,ImightuseIntroduceAssertion(299)tofailfast.Idon’taddteststocatchsuchassertionfailures,astheyarethemselvesaformoftest.
Don’tletthefearthattestingcan’tcatchallbugsstopyoufromwritingteststhatcatchmostbugs.
Whendoyoustop?I’msureyouhaveheardmanytimesthatyoucannotprovethataprogramhasnobugsbytesting.That’strue,butitdoesnotaffecttheabilityoftestingtospeedupprogramming.I’veseenvariousproposedrulestoensureyouhavetestedeverycombinationofeverything.It’sworthtakingalookatthese—butdon’tletthemgettoyou.Thereisalawofdiminishingreturnsintesting,andthereisthedangerthatbytryingtowritetoomanytestsyoubecomediscouragedandendupnotwritingany.Youshouldconcentrateonwheretheriskis.Lookatthecodeandseewhereitbecomescomplex.Lookatafunction
andconsiderthelikelyareasoferror.Yourtestswillnotfindeverybug,butasyourefactor,youwillunderstandtheprogrambetterandthusfindmorebugs.AlthoughIalwaysstartrefactoringwithatestsuite,IinvariablyaddtoitasIgoalong.
MuchMoreThanThis
That’sasfarasI’mgoingtogowiththischapter—afterall,thisisabookonrefactoring,notontesting.Buttestingisanimportanttopic,bothbecauseit’sanecessaryfoundationforrefactoringandbecauseit’savaluabletoolinitsownright.WhileI’vebeenhappytoseethegrowthofrefactoringasaprogrammingpracticesinceIwrotethisbook,I’vebeenevenhappiertoseethechangeinattitudestotesting.Previouslyseenastheresponsibilityofaseparate(andinferior)group,testingisnowincreasinglyafirst-classconcernofanydecentsoftwaredeveloper.Architecturesoftenare,rightly,judgedontheirtestability.
ThekindsoftestsI’veshownhereareunittests,designedtooperateonasmallareaofthecodeandrunfast.Theyarethebackboneofself-testingcode;mosttestsinsuchasystemareunittests.Thereareotherkindsofteststoo,focusingonintegrationbetweencomponents,exercisingmultiplelevelsofthesoftwaretogether,lookingforperformanceissues,etc.(Andevenmorevariedthanthetypesoftestsaretheargumentspeoplegetintoabouthowtoclassifytests.)
Likemostaspectsofprogramming,testingisaniterativeactivity.Unlessyouareeitherveryskilledorverylucky,youwon’tgetyourtestsrightthefirsttime.IfindI’mconstantlyworkingonthetestsuite—justasmuchasIworkonthemaincode.Naturally,thismeansaddingnewtestsasIaddnewfeatures,butitalsoinvolveslookingattheexistingtests.Aretheyclearenough?DoIneedtorefactorthemsoIcanmoreeasilyunderstandwhattheyaredoing?HaveIgottherighttests?Animportanthabittogetintoistorespondtoabugbyfirstwritingatestthatclearlyrevealsthebug.OnlyafterIhavethetestdoIfixthebug.Byhavingthetest,Iknowthebugwillstaydead.Ialsothinkaboutthatbuganditstest:Doesitgivemecluestoothergapsinthetestsuite?
Whenyougetabugreport,startbywritingaunittestthatexposesthebug.
Acommonquestionis,“Howmuchtestingisenough?”There’snogoodmeasurementforthis.Somepeopleadvocateusingtestcoverage[bib-test-coverage]asameasure,buttestcoverageanalysisisonlygoodforidentifying
untestedareasofthecode,notforassessingthequalityofatestsuite.
Thebestmeasureforagoodenoughtestsuiteissubjective:Howconfidentareyouthatifsomeoneintroducesadefectintothecode,sometestwillfail?Thisisn’tsomethingthatcanbeobjectivelyanalyzed,anditdoesn’taccountforfalseconfidence,buttheaimofself-testingcodeistogetthatconfidence.IfIcanrefactormycodeandbeprettysurethatI’venotintroducedabugbecausemytestscomebackgreen—thenIcanbehappythatIhavegoodenoughtests.
Itispossibletowritetoomanytests.OnesignofthatiswhenIspendmoretimechangingtheteststhanthecodeundertest—andIfeelthetestsareslowingmedown.Butwhileover-testingdoeshappen,it’svanishinglyrarecomparedtounder-testing.
Chapter5IntroducingtheCatalogTherestofthisbookisacatalogofrefactorings.ThiscatalogstartedfrommypersonalnotesthatImadetoremindmyselfhowtodorefactoringsinasafeandefficientway.Sincethen,I’verefinedthecatalog,andthere’smoreofitthatcomesfromdeliberateexplorationofsomerefactoringmoves.It’sstillsomethingIusewhenIdoarefactoringIhaven’tdoneinawhile.
FormatoftheRefactorings
AsIdescribetherefactoringsinthisandotherchapters,Iuseastandardformat.Eachrefactoringhasfiveparts,asfollows:
Ibeginwithaname.Thenameisimportanttobuildingavocabularyofrefactorings.ThisisthenameIuseelsewhereinthebook.Refactoringsoftengobydifferentnamesnow,soIalsolistanyaliasesthatseemtobecommon.
Ifollowthenamewithashortsketchoftherefactoring.Thishelpsyoufindarefactoringmorequickly.
Themotivationdescribeswhytherefactoringshouldbedoneanddescribescircumstancesinwhichitshouldn’tbedone.
Themechanicsareaconcise,step-by-stepdescriptionofhowtocarryouttherefactoring.
Theexamplesshowaverysimpleuseoftherefactoringtoillustratehowitworks.
Thesketchshowsacodeexampleofthetransformationoftherefactoring.It’snotmeanttoexplainwhattherefactoringis,letalonehowtodoit,butitshouldremindyouwhattherefactoringisifyou’vecomeacrossitbefore.Ifnot,you’llprobablyneedtoworkthroughtheexampletogetabetteridea.Ialsoincludeasmallgraphic;again,Idon’tintendittobeexplanatory—it’smoreofagraphicmemory-jogger.
ThemechanicscomefrommyownnotestorememberhowtodotherefactoringwhenIhaven’tdoneitforawhile.Assuch,theyaresomewhatterse,usuallywithoutexplanationsofwhythestepsaredonethatway.Igiveamoreexpansiveexplanationintheexample.Thisway,themechanicsareshortnotesyoucanrefertoeasilywhenyouknowtherefactoringbutneedtolookupthesteps(atleastthisishowIusethem).You’llprobablyneedtoreadtheexampleswhenyoufirstdotherefactoring.
I’vewrittenthemechanicsinsuchawaythateachstepofeachrefactoringisassmallaspossible.Iemphasizethesafewayofdoingtherefactoring—whichistotakeverysmallstepsandtestaftereveryone.Atwork,Iusuallytakelargerstepsthansomeofthebabystepsdescribed,butifIrunintoabug,Ibackoutthelaststepandtakethesmallersteps.Thestepsincludeanumberofreferencestospecialcases.Thestepsthusalsofunctionasachecklist;Ioftenforgetthesethingsmyself.
AlthoughI(withfewexceptions)onlylistonesetofmechanics,theyaren’ttheonlywaytocarryouttherefactoring.Iselectedthemechanicsinthebookbecausetheyworkprettywellmostofthetime.It’slikelyyou’llvarythemasyougetmorepracticeinrefactoring,andthat’sfine.Justrememberthatthekeyistotakesmallsteps—andthetrickierthesituation,thesmallerthesteps.
Theexamplesareofthelaughablysimpletextbookkind.Myaimwiththeexamplesistohelpexplainthebasicrefactoringwithminimaldistractions,soIhopeyou’llforgivethesimplicity.(Theyarecertainlynotexamplesofgoodbusinessmodeling.)I’msureyou’llbeabletoapplythemtoyourrathermorecomplexsituations.Someverysimplerefactoringsdon’thaveexamplesbecauseIdidn’tthinkanexamplewouldaddmuch.
Inparticular,rememberthattheexamplesareincludedonlytoillustratetheonerefactoringunderdiscussion.Inmostcases,therearestillproblemswiththecodeattheend—butfixingtheseproblemsrequiresotherrefactorings.Inafewcasesinwhichrefactoringsoftengotogether,Icarryexamplesfromonerefactoringtoanother.Inmostcases,Ileavethecodeasitisafterthesinglerefactoring.Idothistomakeeachrefactoringself-contained,becausetheprimaryroleofthecatalogistobeareference.
Iuseboldfacecodetohighlightchangedcodewhereitmaybedifficulttospotamongcodethathasnotbeenchanged.Idonotuseboldfacetypeforall
changedcode,becausetoomuchdefeatsthepurpose.
TheChoiceofRefactorings
Thisisbynomeansacompletecatalogofrefactorings.Itis,Ihope,acollectionofthosemostusefultohavethemwrittendown.By“mostuseful”Imeanthosethatarebothcommonlyusedandworthwhiletonameanddescribe.Ifindsomethingworthwhiletodescribeforacombinationofreasons:Somehaveinterestingmechanicswhichhelpgeneralrefactoringskills,somehaveastrongeffectonimprovingthedesignofcode.
SomerefactoringsaremissingbecausetheyaresosmallandstraightforwardthatIdon’tfeeltheyareworthwritingup.AnexampleinthefirsteditionwasSlideStatements(221)—whichIusefrequentlybutdidn’trecognizeassomethingIshouldincludeinthecatalog(obviously,Ichangedmymindforthisedition).Thesemaywellgetaddedtothebookovertime,dependingonhowmuchenergyIdevotetonewrefactoringsinthefuture.
Anothercategoryisrefactoringsthatlogicallyexist,buteitheraren’tusedmuchbymeorshowasimplesimilaritytootherrefactorings.Everyrefactoringinthisbookhasalogicalinverserefactoring,butIdidn’twriteallofthemupbecauseIdon’tfindmanyinversesinteresting.EncapsulateVariable(132)isacommonandpowerfulrefactoringbutitsinverseissomethingIhardlyeverdo(anditiseasytoperformanyway)soIdidn’tthinkweneedacatalogentryforit.
Chapter6AFirstSetofRefactoringsI’mstartingthecatalogwithasetofrefactoringsthatIconsiderthemostusefultolearnfirst.
ProbablythemostcommonrefactoringIdoisextractingcodeintoafunction(ExtractFunction(106))oravariable(ExtractVariable(119)).Sincerefactoringisallaboutchange,it’snosurprisethatIalsofrequentlyusetheinversesofthosetwo(InlineFunction(115)andInlineVariable(123)).
Extractionisallaboutgivingnames,andIoftenneedtochangethenamesasIlearn.ChangeFunctionDeclaration(124)changesnamesoffunctions;Ialsousethatrefactoringtoaddorremoveafunction’sarguments.Forvariables,IuseRenameVariable(137),whichreliesonEncapsulateVariable(132).Whenchangingfunctionarguments,IoftenfinditusefultocombineacommonclumpofargumentsintoasingleobjectwithIntroduceParameterObject(140).
Formingandnamingfunctionsareessentiallow-levelrefactorings—but,oncecreated,it’snecessarytogroupfunctionsintohigher-levelmodules.IuseCombineFunctionsintoClass(144)togroupfunctions,togetherwiththedatatheyoperateon,intoaclass.AnotherpathItakeistocombinethemintoatransform(CombineFunctionsintoTransform(149)),whichisparticularlyhandywithread-onlydata.Atastepfurtherinscale,IcanoftenformthesemodulesintodistinctprocessingphasesusingSplitPhase(154).
ExtractFunction
formerly:ExtractMethod
inverseof:InlineFunction(115)
Motivation
ExtractFunction(106)isoneofthemostcommonrefactoringsIdo.(Here,Iusetheterm“function”butthesameistrueforamethodinanobject-orientedlanguage,oranykindofprocedureorsubroutine.)Ilookatafragmentofcode,understandwhatitisdoing,thenextractitintoitsownfunctionnamedafteritspurpose.
Duringmycareer,I’veheardmanyargumentsaboutwhentoenclosecodeinitsownfunction.Someoftheseguidelineswerebasedonlength:Functionsshouldbenolargerthanfitonascreen.Somewerebasedonreuse:Anycodeusedmorethanonceshouldbeputinitsownfunction,butcodeonlyusedonceshouldbeleftinline.Theargumentthatmakesmostsensetome,however,istheseparationbetweenintentionandimplementation.Ifyouhavetospendeffortlookingatafragmentofcodeandfiguringoutwhatit’sdoing,thenyoushouldextractitintoafunctionandnamethefunctionafterthe“what.”Then,whenyoureaditagain,thepurposeofthefunctionleapsrightoutatyou,andmostofthetimeyouwon’tneedtocareabouthowthefunctionfulfillsitspurpose(whichisthebodyofthefunction).
OnceIacceptedthisprinciple,Idevelopedahabitofwritingverysmallfunctions—typically,onlyafewlineslong.Tome,anyfunctionwithmorethanhalf-a-dozenlinesofcodestartstosmell,andit’snotunusualformetohavefunctionsthatareasinglelineofcode.Thefactthatsizeisn’timportantwasbroughthometomebyanexamplethatKentBeckshowedmefromtheoriginalSmalltalksystem.Smalltalkinthosedaysranonblack-and-whitesystems.Ifyouwantedtohighlightsometextorgraphics,youwouldreversethevideo.Smalltalk’sgraphicsclasshadamethodforthiscalledhighlight,whoseimplementationwasjustacalltothemethodreverse.Thenameofthemethodwaslongerthanitsimplementation—butthatdidn’tmatterbecausetherewasabigdistancebetweentheintentionofthecodeanditsimplementation.
Somepeopleareconcernedaboutshortfunctionsbecausetheyworryabouttheperformancecostofafunctioncall.WhenIwasyoung,thatwasoccasionallyafactor,butthat’sveryrarenow.Optimizingcompilersoftenworkbetterwithshorterfunctionswhichcanbecachedmoreeasily.Asalways,followthegeneralguidelinesonperformanceoptimization.
Smallfunctionslikethisonlyworkifthenamesaregood,soyouneedtopaygoodattentiontonaming.Thistakespractice—butonceyougetgoodatit,thisapproachcanmakecoderemarkablyself-documenting.
Often,Iseefragmentsofcodeinalargerfunctionthatstartwithacommenttosaywhattheydo.ThecommentisoftenagoodhintforthenameofthefunctionwhenIextractthatfragment.
Mechanics
Createanewfunction,andnameitaftertheintentofthefunction(nameitbywhatitdoes,notbyhowitdoesit).
IfthecodeIwanttoextractisverysimple,suchasasinglefunctioncall,Istillextractitifthenameofthenewfunctionwillrevealtheintentofthecodeinabetterway.IfIcan’tcomeupwithamoremeaningfulname,that’sasignthatIshouldn’textractthecode.However,Idon’thavetocomeupwiththebestnamerightaway;sometimesagoodnameonlyappearsasIworkwiththeextraction.It’sOKtoextractafunction,trytoworkwithit,realizeitisn’thelping,andtheninlineitbackagain.AslongasI’velearnedsomething,mytimewasn’twasted.
Ifthelanguagesupportsnestedfunctions,nesttheextractedfunctioninsidethesourcefunction.Thatwillreducetheamountofout-of-scopevariablestodealwithafterthenextcoupleofsteps.IcanalwaysuseMoveFunction(196)later.
Copytheextractedcodefromthesourcefunctionintothenewtargetfunction.
Scantheextractedcodeforreferencestoanyvariablesthatarelocalinscopetothesourcefunctionandwillnotbeinscopefortheextractedfunction.Passthemasparameters.
IfIextractintoanestedfunctionofthesourcefunction,Idon’trunintotheseproblems.
Usually,thesearelocalvariablesandparameterstothefunction.Themostgeneralapproachistopassallsuchparametersinasarguments.Thereareusuallynodifficultiesforvariablesthatareusedbutnotassignedto.
Ifavariableisonlyusedinsidetheextractedcodebutisdeclaredoutside,movethedeclarationintotheextractedcode.
Anyvariablesthatareassignedtoneedmorecareiftheyarepassedbyvalue.Ifthere’sonlyoneofthem,Itrytotreattheextractedcodeasaqueryandassigntheresulttothevariableconcerned.
Sometimes,Ifindthattoomanylocalvariablesarebeingassignedbytheextractedcode.It’sbettertoabandontheextractionatthispoint.Whenthishappens,IconsiderotherrefactoringssuchasSplitVariable(240)orReplaceTempwithQuery(176)tosimplifyvariableusageandrevisittheextractionlater.
Compileafterallvariablesaredealtwith
Onceallthevariablesaredealtwith,itcanbeusefultocompileifthelanguageenvironmentdoescompile-timechecks.Often,thiswillhelpfindanyvariablesthathaven’tbeendealtwithproperly.
Replacetheextractedcodeinthesourcefunctionwithacalltothetargetfunction.
Test.
Lookforothercodethat’sthesameorsimilartothecodejustextracted,andconsiderusingReplaceInlineCodewithFunctionCall(220)tocallthenewfunction.
Somerefactoringtoolssupportthisdirectly.Otherwise,itcanbeworthdoingsomequicksearchestoseeifduplicatecodeexistselsewhere.
Example:NoVariablesOutofScope
Inthesimplestcase,ExtractFunctionistriviallyeasy.
functionprintOwing(invoice){
letoutstanding=0;
console.log("***********************");
console.log("****CustomerOwes****");
console.log("***********************");
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
//recordduedate
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
//printdetails
console.log(`name:${invoice.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${invoice.dueDate.toLocaleDateString()}`);
}
YoumaybewonderingwhattheClock.todayisabout.ItisaClockWrapper(https://martinfowler.com/bliki/ClockWrapper.html)—anobjectthatwrapscallstothesystemclock.IavoidputtingdirectcallstothingslikeDate.now()inmycode,becauseitleadstonon-deterministictestsandmakesitdifficulttoreproduceerrorconditionswhendiagnosingfailures.
It’seasytoextractthecodethatprintsthebanner.Ijustcut,paste,andputinacall:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
//recordduedate
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
//printdetails
console.log(`name:${invoice.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${invoice.dueDate.toLocaleDateString()}`);
}
functionprintBanner(){
console.log("***********************");
console.log("****CustomerOwes****");
console.log("***********************");
}
Similarly,Icantaketheprintingofdetailsandextractthattoo:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
//recordduedate
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
printDetails();
functionprintDetails(){
console.log(`name:${invoice.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${invoice.dueDate.toLocaleDateString()}`);
}
ThismakesExtractFunctionseemlikeatriviallyeasyrefactoring.Butinmanysituations,itturnsouttoberathermoretricky.
Inthecaseabove,IdefinedprintDetailssoitwasnestedinsideprintOwing.ThatwayitwasabletoaccessallthevariablesdefinedinprintOwing.Butthat’snotanoptiontomeifI’mprogramminginalanguagethatdoesn’tallownestedfunctions.ThenI’mfaced,essentially,withtheproblemofextractingthefunctiontothetoplevel,whichmeansIhavetopayattentiontoanyvariablesthatexistonlyinthescopeofthesourcefunction.Thesearetheargumentstotheoriginalfunctionandthetemporaryvariablesdefinedinthefunction.
Example:UsingLocalVariables
Theeasiestcasewithlocalvariablesiswhentheyareusedbutnotreassigned.Inthiscase,Icanjustpasstheminasparameters.SoifIhavethefollowingfunction:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
//recordduedate
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
//printdetails
console.log(`name:${invoice.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${invoice.dueDate.toLocaleDateString()}`);
}
Icanextracttheprintingofdetailspassingtwoparameters:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
//recordduedate
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
printDetails(invoice,outstanding);
}
functionprintDetails(invoice,outstanding){
console.log(`name:${invoice.customer}`);
console.log(`amount:${outstanding}`);
console.log(`due:${invoice.dueDate.toLocaleDateString()}`);
}
Thesameistrueifthelocalvariableisastructure(suchasanarray,record,orobject)andImodifythatstructure.So,Icansimilarlyextractthesettingoftheduedate:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
functionrecordDueDate(invoice){
consttoday=Clock.today;
invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);
}
Example:ReassigningaLocalVariable
It’stheassignmenttolocalvariablesthatbecomescomplicated.Inthiscase,we’reonlytalkingabouttemps.IfIseeanassignmenttoaparameter,IimmediatelyuseSplitVariable(240),whichturnsitintoatemp.
Fortempsthatareassignedto,therearetwocases.Thesimplercaseiswherethevariableisatemporaryvariableusedonlywithintheextractedcode.Whenthathappens,thevariablejustexistswithintheextractedcode.Sometimes,particularlywhenvariablesareinitializedatsomedistancebeforetheyareused,it’shandytouseSlideStatements(221)togetallthevariablemanipulationtogether.
Themoreawkwardcaseiswherethevariableisusedoutsidetheextractedfunction.Inthatcase,Ineedtoreturnthenewvalue.Icanillustratethiswiththefollowingfamiliar-lookingfunction:
functionprintOwing(invoice){
letoutstanding=0;
printBanner();
//calculateoutstanding
for(constoofinvoice.orders){
outstanding+=o.amount;
}
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
I’veshownthepreviousrefactoringsallinonestep,sincetheywerestraightforward,butthistimeI’lltakeitonestepatatimefromthemechanics.
First,I’llslidethedeclarationnexttoitsuse.
functionprintOwing(invoice){
printBanner();
//calculateoutstanding
letoutstanding=0;
for(constoofinvoice.orders){
outstanding+=o.amount;
}
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
IthencopythecodeIwanttoextractintoatargetfunction.
functionprintOwing(invoice){
printBanner();
//calculateoutstanding
letoutstanding=0;
for(constoofinvoice.orders){
outstanding+=o.amount;
}
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
functioncalculateOutstanding(invoice){
letoutstanding=0;
for(constoofinvoice.orders){
outstanding+=o.amount;
}
returnoutstanding;
}
SinceImovedthedeclarationofoutstandingintotheextractedcode,Idon’tneedtopassitinasaparameter.Theoutstandingvariableistheonlyonereassignedintheextractedcode,soIcanreturnit.
MyJavaScriptenvironmentdoesn’tyieldanyvaluebycompiling—indeedlessthanI’mgettingfromthesyntaxanalysisinmyeditor—sothere’snosteptodohere.Mynextthingtodoistoreplacetheoriginalcodewithacalltothenewfunction.SinceI’mreturningthevalue,Ineedtostoreitintheoriginalvariable.
functionprintOwing(invoice){
printBanner();
letoutstanding=calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
functioncalculateOutstanding(invoice){
letoutstanding=0;
for(constoofinvoice.orders){
outstanding+=o.amount;
}
returnoutstanding;
}
BeforeIconsidermyselfdone,Irenamethereturnvaluetofollowmyusualcodingstyle.
functionprintOwing(invoice){
printBanner();
constoutstanding=calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice,outstanding);
}
functioncalculateOutstanding(invoice){
letresult=0;
for(constoofinvoice.orders){
result+=o.amount;
}
returnresult;
}
Ialsotaketheopportunitytochangetheoriginaloutstandingintoaconst.
Atthispointyoumaybewondering,“Whathappensifmorethanonevariableneedstobereturned?”
Here,Ihaveseveraloptions.UsuallyIprefertopickdifferentcodetoextract.Ilikeafunctiontoreturnonevalue,soIwouldtrytoarrangeformultiplefunctionsforthedifferentvalues.IfIreallyneedtoextractwithmultiplevalues,Icanformarecordandreturnthat—butusuallyIfinditbettertoreworkthetemporaryvariablesinstead.HereIlikeusingReplaceTempwithQuery(176)andSplitVariable(240).
ThisraisesaninterestingquestionwhenI’mextractingfunctionsthatIexpecttothenmovetoanothercontext,suchastoplevel.Iprefersmallsteps,somyinstinctistoextractintoanestedfunctionfirst,thenmovethatnestedfunctiontoitsnewcontext.ButthetrickypartofthisisdealingwithvariablesandIdon’texposethatdifficultyuntilIdothemove.ThisarguesthateventhoughIcanextractintoanestedfunction,itmakessensetoextracttoatleastthesiblinglevelofthesourcefunctionfirst,soIcanimmediatelytelliftheextractedcodemakessense.
InlineFunction
formerly:InlineMethod
inverseof:ExtractFunction(106)
Motivation
Oneofthethemesofthisbookisusingshortfunctionsnamedtoshowtheirintent,becausethesefunctionsleadtoclearerandeasiertoreadcode.Butsometimes,Idocomeacrossafunctioninwhichthebodyisasclearasthename.Or,Irefactorthebodyofthecodeintosomethingthatisjustasclearasthename.Whenthishappens,Igetridofthefunction.Indirectioncanbehelpful,butneedlessindirectionisirritating.
IalsouseInlineFunctioniswhenIhaveagroupoffunctionsthatseembadlyfactored.Icaninlinethemallintoonebigfunctionandthenre-extractthefunctionsthewayIprefer.
IcommonlyuseInlineFunctionwhenIseecodethat’susingtoomuch
indirection—whenitseemsthateveryfunctiondoessimpledelegationtoanotherfunction,andIgetlostinallthedelegation.Someofthisindirectionmaybeworthwhile,butnotallofit.Byinlining,Icanflushouttheusefulonesandeliminatetherest.
Mechanics
Checkthatthisisn’tapolymorphicmethod.
Ifthisisamethodinaclass,andhassubclassesthatoverrideit,thenIcan’tinlineit.
Findallthecallersofthefunction.
Replaceeachcallwiththefunction’sbody.
Testaftereachreplacement.
Theentireinliningdoesn’thavetobedoneallatonce.Ifsomepartsoftheinlinearetricky,theycanbedonegraduallyasopportunitypermits.
Removethefunctiondefinition.
Writtenthisway,InlineFunctionissimple.Ingeneral,itisn’t.Icouldwritepagesonhowtohandlerecursion,multiplereturnpoints,inliningamethodintoanotherobjectwhenyoudon’thaveaccessors,andthelike.ThereasonIdon’tisthatifyouencounterthesecomplexities,youshouldn’tdothisrefactoring.
Example
Inthesimplestcase,thisrefactoringissoeasyit’strivial.Istartwith
functionrating(aDriver){
returnmoreThanFiveLateDeliveries(aDriver)?2:1;
}
functionmoreThanFiveLateDeliveries(aDriver){
returnaDriver.numberOfLateDeliveries>5;
}
Icanjusttakethereturnexpressionofthecalledfunctionandpasteitintothe
callertoreplacethecall.
functionrating(aDriver){
returnaDriver.numberOfLateDeliveries>5?2:1;
}
Butitcanbealittlemoreinvolvedthanthat,requiringmetodomoreworktofitthecodeintoitsnewhome.ConsiderthecasewhereIstartwiththisslightvariationontheearlierinitialcode.
functionrating(aDriver){
returnmoreThanFiveLateDeliveries(aDriver)?2:1;
}
functionmoreThanFiveLateDeliveries(dvr){
returndvr.numberOfLateDeliveries>5;
}
Almostthesame,butnowthedeclaredargumentonmoreThanFiveLateDeliveriesisdifferenttothenameofthepassed-inargument.SoIhavetofitthecodealittlewhenIdotheinline.
functionrating(aDriver){
returnaDriver.numberOfLateDeliveries>5?2:1;
}
Itcanbeevenmoreinvolvedthanthis.Considerthiscode:
functionreportLines(aCustomer){
constlines=[];
gatherCustomerData(lines,aCustomer);
returnlines;
}
functiongatherCustomerData(out,aCustomer){
out.push(["name",aCustomer.name]);
out.push(["location",aCustomer.location]);
}
InlininggatherCustomerDataintoreportLinesisn’tasimplecutandpaste.It’snottoocomplicated,andmosttimesIwouldstilldothisinonego,withabitoffitting.Buttobecautious,itmaymakesensetomoveonelineatatime.SoI’dstartwithusingMoveStatementstoCallers(215)onthefirstline(I’ddoitthesimplewaywithacut,paste,andfit).
functionreportLines(aCustomer){
constlines=[];
lines.push(["name",aCustomer.name]);
gatherCustomerData(lines,aCustomer);
returnlines;
}
functiongatherCustomerData(out,aCustomer){
out.push(["name",aCustomer.name]);
out.push(["location",aCustomer.location]);
}
IthencontinuewiththeotherlinesuntilI’mdone.
functionreportLines(aCustomer){
constlines=[];
lines.push(["name",aCustomer.name]);
lines.push(["location",aCustomer.location]);
returnlines;
}
Thepointhereistoalwaysbereadytotakesmallersteps.Mostofthetime,withthesmallfunctionsInormallywrite,IcandoInlineFunctioninonego,evenifthereisabitofrefittingtodo.ButifIrunintocomplications,Igoonelineatatime.Evenwithoneline,thingscangetabitawkward;then,I’llusethemoreelaboratemechanicsforMoveStatementstoCallers(215)tobreakthingsdownevenmore.Andif,feelingconfident,Idosomethingthequickwayandthetestsbreak,Iprefertorevertbacktomylastgreencodeandrepeattherefactoringwithsmallerstepsandatouchofchagrin.
ExtractVariable
formerly:IntroduceExplainingVariable
inverseof:InlineVariable(123)
Motivation
Expressionscanbecomeverycomplexandhardtoread.Insuchsituations,localvariablesmayhelpbreaktheexpressiondownintosomethingmoremanageable.Inparticular,theygivemeanabilitytonameapartofamorecomplexpieceoflogic.Thisallowsmetobetterunderstandthepurposeofwhat’shappening.
Suchvariablesarealsohandyfordebugging,sincetheyprovideaneasyhookforadebuggerorprintstatementtocapture.
IfI’mconsideringExtractVariable,itmeansIwanttoaddanametoanexpressioninmycode.OnceI’vedecidedIwanttodothat,Ialsothinkaboutthecontextofthatname.Ifit’sonlymeaningfulwithinthefunctionI’mworkingon,thenExtractVariableisagoodchoice—butifitmakessenseinabroadercontext,I’llconsidermakingthenameavailableinthatbroadercontext,usuallyasafunction.Ifthenameisavailablemorewidely,thenothercodecanusethatexpressionwithouthavingtorepeattheexpression,leadingtolessduplicationandabetterstatementofmyintent.
Thedownsideofpromotingthenametoabroadercontextisextraeffort.Ifit’ssignificantlymoreeffort,I’mlikelytoleaveittilllaterwhenIcanuseReplaceTempwithQuery(176).Butifit’seasy,Iliketodoitnowsothenameisimmediatelyavailableinthecode.Asagoodexampleofthis,ifI’mworkinginaclass,thenExtractFunction(106)isveryeasytodo.
Mechanics
Ensurethattheexpressionyouwanttoextractdoesnothaveside-effects.
Declareanimmutablevariable.Setittoacopyoftheexpressionyouwanttoname.
Replacetheoriginalexpressionwiththenewvariable.
Test.
Iftheexpressionappearsmorethanonce,replaceeachoccurrencewiththevariable,testingaftereachreplacement.
Example
Istartwithasimplecalculation
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
returnorder.quantity*order.itemPrice-
Math.max(0,order.quantity-500)*order.itemPrice*0.05+
Math.min(order.quantity*order.itemPrice*0.1,100);
}
Simpleasitmaybe,Icanmakeitstilleasiertofollow.First,Irecognizethatthebasepriceisthemultipleofthequantityandtheitemprice.
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
returnorder.quantity*order.itemPrice-
Math.max(0,order.quantity-500)*order.itemPrice*0.05+
Math.min(order.quantity*order.itemPrice*0.1,100);
}
Oncethatunderstandingisinmyhead,Iputitinthecodebycreatingand
namingavariableforit.
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
constbasePrice=order.quantity*order.itemPrice;
returnorder.quantity*order.itemPrice-
Math.max(0,order.quantity-500)*order.itemPrice*0.05+
Math.min(order.quantity*order.itemPrice*0.1,100);
}
Ofcourse,justdeclaringandinitializingavariabledoesn’tdoanything;Ialsohavetouseit,soIreplacetheexpressionthatIusedasitssource.
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
constbasePrice=order.quantity*order.itemPrice;
returnbasePrice-
Math.max(0,order.quantity-500)*order.itemPrice*0.05+
Math.min(order.quantity*order.itemPrice*0.1,100);
}
Thatsameexpressionisusedlateron,soIcanreplaceitwiththevariabletheretoo.
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
constbasePrice=order.quantity*order.itemPrice;
returnbasePrice-
Math.max(0,order.quantity-500)*order.itemPrice*0.05+
Math.min(basePrice*0.1,100);
}
Thenextlineisthequantitydiscount,soIcanextractthattoo.
functionprice(order){
//priceisbaseprice-quantitydiscount+shipping
constbasePrice=order.quantity*order.itemPrice;
constquantityDiscount=Math.max(0,order.quantity-500)*order.itemPrice*0.05;
returnbasePrice-
quantityDiscount+
Math.min(basePrice*0.1,100);
}
Finally,Ifinishwiththeshipping.AsIdothat,Icanremovethecomment,too,becauseitnolongersaysanythingthecodedoesn’tsay.
functionprice(order){
constbasePrice=order.quantity*order.itemPrice;
constquantityDiscount=Math.max(0,order.quantity-500)*order.itemPrice*0.05;
constshipping=Math.min(basePrice*0.1,100);
returnbasePrice-quantityDiscount+shipping;
}
Example:withaClass
Here’sthesamecode,butthistimeinthecontextofaclass
classOrder{
constructor(aRecord){
this._data=aRecord;
}
getquantity(){returnthis._data.quantity;}
getitemPrice(){returnthis._data.itemPrice;}
getprice(){
returnthis.quantity*this.itemPrice-
Math.max(0,this.quantity-500)*this.itemPrice*0.05+
Math.min(this.quantity*this.itemPrice*0.1,100);
}
}
Inthiscase,Iwanttoextractthesamenames,butIrealizethatthenamesapplytotheOrderasawhole,notjustthecalculationoftheprice.Sincetheyapplytothewholeorder,I’minclinedtoextractthenamesasmethodsratherthanvariables.
classOrder{
constructor(aRecord){
this._data=aRecord;
}
getquantity(){returnthis._data.quantity;}
getitemPrice(){returnthis._data.itemPrice;}
getprice(){
returnthis.basePrice-this.quantityDiscount+this.shipping;
}
getbasePrice(){returnthis.quantity*this.itemPrice;}
getquantityDiscount(){returnMath.max(0,this.quantity-500)*this.itemPrice*0.05;}
getshipping(){returnMath.min(this.basePrice*0.1,100);}
}
Thisisoneofthegreatbenefitsofobjects—theygiveyouareasonableamount
ofcontextforlogictoshareotherbitsoflogicanddata.Forsomethingassimpleasthis,itdoesn’tmattersomuch,butwithalargerclassitbecomesveryusefultocalloutcommonhunksofbehaviorastheirownabstractionswiththeirownnamestorefertothemwheneverI’mworkingwiththeobject.
InlineVariable
formerly:InlineTemp
inverseof:ExtractVariable(119)
Motivation
Variablesprovidenamesforexpressionswithinafunction,andassuchtheyareusuallyaGoodThing.Butsometimes,thenamedoesn’treallycommunicatemorethantheexpressionitself.Atothertimes,youmayfindthatavariablegetsinthewayofrefactoringtheneighboringcode.Inthesecases,itcanbeusefultoinlinethevariable.
Mechanics
Checkthattheright-handsideoftheassignmentisfreeofsideeffects.
Ifthevariableisn’talreadydeclaredimmutable,dosoandtest.
Thischecksthatit’sonlyassignedtoonce.
Findthefirstreferencetothevariableandreplaceitwiththeright-handsideoftheassignment.
Test.
Repeatreplacingreferencestothevariableuntilyou’vereplacedallofthem.
Removethedeclarationandassignmentofthevariable.
Test.
ChangeFunctionDeclaration
aka:RenameFunction
formerly:RenameMethod
formerly:AddParameter
formerly:RemoveParameter
aka:ChangeSignature
Motivation
Functionsrepresenttheprimarywaywebreakaprogramdownintoparts.Functiondeclarationsrepresenthowthesepartsfittogether—effectively,theyrepresentthejointsinoursoftwaresystems.And,aswithanyconstruction,muchdependsonthosejoints.Goodjointsallowmetoaddnewpartsthesystemeasily,butbadonesareaconstantsourceofdifficulty,makingithardertofigureoutwhatthesoftwaredoesandhowtomodifyitasmyneedschange.Fortunately,software,beingsoft,allowsmetochangethesejoints,providingIdoitcarefully.
Themostimportantelementofsuchajointisthenameofthefunction.AgoodnameallowsmetounderstandwhatthefunctiondoeswhenIseeitcalled,withoutseeingthecodethatdefinesitsimplementation.However,comingupwithgoodnamesishard,andIrarelygetmynamesrightthefirsttime.WhenIfindanamethat’sconfusedme,I’mtemptedtoleaveit—afterall,it’sonlyaname.ThisistheworkoftheevildemonObfuscatis;forthesakeofmyprogram’ssoulImustneverlistentohim.IfIseeafunctionwiththewrongname,itisimperativethatIchangeitassoonasIunderstandwhatabetternamecouldbe.Thatway,thenexttimeI’mlookingatthiscode,Idon’thavetofigureoutagainwhat’sgoingon.(Often,agoodwaytoimproveanameistowriteacommenttodescribethefunction’spurpose,thenturnthatcommentintoaname.)
Similarlogicappliestoafunction’sparameters.Theparametersofafunctiondictatehowafunctionfitsinwiththerestofitsworld.ParameterssetthecontextinwhichIcanuseafunction.IfIhaveafunctiontoformataperson’stelephonenumber,andthatfunctiontakesapersonasitsargument,thenIcan’tuseittoformatacompany’stelephonenumber.IfIreplacethepersonparameterwiththetelephonenumberitself,thentheformattingcodeismorewidelyuseful.
Apartfromincreasingafunction’srangeofapplicability,Icanalsoremovesomecoupling,changingwhatmodulesneedtoconnecttoothers.Telephoneformattinglogicmaysitinamodulethathasnoknowledgeaboutpeople.ReducinghowmuchmodulesneedtoknowabouteachotherhelpsreducehowmuchIneedtoputintomybrainwhenIchangesomething—andmybrainisn’tasbigasitusedtobe(thatdoesn’tsayanythingaboutthesizeofitscontainer,
though).
Choosingtherightparametersisn’tsomethingthatadherestosimplerules.Imayhaveasimplefunctionfordeterminingifapaymentisoverdue,bylookingatifit’solderthan30days.Shouldtheparametertothisfunctionbethepaymentobject,ortheduedateofthepayment?Usingthepaymentcouplesthefunctiontotheinterfaceofthepaymentobject.ButifIusethepayment,Icaneasilyaccessotherpropertiesofthepayment,shouldthelogicevolve,withouthavingtochangeeverybitofcodethatcallsthisfunction—essentially,increasingtheencapsulationofthefunction.
Theonlyrightanswertothispuzzleisthatthereisnorightanswer,especiallyovertime.SoIfindit’sessentialtobefamiliarwithChangeFunctionDeclarationsothecodecanevolvewithmyunderstandingofwhatthebestjointsinthecodeneedtobe.
Usually,IonlyusethemainnameofarefactoringwhenIrefertoitfromelsewhereinthisbook.However,sincerenamingissuchasignificantusecaseforChangeFunctionDeclaration,ifI’mjustrenamingsomething,I’llrefertothisrefactoringasChangeFunctionDeclaration(124)tomakeitclearerwhatI’mdoing.WhetherI’mmerelyrenamingormanipulatingtheparameters,Iusethesamemechanics.
Mechanics
Inmostoftherefactoringsinthisbook,Ipresentonlyasinglesetofmechanics.Thisisn’tbecausethereisonlyonesetthatwilldothejobbutbecause,usually,onesetofmechanicswillworkreasonablywellformostcases.ChangeFunctionDeclaration,however,isanexception.Thesimplemechanicsareofteneffective,butthereareplentyofcaseswhenamoregradualmigrationmakesmoresense.So,withthisrefactoring,IlookatthechangeandaskmyselfifIthinkIcanchangethedeclarationandallitscallerseasilyinonego.Ifso,Ifollowthesimplemechanics.Themigration-stylemechanicsallowmetochangethecallersmoregradually—whichisimportantifIhavelotsofthem,theyareawkwardtogetto,thefunctionisapolymorphicmethod,orIhaveamorecomplicatedchangetothedeclaration.
SimpleMechanics
Ifyou’reremovingaparameter,ensureitisn’treferencedinthebodyofthefunction.
Changethemethoddeclarationtothedesireddeclaration.
Findallreferencestotheoldmethoddeclaration,updatethemtothenewone.
Test.
It’softenbesttoseparatechanges,soifyouwanttobothchangethenameandaddaparameter,dotheseasseparatesteps.(Inanycase,ifyourunintotrouble,revertandusethemigrationmechanicsinstead.)
MigrationMechanics
Ifnecessary,refactorthebodyofthefunctiontomakeiteasytodothefollowingextractionstep.
UseExtractFunction(106)onthefunctionbodytocreatethenewfunction.
Ifthenewfunctionwillhavethesamenameastheoldone,givethenewfunctionatemporarynamethat’seasytosearchfor.
Iftheextractedfunctionneedsadditionalparameters,usethesimplemechanicstoaddthem.
Test.
ApplyInlineFunction(115)totheoldfunction.
Ifyouusedatemporaryname,useChangeFunctionDeclaration(124)againtorestoreittotheoriginalname.
Test.
Ifyou’rechangingamethodonaclasswithpolymorphism,you’llneedtoaddindirectionforeachbinding.Ifthemethodispolymorphicwithinasingleclasshierarchy,youonlyneedtheforwardingmethodonthesuperclass.Ifthepolymorphismhasnosuperclasslink,thenyou’llneedforwardingmethodsoneachimplementationclass.
IfyouarerefactoringapublishedAPI,youcanpausetherefactoringonceyou’vecreatedthenewfunction.Duringthispause,deprecatetheoriginalfunctionandwaitforclientstochangetothenewfunction.Theoriginalfunctiondeclarationcanberemovedwhen(andif)you’reconfidentalltheclientsoftheoldfunctionhavemigratedtothenewone.
Example:RenamingaFunction(SimpleMechanics)
Considerthisfunctionwithanoverlyabbrevedname.
functioncircum(radius){
return2*Math.PI*radius;
}
Iwanttochangethattosomethingmoresensible.Ibeginbychangingthedeclaration:
functioncircumference(radius){
return2*Math.PI*radius;
}
Ithenfindallthecallersofcircumandchangethenametocircumference.
Differentlanguageenvironmentshaveanimpactonhoweasyitistofindallthereferencestotheoldfunction.StatictypingandagoodIDEprovidethebestexperience,usuallyallowingmetorenamefunctionsautomaticallywithlittlechanceoferror.Withoutstatictyping,thiscanbemoreinvolved;evengoodsearchingtoolswillthenhavealotoffalsepositives.
Iusethesameapproachforaddingorremovingparameters:findallthecallers,changethedeclaration,andchangethecallers.It’softenbettertodotheseasseparatesteps—so,ifI’mbothrenamingthefunctionandaddingaparameter,Ifirstdotherename,test,thenaddtheparameter,andtestagain.
AdisadvantageofthissimplewayofdoingtherefactoringisthatIhavetodoallthecallersandthedeclaration(orallofthem,ifpolymorphic)atonce.Ifthereareonlyafewofthem,orifIhavedecentautomatedrefactoringtools,thisisreasonable.Butifthere’salot,itcangettricky.Anotherproblemiswhenthenamesaren’tunique—e.g.IwanttorenametheachangeAddressmethodonapersonclassbutthesamemethod,whichIdon’twanttochange,existsonaninsuranceagreementclass.Themorecomplexthechangeis,thelessIwanttodo
itinonegolikethis.Whenthiskindofproblemarises,Iusethemigrationmechanicsinstead.Similarly,ifIusesimplemechanicsandsomethinggoeswrong,I’llrevertthecodetothelastknowngoodstateandtryagainusingmigrationmechanics.
Example:RenamingaFunction(MigrationMechanics)
Again,Ihavethisfunctionwithitsoverlyabbrevedname.
functioncircum(radius){
return2*Math.PI*radius;
}
Todothisrefactoringwithmigrationmechanics,IbeginbyapplyingExtractFunction(106)totheentirefunctionbody.
functioncircum(radius){
returncircumference(radius);
}
functioncircumference(radius){
return2*Math.PI*radius;
}
Itestthat,thenapplyInlineFunction(115)totheoldfunctions.Ifindallthecallsoftheoldfunctionandreplaceeachonewithacallofthenewone.Icantestaftereachchange,whichallowsmetodothemoneatatime.OnceI’vegotthemall,Iremovetheoldfunction.
Withmostrefactorings,I’mchangingcodethatIcanmodify,butthisrefactoringcanbehandywithapublishedAPI—thatis,oneusedbycodethatI’munabletochangemyself.Icanpausetherefactoringaftercreatingcircumferenceand,ifpossible,markcircumasdeprecated.Iwillthenwaitforcallerstochangetousecircumference;oncetheydo,Icandeletecircum.EvenifI’mneverabletoreachthehappypointofdeletingcircum,atleastIhaveabetternamefornewcode.
Example:AddingaParameter
Insomesoftware,tomanagealibraryofbooks,Ihaveabookclasswhichhastheabilitytotakeareservationforacustomer.
classBook…
addReservation(customer){
this._reservations.push(customer);
}
Ineedtosupportapriorityqueueforreservations.Thus,IneedanextraparameteronaddReservationtoindicatewhetherthereservationshouldgointheusualqueueorthehigh-priorityqueue.IfIcaneasilyfindandchangeallthecallers,thenIcanjustgoaheadwiththechange—butifnot,Icanusethemigrationapproach,whichI’llshowhere.
IbeginbyusingExtractFunction(106)onthebodyofaddReservationtocreatethenewfunction.AlthoughitwilleventuallybecalledaddReservation,thenewandoldfunctionscan’tcoexistwiththesamename.SoIuseatemporarynamethatwillbeeasytosearchforlater.
classBook…
addReservation(customer){
this.zz_addReservation(customer);
}
zz_addReservation(customer){
this._reservations.push(customer);
}
Ithenaddtheparametertothenewdeclarationanditscall(ineffect,usingthesimplemechanics).
classBook…
addReservation(customer){
this.zz_addReservation(customer,false);
}
zz_addReservation(customer,isPriority){
this._reservations.push(customer);
}
WhenIuseJavaScript,beforeIchangeanyofthecallers,IliketoapplyIntroduceAssertion(299)tocheckthenewparameterisusedbythecaller.
classBook…
zz_addReservation(customer,isPriority){
assert(isPriority===true||isPriority===false);
this._reservations.push(customer);
}
Now,whenIchangethecallers,ifImakeamistakeandleaveoffthenewparameter,thisassertionwillhelpmecatchthemistake.AndIknowfromlongexperiencetherearefewmoremistake-proneprogrammersthanmyself.
Now,IcanstartchangingthecallersbyusingInlineFunction(115)ontheoriginalfunction.Thisallowsmetochangeonecalleratatime.
Ithenrenamethenewfunctionbacktotheoriginal.Usually,thesimplemechanicsworkfineforthis,butIcanalsousethemigrationapproachifIneedto.
Example:ChangingaParametertoOneofItsProperties
Theexamplessofararesimplechangesofanameandaddinganewparameter,butwiththemigrationmechanics,thisrefactoringcanhandlemorecomplicatedcasesquiteneatly.Here’sanexamplethatisabitmoreinvolved.
IhaveafunctionwhichdeterminesifacustomerisbasedinNewEngland.
functioninNewEngland(aCustomer){
return["MA","CT","ME","VT","NH","RI"].includes(aCustomer.address.state);
}
Hereisoneofitscallers
caller…
constnewEnglanders=someCustomers.filter(c=>inNewEngland(c));
inNewEnglandonlyusesthecustomer’shomestatetodetermineifit’sinNewEngland.I’dprefertorefactorinNewEnglandsothatittakesastatecodeasaparameter,makingitusableinmorecontextsbyremovingthedependencyonthecustomer.
WithChangeFunctionDeclaration,myusualfirstmoveistoapplyExtractFunction(106),butinthiscaseIcanmakeiteasierbyfirstrefactoringthe
functionbodyalittle.IuseExtractVariable(119)onmydesirednewparameter.
functioninNewEngland(aCustomer){
conststateCode=aCustomer.address.state;
return["MA","CT","ME","VT","NH","RI"].includes(stateCode);
}
NowIuseExtractFunction(106)tocreatethatnewfunction.
functioninNewEngland(aCustomer){
conststateCode=aCustomer.address.state;
returnxxNEWinNewEngland(stateCode);
}
functionxxNEWinNewEngland(stateCode){
return["MA","CT","ME","VT","NH","RI"].includes(stateCode);
}
Igivethefunctionanamethat’seasytoautomaticallyreplacetoturnintotheoriginalnamelater.(YoucantellIdon’thaveastandardforthesetemporarynames.)
IapplyInlineVariable(123)ontheinputparameterintheoriginalfunction.
functioninNewEngland(aCustomer){
returnxxNEWinNewEngland(aCustomer.address.state);
}
IuseInlineFunction(115)tofoldtheoldfunctionintoitscallers,effectivelyreplacingthecalltotheoldfunctionwithacalltothenewone.Icandotheseoneatatime.
caller…
constnewEnglanders=someCustomers.filter(c=>xxNEWinNewEngland(c.address.state))
OnceI’veinlinedtheoldfunctionintoeverycaller,IuseChangeFunctionDeclarationagaintochangethenameofthenewfunctiontothatoftheoriginal.
caller…
constnewEnglanders=someCustomers.filter(c=>inNewEngland(c.address.state));
toplevel…
functioninNewEngland(stateCode){
return["MA","CT","ME","VT","NH","RI"].includes(stateCode);
}
Automatedrefactoringtoolsmakethemigrationmechanicsbothlessusefulandmoreeffective.Theymakeitlessusefulbecausetheyhandleevencomplicatedrenamesandparameterchangessafer,soIdon’thavetousethemigrationapproachasoftenasIdowithoutthatsupport.However,incaseslikethisexample,wherethetoolscan’tdothewholerefactoring,theystillmakeitmucheasierasthekeymovesofextractandinlinecanbedonemorequicklyandsafelywiththetool.
EncapsulateVariable
formerly:Self-EncapsulateField
formerly:EncapsulateField
Motivation
Refactoringisallaboutmanipulatingtheelementsofourprograms.Dataismoreawkwardtomanipulatethanfunctions.Sinceusingafunctionusuallymeanscallingit,Icaneasilyrenameormoveafunctionwhilekeepingtheoldfunction
intactasaforwardingfunction(somyoldcodecallstheoldfunction,whichcallsthenewfunction).I’llusuallynotkeepthisforwardingfunctionaroundforlong,butitdoessimplifytherefactoring.
DataismoreawkwardbecauseIcan’tdothat.IfImovedataaround,Ihavetochangeallthereferencestothedatainasinglecycletokeepthecodeworking.Fordatawithaverysmallscopeofaccess,suchasatemporaryvariableinasmallfunction,thisisn’taproblem.Butasthescopegrows,sodoesthedifficulty,whichiswhyglobaldataissuchapain.
SoifIwanttomovewidely-accesseddata,oftenthebestapproachistofirstencapsulateitbyroutingallitsaccessthroughfunctions.Thatway,Iturnthedifficulttaskofreorganizingdataintothesimplertaskofreorganizingfunctions.
Encapsulatingdataisvaluableforotherthingstoo.Itprovidesaclearpointtomonitorchangesanduseofthedata;Icaneasilyaddvalidationorconsequentiallogicontheupdates.Itismyhabittomakeallmutabledataencapsulatedlikethisandonlyaccessedthroughfunctionsifitsscopeisgreaterthanasinglefunction.Thegreaterthescopeofthedata,themoreimportantitistoencapsulate.MyapproachwithlegacycodeisthatwheneverIneedtochangeoraddanewreferencetosuchavariable,Ishouldtaketheopportunitytoencapsulateit.ThatwayIpreventtheincreaseofcouplingtocommonlyuseddata.
Thisprincipleiswhytheobject-orientedapproachputssomuchemphasisonkeepinganobject’sdataprivate.WheneverIseeapublicfield,IconsiderusingEncapsulateVariable(inthatcaseoftencalledEncapsulateVariable(132))toreduceitsvisibility.Somegofurtherandarguethateveninternalreferencestofieldswithinaclassshouldgothroughaccessorfunctions—anapproachknownasself-encapsulation.Onthewhole,Ifindself-encapsulationexcessive—ifaclassissobigthatIneedtoself-encapsulateitsfields,itneedstobebrokenupanyway.Butself-encapsulatingafieldisausefulstepbeforesplittingaclass.
Keepingdataencapsulatedismuchlessimportantforimmutabledata.Whenthedatadoesn’tchange,Idon’tneedaplacetoputinvalidationorotherlogichooksbeforeupdates.Icanalsofreelycopythedataratherthanmoveit—soIdon’thavetochangereferencesfromoldlocations,nordoIworryaboutsectionsofcodegettingstaledata.Immutabilityisapowerfulpreservative.
Mechanics
Createencapsulatingfunctionstoaccessandupdatethevariable.
Runstaticchecks.
Foreachreferencetothevariable,replacewithacalltotheappropriateencapsulatingfunction.Testaftereachreplacement.
Restrictthevisibilityofthevariable.
Sometimesit’snotpossibletopreventaccesstothevariable.Ifso,itmaybeusefultodetectanyremainingreferencesbyrenamingthevariableandtesting.
Test.
Ifthevalueofthevariableisarecord,considerEncapsulateRecord(160).
Example
Considersomeusefuldataheldinaglobalvariable.
letdefaultOwner={firstName:"Martin",lastName:"Fowler"};
Likeanydata,it’sreferencedwithcodelikethis:
spaceship.owner=defaultOwner;
Andupdatedlikethis:
defaultOwner={firstName:"Rebecca",lastName:"Parsons"};
Todoabasicencapsulationonthis,Istartbydefiningfunctionstoreadandwritethedata.
functiongetDefaultOwner(){returndefaultOwner;}
functionsetDefaultOwner(arg){defaultOwner=arg;}
IthenstartworkingonreferencestodefaultOwner.WhenIseeareference,Ireplaceitwithacalltothegettingfunction.
spaceship.owner=getDefaultOwner();
WhenIseeanassignment,Ireplaceitwiththesettingfunction.
setDefaultOwner({firstName:"Rebecca",lastName:"Parsons"});
Itestaftereachreplacement.
OnceI’mdonewithallthereferences,Irestrictthevisibilityofthevariable.Thisbothchecksthattherearen’tanyreferencesthatI’vemissed,andensuresthatfuturechangestothecodewon’taccessthevariabledirectly.IcandothatinJavaScriptbymovingboththevariableandtheaccessormethodstotheirownfileandonlyexportingtheaccessormethods.
defaultOwner.js…
letdefaultOwner={firstName:"Martin",lastName:"Fowler"};
exportfunctiongetDefaultOwner(){returndefaultOwner;}
exportfunctionsetDefaultOwner(arg){defaultOwner=arg;}
IfI’minasituationwhereIcannotrestricttheaccesstoavariable,itmaybeusefultorenamethevariableandretest.Thatwon’tpreventfuturedirectaccess,butnamingthevariablesomethingmeaningfulandawkwardsuchas__privateOnly_defaultOwnermayhelp.
Idon’tliketheuseofgetprefixesongetters,soI’llrenametoremoveit.
defaultOwner.js…
letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};
exportfunctiongetdefaultOwner(){returndefaultOwnerData;}
exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}
AcommonconventioninJavaScriptistonameagettingfunctionandsettingfunctionthesameanddifferentiatethemduethepresenceofanargument.IcallthispracticeOverloadedGetterSetter[bib-overloaded-getter-setter]andstronglydislikeit.So,eventhoughIdon’tlikethegetprefix,Iwillkeepthesetprefix.
EncapsulatingtheValue
ThebasicrefactoringI’veoutlinedhereencapsulatesareferencetosomedatastructure,allowingmetocontrolitsaccessandre-assignment.Butitdoesn’t
controlchangestothatstructure.
constowner1=defaultOwner();
assert.equal("Fowler",owner1.lastName,"whenset");
constowner2=defaultOwner();
owner2.lastName="Parsons";
assert.equal("Parsons",owner1.lastName,"afterchangeowner2");//isthisok?
Thebasicrefactoringencapsulatesthereferencetothedataitem.Inmanycases,thisisallIwanttodoforthemoment.ButIoftenwanttotaketheencapsulationdeepertocontrolnotjustchangestothevariablebutalsotoitscontents.
Forthis,Ihaveacoupleofoptions.Thesimplestoneistopreventanychangestothevalue.Myfavoritewaytohandlethisisbymodifyingthegettingfunctiontoreturnacopyofthedata.
defaultOwner.js…
letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};
exportfunctiondefaultOwner(){returnObject.assign({},defaultOwnerData);}
exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}
Iusethisapproachparticularlyoftenwithlists.IfIreturnacopyofthedata,anyclientsusingitcanchangeit,butthatchangeisn’treflectedintheshareddata.Ihavetobecarefulwithusingcopies,however:Somecodemayexpecttochangeshareddata.Ifthat’sthecase,I’mrelyingonmyteststodetectaproblem.Analternativeistopreventchanges—andagoodwayofdoingthatisEncapsulateRecord(160).
letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};
exportfunctiondefaultOwner(){returnnewPerson(defaultOwnerData);}
exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}
classPerson{
constructor(data){
this._lastName=data.lastName;
this._firstName=data.firstName
}
getlastName(){returnthis._lastName;}
getfirstName(){returnthis._firstName;}
//andsoonforotherproperties
Now,anyattempttoreassignthepropertiesofthedefaultownerwillcauseanerror.Differentlanguageshavedifferenttechniquestodetectorpreventchanges
likethis,sodependingonthelanguageI’dconsiderotheroptions.
Detectingandpreventingchangeslikethisisoftenworthwhileasatemporarymeasure.Icaneitherremovethechanges,orprovidesuitablemutatingfunctions.Then,oncetheyarealldealtwith,Icanmodifythegettingmethodtoreturnacopy.
SofarI’vetalkedaboutcopyingongettingdata,butitmaybeworthwhiletomakeacopyinthesettertoo.ThatwilldependonwherethedatacomesfromandwhetherIneedtomaintainalinktoreflectanychangesinthatoriginaldata.IfIdon’tneedsuchalink,acopypreventsaccidentsduetochangesonthatsourcedata.Takingacopymaybesuperfluousmostofthetime,butcopiesinthesecasesusuallyhaveanegligibleeffectonperformance;ontheotherhand,ifIdon’tdothem,thereisariskofalonganddifficultboutofdebugginginthefuture.
Rememberthatthecopyingabove,andtheclasswrapper,bothonlyworkoneleveldeepintherecordstructure.Goingdeeperrequiresmorelevelsofcopiesorobjectwrapping.
Asyoucansee,encapsulatingdataisvaluable,butoftennotstraightforward.Exactlywhattoencapsulate—andhowtodoit—dependsonthewaythedataisbeingusedandthechangesIhaveinmind.Butthemorewidelyit’sused,themoreit’sworthmyattentiontoencapsulateproperly.
RenameVariable
Motivation
Namingthingswellistheheartofclearprogramming.VariablescandoalottoexplainwhatI’mupto—ifInamethemwell.ButIfrequentlygetmynameswrong—sometimesbecauseI’mnotthinkingcarefullyenough,sometimesbecausemyunderstandingoftheproblemimprovesasIlearnmore,andsometimesbecausetheprogram’spurposechangesasmyusers’needschange.
Evenmorethanmostprogramelements,theimportanceofanamedependsonhowwidelyit’sused.Avariableusedinaone-linelambdaexpressionisusuallyeasytofollow—Ioftenuseasingleletterinthatcasesincethevariable’spurposeisclearfromitscontext.Parametersforshortfunctionscanoftenbeterseforthesamereason,althoughinadynamicallytypedlanguagelikeJavaScript,Idoliketoputthetypeintothename(henceparameternameslikeaCustomer).
Persistentfieldsthatlastbeyondasinglefunctioninvocationrequiremorecarefulnaming.ThisiswhereI’mlikelytoputmostofmyattention.
Mechanics
Ifthevariableisusedwidely,considerEncapsulateVariable(132).
Findallreferencestothevariable,andchangeeveryone.
Iftherearereferencesfromanothercodebase,thevariableisapublishedvariable,andyoucannotdothisrefactoring.
Ifthevariabledoesnotchange,youcancopyittoonewiththenewname,thenchangegradually,testingaftereachchange.
Test.
Example
Thesimplestcaseforrenamingavariableiswhenit’slocaltoasinglefunction:atemporargument.It’stootrivialforevenanexample:Ijustfindeachreferenceandchangeit.AfterI’mdone,ItesttoensureIdidn’tmessup.
Problemsoccurwhenthevariablehasawiderscopethanjustasinglefunction.Theremaybealotofreferencesalloverthecodebase.
lettpHd="untitled";
Somereferencesaccessthevariable:
result+=`<h1>${tpHd}</h1>`;
Othersupdateit:
tpHd=obj['articleTitle'];
MyusualresponsetothisisapplyEncapsulateVariable(132).
result+=`<h1>${title()}</h1>`;
setTitle(obj['articleTitle']);
functiontitle(){returntpHd;}
functionsetTitle(arg){tpHd=arg;}
Atthispoint,Icanrenamethevariable.
let_title="untitled";
functiontitle(){return_title;}
functionsetTitle(arg){_title=arg;}
Atthispoint,Icouldcontinuebyinliningthewrappingfunctionssoallcallersareusingthevariabledirectly.ButI’drarelywanttodothis.IfthevariableisusedwidelyenoughthatIfeeltheneedtoencapsulateitinordertochangeitsname,it’sworthkeepingitencapsulatedbehindfunctionsforthefuture.
IncaseswhereIwasgoingtoinline,I’dcallthegettingfunctiongetTitleandnotuseanunderscoreforthevariablenamewhenIrenameit.
RenamingaConstant
IfI’mrenamingaconstant(orsomethingthatactslikeaconstanttoclients)Icanavoidencapsulation,andstilldotherenamegradually,bycopying.Iftheoriginaldeclarationlookslikethis:
constcpyNm="AcmeGooseberries";
Icanbegintherenamingbymakingacopy:
constcompanyName="AcmeGooseberries";
constcpyNm=companyName;
Withthecopy,Icangraduallychangereferencesfromtheoldnametothenewname.WhenI’mdone,Iremovethecopy.Iprefertodeclarethenewnameandcopytotheoldnameifitmakesitatadeasiertoremovetheoldnameandputitbackagainshouldatestfail.
Thisworksforconstantsaswellasforvariablesthatareread-onlytoclients(suchasanexportedvariableinJavaScript).
IntroduceParameterObject
Motivation
Ioftenseegroupsofdataitemsthatregularlytraveltogether,appearinginfunctionafterfunction.Suchagroupisadataclump,andIliketoreplaceitwithasingledatastructure.
Groupingdataintoastructureisvaluablebecauseitmakesexplicittherelationshipbetweenthedataitems.Itreducesthesizeofparameterlistsforanyfunctionthatusesthenewstructure.Ithelpsconsistencysinceallfunctionsthatusethestructurewillusethesamenamestogetatitselements.
Buttherealpowerofthisrefactoringishowitenablesdeeperchangestothecode.WhenIidentifythesenewstructures,Icanreorientthebehavioroftheprogramtousethesestructures.Iwillcreatefunctionsthatcapturethecommonbehavioroverthisdata—eitherasasetofcommonfunctionsorasaclassthatcombinesthedatastructurewiththesefunctions.Thisprocesscanchangetheconceptualpictureofthecode,raisingthesestructuresasnewabstractionsthatcangreatlysimplifymyunderstandingofthedomain.Whenthisworks,itcanhavesurprisinglypowerfuleffects—butnoneofthisispossibleunlessIuseIntroduceParameterObjecttobegintheprocess.
Mechanics
Ifthereisn’tasuitablestructurealready,createone.
Iprefertouseaclass,asthatmakesiteasiertogroupbehaviorlateron.Iusuallyliketoensurethesestructuresarevalueobjects[bib-value-object].
Test.
UseChangeFunctionDeclaration(124)toaddaparameterforthenewstructure.
Test.
Adjusteachcallertopassinthecorrectinstanceofthenewstructure.Testaftereachone.
Foreachelementofthenewstructure,replacetheuseoftheoriginalparameterwiththeelementofthestructure.Removetheparameter.Test.
Example
I’llbeginwithsomecodethatlooksatasetoftemperaturereadingsanddetermineswhetheranyofthemfalloutsideofanoperatingrange.Here’swhat
thedatalookslikeforthereadings:
conststation={name:"ZB1",
readings:[
{temp:47,time:"2016-11-1009:10"},
{temp:53,time:"2016-11-1009:20"},
{temp:58,time:"2016-11-1009:30"},
{temp:53,time:"2016-11-1009:40"},
{temp:51,time:"2016-11-1009:50"},
]
};
Ihaveafunctiontofindthereadingsthatareoutsideatemperaturerange.
functionreadingsOutsideRange(station,min,max){
returnstation.readings
.filter(r=>r.temp<min||r.temp>max);
}
Itmightbecalledfromsomecodelikethis:
caller
alerts=readingsOutsideRange(station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling);
NoticehowthecallingcodepullsthetwodataitemsasapairfromanotherobjectandpassesthepairintoreadingsOutsideRange.TheoperatingplanusesdifferentnamestoindicatethestartandendoftherangecomparedtoreadingsOutsideRange.Arangelikethisisacommoncasewheretwoseparatedataitemsarebettercombinedintoasingleobject.I’llbeginbydeclaringaclassforthecombineddata.
classNumberRange{
constructor(min,max){
this._data={min:min,max:max};
}
getmin(){returnthis._data.min;}
getmax(){returnthis._data.max;}
}
Ideclareaclass,ratherthanjustusingabasicJavaScriptobject,becauseIusuallyfindthisrefactoringtobeafirststeptomovingbehaviorintothenewlycreatedobject.Sinceaclassmakessenseforthis,Igorightaheadanduseone
directly.Ialsodon’tprovideanyupdatemethodsforthenewclass,asI’llprobablymakethisaValueObject(https://martinfowler.com/bliki/ValueObject.html).MosttimesIdothisrefactoring,Icreatevalueobjects.
IthenuseChangeFunctionDeclaration(124)toaddthenewobjectasaparametertoreadingsOutsideRange.
functionreadingsOutsideRange(station,min,max,range){
returnstation.readings
.filter(r=>r.temp<min||r.temp>max);
}
InJavaScript,Icanleavethecallerasis,butinotherlanguagesI’dhavetoaddanullforthenewparameterwhichwouldlooksomethinglikethis:
caller
alerts=readingsOutsideRange(station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling,
null);
AtthispointIhaven’tchangedanybehavior,andtestsshouldstillpass.Ithengotoeachcallerandadjustittopassinthecorrectdaterange.
caller
constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);
alerts=readingsOutsideRange(station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling,
range);
Istillhaven’talteredanybehavioryet,astheparameterisn’tused.Alltestsshouldstillwork.
NowIcanstartreplacingtheusageoftheparameters.I’llstartwiththemaximum.
functionreadingsOutsideRange(station,min,max,range){
returnstation.readings
.filter(r=>r.temp<min||r.temp>range.max);
}
caller
constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);
alerts=readingsOutsideRange(station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling,
range);
Icantestatthispoint,thenremovetheotherparameter.
functionreadingsOutsideRange(station,min,range){
returnstation.readings
.filter(r=>r.temp<range.min||r.temp>range.max);
}
caller
constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);
alerts=readingsOutsideRange(station,
operatingPlan.temperatureFloor,
range);
Thatcompletesthisrefactoring.However,replacingaclumpofparameterswitharealobjectisjustthesetupforthereallygoodstuff.ThegreatbenefitsofmakingaclasslikethisisthatIcanthenmovebehaviorintothenewclass.Inthiscase,I’daddamethodforrangethattestsifavaluefallswithintherange.
functionreadingsOutsideRange(station,range){
returnstation.readings
.filter(r=>!range.contains(r.temp));
}
classNumberRange…
contains(arg){return(arg>=this.min&&arg<=this.max);}
Thisisafirststeptocreatingarange[bib-range-pattern]thatcantakeonalotofusefulbehavior.OnceI’veidentifiedtheneedforarangeinmycode,IcanbeconstantlyonthelookoutforothercaseswhereIseeamax/minpairofnumbersandreplacethemwitharange.(Oneimmediatepossibilityistheoperatingplan,replacingtemperatureFloorandtemperatureCeilingwithatemperatureRange.)AsIlookathowthesepairsareused,Icanmovemoreusefulbehaviorintotherangeclass,simplifyingitsusageacrossthecodebase.OneofthefirstthingsImayaddisavalue-basedequalitymethodtomakeita
truevalueobject.
CombineFunctionsintoClass
Motivation
Classesareafundamentalconstructinmostmodernprogramminglanguages.Theybindtogetherdataandfunctionsintoasharedenvironment,exposingsomeofthatdataandfunctiontootherprogramelementsforcollaboration.Theyaretheprimaryconstructinobject-orientedlanguages,butarealsousefulwithotherapproachestoo.
WhenIseeagroupoffunctionsthatoperatecloselytogetheronacommonbodyofdata(usuallypassedasargumentstothefunctioncall),Iseeanopportunitytoformaclass.Usingaclassmakesthecommonenvironmentthatthesefunctionssharemoreexplicit,allowsmetosimplifyfunctioncallsinsidetheobjectbyremovingmanyofthearguments,andprovidesareferencetopasssuchanobjecttootherpartsofthesystem.
Inadditiontoorganizingalreadyformedfunctions,thisrefactoringalsoprovides
agoodopportunitytoidentifyotherbitsofcomputationandrefactorthemintomethodsonthenewclass.
AnotherwayoforganizingfunctionstogetherisCombineFunctionsintoTransform(149).Whichonetousedependsmoreonthebroadercontextoftheprogram.Onesignificantadvantageofusingaclassisthatitallowsclientstomutatethecoredataoftheobject,andthederivationsremainconsistent.
Aswellasaclass,functionslikethiscanalsobecombinedintoanestedfunction.UsuallyIpreferaclasstoanestedfunction,asitcanbedifficulttotestfunctionsnestedwithinanother.ClassesarealsonecessarywhenthereismorethanonefunctioninthegroupthatIwanttoexposetocollaborators.
Languagesthatdon’thaveclassesasafirst-classelement,butdohavefirst-classfunctions,oftenusetheFunctionAsObject(https://martinfowler.com/bliki/FunctionAsObject.html)toprovidethiscapability.
Mechanics
ApplyEncapsulateRecord(160)tothecommondatarecordthatthefunctionsshare.
Ifthedatathatiscommonbetweenthefunctionsisn’talreadygroupedintoarecordstructure,useIntroduceParameterObject(140)tocreatearecordtogroupittogether.
TakeeachfunctionthatusesthecommonrecordanduseMoveFunction(196)tomoveitintothenewclass.
Anyargumentstothefunctioncallthatarememberscanberemovedfromtheargumentlist.
EachbitoflogicthatmanipulatesthedatacanbeextractedwithExtractFunction(106)andthenmovedintothenewclass.
Example
IgrewupinEngland,acountryrenownedforitsloveofTea.(Personally,I
don’tlikemostteatheyserveinEngland,buthavesinceacquiredatasteforChineseandJapaneseteas.)Somyauthor’sfantasyconjuresupastateutilityforprovidingteatothepopulation.Everymonththeyreadtheteameters,togetarecordlikethis:
reading={customer:"ivan",quantity:10,month:5,year:2017};
Ilookthroughthecodethatprocessestheserecords,andIseelotsofplaceswheresimilarcalculationsaredoneonthedata.SoIfindaspotthatcalculatesthebasecharge:
client1…
constaReading=acquireReading();
constbaseCharge=baseRate(aReading.month,aReading.year)*aReading.quantity;
BeingEngland,everythingessentialmustbetaxed,soitiswithtea.Buttherulesallowatleastanessentiallevelofteatobefreeoftaxation.
client2…
constaReading=acquireReading();
constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);
consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));
I’msurethat,likeme,younoticedthattheformulaforthebasechargeisduplicatedbetweenthesetwofragments.Ifyou’relikeme,you’realreadyreachingforExtractFunction(106).Interestingly,itseemsourworkhasbeendoneforuselsewhere.
client3…
constaReading=acquireReading();
constbasicChargeAmount=calculateBaseCharge(aReading);
functioncalculateBaseCharge(aReading){
returnbaseRate(aReading.month,aReading.year)*aReading.quantity;
}
Giventhis,Ihaveanaturalimpulsetochangethetwoearlierbitsofclientcodetousethisfunction.Butthetroublewithtop-levelfunctionslikethisisthattheyareofteneasytomiss.I’dratherchangethecodetogivethefunctionacloserconnectiontothedataitprocesses.Agoodwaytodothisistoturnthedatainto
aclass.
Toturntherecordintoaclass,IuseEncapsulateRecord(160).
classReading{
constructor(data){
this._customer=data.customer;
this._quantity=data.quantity;
this._month=data.month;
this._year=data.year;
}
getcustomer(){returnthis._customer;}
getquantity(){returnthis._quantity;}
getmonth(){returnthis._month;}
getyear(){returnthis._year;}
}
Tomovethebehavior,I’llstartwiththefunctionIalreadyhave:calculateBaseCharge.Tousethenewclass,IneedtoapplyittothedataassoonasI’veacquiredit.
client3…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
constbasicChargeAmount=calculateBaseCharge(aReading);
IthenuseMoveFunction(196)tomovecalculateBaseChargeintothenewclass.
classReading…
getcalculateBaseCharge(){
returnbaseRate(this.month,this.year)*this.quantity;
}
client3…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
constbasicChargeAmount=aReading.calculateBaseCharge;
WhileI’matit,IuseChangeFunctionDeclaration(124)tomakeitsomethingmoretomyliking.
getbaseCharge(){
returnbaseRate(this.month,this.year)*this.quantity;
}
client3…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
constbasicChargeAmount=aReading.baseCharge;
Withthisnaming,theclientofthereadingclasscan’ttellwhetherthebasechargeisafieldoraderivedvalue.ThisisaGoodThing—theUniformAccessPrinciple[bib-uniform-access].
Inowalterthefirstclienttocallthemethodratherthanrepeatthecalculation.
client1…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
constbaseCharge=aReading.baseCharge;
There’sastrongchanceI’lluseInlineVariable(123)onthebaseChargevariablebeforethedayisout.Butmorerelevanttothisrefactoringistheclientthatcalculatesthetaxableamount.Myfirststephereistousethenewbasechargeproperty.
client2…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
consttaxableCharge=Math.max(0,aReading.baseCharge-taxThreshold(aReading.year));
IuseExtractFunction(106)onthecalculationforthetaxablecharge.
functiontaxableChargeFn(aReading){
returnMath.max(0,aReading.baseCharge-taxThreshold(aReading.year));
}
client3…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
consttaxableCharge=taxableChargeFn(aReading);
ThenIapplyMoveFunction(196).
classReading…
gettaxableCharge(){
returnMath.max(0,this.baseCharge-taxThreshold(this.year));
}
client3…
constrawReading=acquireReading();
constaReading=newReading(rawReading);
consttaxableCharge=aReading.taxableCharge;
Sinceallthederiveddataiscalculatedondemand,IhavenoproblemshouldIneedtoupdatethestoreddata.Ingeneral,Ipreferimmutabledata,butmanycircumstancesforceustoworkwithmutabledata(suchasJavaScript,alanguageecosystemthatwasn’tdesignedwithimmutabilityinmind).Whenthereisareasonablechancethedatawillbeupdatedsomewhereintheprogram,thenaclassisveryhelpful.
CombineFunctionsintoTransform
Motivation
Softwareofteninvolvesfeedingdataintoprogramsthatcalculatevariousderivedinformationfromit.Thesederivedvaluesmaybeneededinseveralplaces,andthosecalculationsareoftenrepeatedwhereverthederiveddataisused.Iprefertobringallofthesederivationstogether,soIhaveaconsistentplacetofindandupdatethemandavoidanyduplicatelogic.
Onewaytodothisistouseadatatransformationfunctionthattakesthesourcedataasinputandcalculatesallthederivations,puttingeachderivedvalueasafieldintheoutputdata.Then,toexaminethederivations,allIneeddoislookatthetransformfunction.
AnalternativetoCombineFunctionsintoTransformisCombineFunctionsintoClass(144)thatmovesthelogicintomethodsonaclassformedfromthesourcedata.Eitheroftheserefactoringsarehelpful,andmychoicewilloftendependonthestyleofprogrammingalreadyinthesoftware.Butthereisoneimportantdifference:Usingaclassismuchbetterifthesourcedatagetsupdatedwithinthe
code.Usingatransformstoresderiveddatainthenewrecord,soifthesourcedatachanges,Iwillrunintoinconsistencies.
OneofthereasonsIliketodocombinefunctionsistoavoidduplicationofthederivationlogic.IcandothatjustbyusingExtractFunction(106)onthelogic,butit’softendifficulttofindthefunctionsunlesstheyarekeptclosetothedatastructurestheyoperateon.Usingatransform(oraclass)makesiteasytofindandusethem.
Mechanics
Createatransformationfunctionthatthattakestherecordtobetransformedandreturnsthesamevalues.
Thiswillusuallyinvolveadeepcopyoftherecord.Itisoftenworthwhiletowriteatesttoensurethetransformdoesnotaltertheoriginalrecord.
Picksomelogicandmoveitsbodyintothetransformtocreateanewfieldintherecord.Changetheclientcodetoaccessthenewfield.
Ifthelogiciscomplex,useExtractFunction(106)first.
Test.
Repeatfortheotherrelevantfunctions.
Example
WhereIgrewup,teaisanimportantpartoflife—somuchthatIcanimagineaspecialutilitythatprovidesteatothepopulacethat’sregulatedlikeautility.Everymonth,theutilitygetsareadingofhowmuchteaacustomerhasacquired.
reading={customer:"ivan",quantity:10,month:5,year:2017};
Codeinvariousplacescalculatesvariousconsequencesofthisteausage.Onesuchcalculationisthebasemonetaryamountthat’susedtocalculatethechargeforthecustomer.
client1…
constaReading=acquireReading();
constbaseCharge=baseRate(aReading.month,aReading.year)*aReading.quantity;
Anotheristheamountthatshouldbetaxed—whichislessthanthebaseamountsincethegovernmentwiselyconsidersthateverycitizenshouldgetsometeatax-free.
client2…
constaReading=acquireReading();
constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);
consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));
Lookingthroughthiscode,Iseethesecalculationsrepeatedinseveralplaces.Suchduplicationisaskingfortroublewhentheyneedtochange(andI’dbetit’s“when”not“if”).IcandealwiththisrepetitionbyusingExtractFunction(106)onthesecalculations,butsuchfunctionsoftenendupscatteredaroundtheprogrammakingithardforfuturedeveloperstorealizetheyarethere.Indeed,lookingaroundIdiscoversuchafunction,usedinanotherareaofthecode.
client3…
constaReading=acquireReading();
constbasicChargeAmount=calculateBaseCharge(aReading);
functioncalculateBaseCharge(aReading){
returnbaseRate(aReading.month,aReading.year)*aReading.quantity;
}
Onewayofdealingwiththisistomoveallofthesederivationsintoatransformationstepthattakestherawreadingandemitsareadingenrichedwithallthecommonderivedresults.
Ibeginbycreatingatransformationfunctionthatmerelycopiestheinputobject.
functionenrichReading(original){
constresult=_.cloneDeep(original);
returnresult;
}
I’musingthecloneDeepfromlodashtocreateadeepcopy.
WhenI’mapplyingatransformationthatproducesessentiallythesamethingbut
withadditionalinformation,Iliketonameitusing“enrich”.IfitwereproducingsomethingIfeltwasdifferent,Iwouldnameitusing“transform”.
IthenpickoneofthecalculationsIwanttochange.First,Ienrichthereadingituseswiththecurrentonethatdoesnothingyet.
client3…
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
constbasicChargeAmount=calculateBaseCharge(aReading);
IuseMoveFunction(196)oncalculateBaseChargetomoveitintotheenrichmentcalculation.
functionenrichReading(original){
constresult=_.cloneDeep(original);
result.baseCharge=calculateBaseCharge(result);
returnresult;
}
Withinthetransformationfunction,I’mhappytomutatearesultobject,insteadofcopyingeachtime.Ilikeimmutability,butmostcommonlanguagesmakeitdifficulttoworkwith.I’mpreparedtogothroughtheextraefforttosupportitatboundaries,butwillmutatewithinsmallerscopes.Ialsopickmynames(usingaReadingastheaccumulatingvariable)tomakeiteasiertomovethecodeintothetransformerfunction.
Ichangetheclientthatusesthatfunctiontousetheenrichedfieldinstead.
client3…
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
constbasicChargeAmount=aReading.baseCharge;
OnceI’vemovedallcallstocalculateBaseCharge,IcannestitinsideenrichReading.Thatwouldmakeitclearthatclientsthatneedthecalculatedbasechargeshouldusetheenrichedrecord.
Onetraptobewareofhere.WhenIwriteenrichReadinglikethis,toreturntheenrichedreading,I’mimplyingthattheoriginalreadingrecordisn’tchanged.So
it’swiseformetoaddatest.
it('checkreadingunchanged',function(){
constbaseReading={customer:"ivan",quantity:15,month:5,year:2017};
constoracle=_.cloneDeep(baseReading);
enrichReading(baseReading);
assert.deepEqual(baseReading,oracle);
});
Icanthenchangeclient1toalsousethesamefield.
client1…
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
constbaseCharge=aReading.baseCharge;
ThereisagoodchanceIcanthenuseInlineVariable(123)onbaseChargetoo.
NowIturntothetaxableamountcalculation.Myfirststepistoaddinthetransformationfunction.
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);
consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));
Icanimmediatelyreplacethecalculationofthebasechargewiththenewfield.Ifthecalculationwascomplex,IcouldExtractFunction(106)first,buthereit’ssimpleenoughtodoinonestep.
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
constbase=aReading.baseCharge;
consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));
OnceI’vetestedthatthatworks,IapplyInlineVariable(123).
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
consttaxableCharge=Math.max(0,aReading.baseCharge-taxThreshold(aReading.year));
Andmovethatcomputationintothetransformer.
functionenrichReading(original){
constresult=_.cloneDeep(original);
result.baseCharge=calculateBaseCharge(result);
result.taxableCharge=Math.max(0,result.baseCharge-taxThreshold(result.year));
returnresult;
}
Imodifytheoriginalcodetousethenewfield.
constrawReading=acquireReading();
constaReading=enrichReading(rawReading);
consttaxableCharge=aReading.taxableCharge;
OnceI’vetestedthat,it’slikelyIwouldbeabletouseInlineVariable(123)ontaxableCharge.
Onebigproblemwithanenrichedreadinglikethisis:Whathappensshouldaclientchangeadatavalue?Changing,say,thequantityfieldwouldresultindatathat’sinconsistent.ToavoidthisinJavaScript,mybestoptionistouseCombineFunctionsintoClass(144)instead.IfI’minalanguagewithimmutabledatastructures,Idon’thavethisproblem,soitsmorecommontoseetransformsinthoselanguages.Buteveninlanguageswithoutimmutability,Icanusetransformsifthedataappearsinaread-onlycontext,suchasderivingdatatodisplayonawebpage.
SplitPhase
Motivation
WhenIrunintocodethat’sdealingwithtwodifferentthings,Ilookforawaytosplititintoseparatemodules.Iendeavortomakethissplitbecause,ifIneedtomakeachange,Icandealwitheachtopicseparatelyandnothavetoholdbothinmyheadtogether.IfI’mlucky,Imayonlyhavetochangeonemodulewithouthavingtorememberthedetailsoftheotheroneatall.
Oneoftheneatestwaystodoasplitlikethisistodividethebehaviorintotwosequentialphases.Agoodexampleofthisiswhenyouhavesomeprocessingwhoseinputsdon’treflectthemodelyouneedtocarryoutthelogic.Beforeyou
begin,youcanmassagetheinputintoaconvenientformforyourmainprocessing.Or,youcantakethelogicyouneedtodoandbreakitdownintosequentialsteps,whereeachstepissignificantlydifferentinwhatitdoes.
Themostobviousexampleofthisisacompiler.It’sabasictaskistotakesometext(codeinaprogramminglanguage)andturnitintosomeexecutableform(e.g.objectcodeforaspecifichardware).Overtime,we’vefoundthiscanbeusefullysplitintoachainofphases:tokenizingthetext,parsingthetokensintoasyntaxtree,thenvariousstepsoftransformingthesyntaxtree(e.g.foroptimization),andfinallygeneratingtheobjectcode.EachstephasalimitedscopeandIcanthinkofonestepwithoutunderstandingthedetailsofothers.
Splittingphaseslikethisiscommoninlargesoftware;thevariousphasesinacompilercaneachcontainmanyfunctionsandclasses.ButIcancarryoutthebasicsplit-phaserefactoringonanyfragmentofcode—wheneverIseeanopportunitytousefullyseparatethecodeintodifferentphases.Thebestclueiswhendifferentstagesofthefragmentusedifferentsetsofdataandfunctions.ByturningthemintoseparatemodulesIcanmakethisdifferenceexplicit,revealingthedifferenceinthecode.
Mechanics
Extractthesecondphasecodeintoitsownfunction.
Test.
Introduceanintermediatedatastructureasanadditionalargumenttotheextractedfunction.
Test.
Examineeachparameteroftheextractedsecondphase.Ifitisusedbyfirstphase,moveittotheintermediatedatastructure.Testaftereachmove.
Sometimes,aparametershouldnotbeusedbythesecondphase.Inthiscase,extracttheresultsofeachusageoftheparameterintoafieldoftheintermediatedatastructureanduseMoveStatementstoCallers(215)onthelinethatpopulatesit.
ApplyExtractFunction(106)onthefirst-phasecode,returningtheintermediatedatastructure.
It’salsoreasonabletoextractthefirstphaseintoatransformerobject.
Example
I’llstartwithcodetopriceanorderforsomevagueandunimportantkindofgoods:
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constshippingPerCase=(basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=quantity*shippingPerCase;
constprice=basePrice-discount+shippingCost;
returnprice;
}
Althoughthisistheusualkindoftrivialexample,thereisasenseoftwophasesgoingonhere.Thefirstcoupleoflinesofcodeusetheproductinformationtocalculatetheproduct-orientedpriceoftheorder,whilethelatercodeusesshippinginformationtodeterminetheshippingcost.IfIhavechangescomingupthatcomplicatethepricingandshippingcalculations,buttheyworkrelativelyindependently,thensplittingthiscodeintotwophasesisvaluable.
IbeginbyapplyingExtractFunction(106)totheshippingcalculation.
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constprice=applyShipping(basePrice,shippingMethod,quantity,discount);
returnprice;
}
functionapplyShipping(basePrice,shippingMethod,quantity,discount){
constshippingPerCase=(basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=quantity*shippingPerCase;
constprice=basePrice-discount+shippingCost;
returnprice;
}
Ipassinallthedatathatthissecondphaseneedsasindividualparameters.Inamorerealisticcase,therecanbealotofthese,butIdon’tworryaboutitasI’llwhittlethemdownlater.
Next,Iintroducetheintermediatedatastructurethatwillcommunicatebetweenthetwophases.
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constpriceData={};
constprice=applyShipping(priceData,basePrice,shippingMethod,quantity,discount);
returnprice;
}
functionapplyShipping(priceData,basePrice,shippingMethod,quantity,discount){
constshippingPerCase=(basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=quantity*shippingPerCase;
constprice=basePrice-discount+shippingCost;
returnprice;
}
Now,IlookatthevariousparameterstoapplyShipping.ThefirstoneisbasePricewhichiscreatedbythefirst-phasecode.SoImovethisintotheintermediatedatastructure,removingitfromtheparameterlist.
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constpriceData={basePrice:basePrice};
constprice=applyShipping(priceData,basePrice,shippingMethod,quantity,discount);
returnprice;
}
functionapplyShipping(priceData,basePrice,shippingMethod,quantity,discount){
constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=quantity*shippingPerCase;
constprice=priceData.basePrice-discount+shippingCost;
returnprice;
}
ThenextparameterinthelistisshippingMethod.ThisoneIleaveasis,sinceitisn’tusedbythefirst-phasecode.
Afterthis,Ihavequantity.Thisisusedbythefirstphasebutnotcreatedbyit,soIcouldactuallyleavethisintheparameterlist.Myusualpreference,however,istomoveasmuchasIcantotheintermediatedatastructure.
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constpriceData={basePrice:basePrice,quantity:quantity};
constprice=applyShipping(priceData,shippingMethod,quantity,discount);
returnprice;
}
functionapplyShipping(priceData,shippingMethod,quantity,discount){
constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=priceData.quantity*shippingPerCase;
constprice=priceData.basePrice-discount+shippingCost;
returnprice;
}
Idothesamewithdiscount.
functionpriceOrder(product,quantity,shippingMethod){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
constpriceData={basePrice:basePrice,quantity:quantity,discount:discount};
constprice=applyShipping(priceData,shippingMethod,discount);
returnprice;
}
functionapplyShipping(priceData,shippingMethod,discount){
constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=priceData.quantity*shippingPerCase;
constprice=priceData.basePrice-priceData.discount+shippingCost;
returnprice;
}
OnceI’vegonethroughallthefunctionparameters,Ihavetheintermediatedatastructurefullyformed.SoIcanextractthefirst-phasecodeintoitsownfunction,returningthisdata.
functionpriceOrder(product,quantity,shippingMethod){
constpriceData=calculatePricingData(product,quantity);
constprice=applyShipping(priceData,shippingMethod);
returnprice;
}
functioncalculatePricingData(product,quantity){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
return{basePrice:basePrice,quantity:quantity,discount:discount};
}
functionapplyShipping(priceData,shippingMethod){
constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=priceData.quantity*shippingPerCase;
constprice=priceData.basePrice-priceData.discount+shippingCost;
returnprice;
}
Ican’tresisttidyingoutthosefinalconstants.
functionpriceOrder(product,quantity,shippingMethod){
constpriceData=calculatePricingData(product,quantity);
returnapplyShipping(priceData,shippingMethod);
}
functioncalculatePricingData(product,quantity){
constbasePrice=product.basePrice*quantity;
constdiscount=Math.max(quantity-product.discountThreshold,0)
*product.basePrice*product.discountRate;
return{basePrice:basePrice,quantity:quantity,discount:discount};
}
functionapplyShipping(priceData,shippingMethod){
constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)
?shippingMethod.discountedFee:shippingMethod.feePerCase;
constshippingCost=priceData.quantity*shippingPerCase;
returnpriceData.basePrice-priceData.discount+shippingCost;
}
Chapter7EncapsulationPerhapsthemostimportantcriteriatobeusedindecomposingmodulesistoidentifysecretsthatmodulesshouldhidefromtherestofthesystem[bib-parnas].Datastructuresarethemostcommonsecrets,andIcanhidedatastructuresbyencapsulatingthemwithEncapsulateRecord(160)andEncapsulateCollection(168).EvenprimitivedatavaluescanbeencapsulatedwithReplacePrimitivewithObject(172)—themagnitudeofsecond-orderbenefitsfromdoingthisoftensurprisespeople.Temporaryvariablesoftengetinthewayofrefactoring—Ihavetoensuretheyarecalcuatedintherightorderandtheirvaluesareavailabletootherpartsofthecodethatneedthem.UsingReplaceTempwithQuery(176)isagreathelphere,particularlywhensplittingupanoverlylongfunction.
Classesweredesignedforinformationhiding.Inthepreviouschapter,IdescribedawaytoformthemwithCombineFunctionsintoClass(144).Thecommonextract/inlineoperationsalsoapplytoclasseswithExtractClass(180)andInlineClass(184).
Aswellashidingtheinternalsofclasses,it’softenusefultohideconnectionsbetweenclasses,whichIcandowithHideDelegate(187).Buttoomuchhidingleadstobloatedinterfaces,soIalsoneeditsreverse:RemoveMiddleMan(190).
Classesandmodulesarethelargestformsofencapsulation,butfunctionsalsoencapsulatetheirimplementation.Sometimes,Imayneedtomakeawholesalechangetoanalgorithm,whichIcandobywrappingitinafunctionwithExtractFunction(106)andapplyingSubstituteAlgorithm(193).
EncapsulateRecord
formerly:ReplaceRecordwithDataClass
Motivation
Recordstructuresareacommonfeatureinprogramminglanguages.Theyprovideanintuitivewaytogrouprelateddatatogether,allowingmetopassmeaningfulunitsofdataratherthanlooseclumps.Butsimplerecordstructureshavedisadvantages.Themostannoyingoneisthattheyforcemetoclearlyseparatewhatisstoredintherecordfromcalculatedvalues.Considerthenotionofaninclusiverangeofintegers.Icanstorethisas{start:1,end:5}or{start:1,length:5}(oreven{end:5,length:5},ifIwanttoflauntmycontrariness).Butwhatevermystore,Iwanttoknowwhatthestart,end,andlengthare.
ThisiswhyIoftenfavorobjectsoverrecordsformutabledata.Withobjects,Icanhidewhatisstoredandprovidemethodsforallthreevalues.Theuseroftheobjectdoesn’tneedtoknoworcarewhichisstoredandwhichiscalculated.This
encapsulationalsohelpswithrenaming:Icanrenamethefieldwhileprovidingmethodsforboththenewandtheoldnames,graduallyupdatingcallersuntiltheyarealldone.
IjustsaidIfavorobjectsformutabledata.IfIhaveanimmutablevalue,Icanjusthaveallthreevaluesinmyrecord,usinganenrichmentstepifnecessary.Similarly,it’seasytocopythefieldwhenrenaming.
Icanhavetwokindsofrecordstructures:thosewhereIdeclarethelegalfieldnamesandthosethatallowmetousewhateverIlike.Thelatterareoftenimplementedthroughalibraryclasscalledsomethinglikehash,map,hashmap,dictionary,orassociativearray.Manylanguagesprovideconvenientsyntaxforcreatinghashmaps,whichmakesthemusefulinmanyprogrammingsituations.Thedownsideofusingthemistheyarearen’texplicitabouttheirfields.TheonlywayIcantelliftheyusestart/endorstart/lengthisbylookingatwheretheyarecreatedandused.Thisisn’taproblemiftheyareonlyusedinasmallsectionofaprogram,butthewidertheirscopeofusage,thegreaterproblemIgetfromtheirimplicitstructure.Icouldrefactorsuchimplicitrecordsintoexplicitones—butifIneedtodothat,I’drathermakethemclassesinstead.
It’scommontopassnestedstructuresoflistsandhashmapswhichareoftenserializedintoformatslikeJSONorXML.Suchstructurescanbeencapsulatedtoo,whichhelpsiftheirformatschangelateronorifI’mconcernedaboutupdatestothedatathatarehardtokeeptrackof.
Mechanics
UseEncapsulateVariable(132)onthevariableholdingtherecord.
Givethefunctionsthatencapsulatetherecordnamesthatareeasilysearchable.
Replacethecontentofthevariablewithasimpleclassthatwrapstherecord.Defineanaccessorinsidethisclassthatreturnstherawrecord.Modifythefunctionsthatencapsulatethevariabletousethisaccessor.
Test.
Providenewfunctionsthatreturntheobjectratherthantherawrecord.
Foreachuseroftherecord,replaceitsuseofafunctionthatreturnstherecordwithafunctionthatreturnstheobject.Useanaccessorontheobjecttogetatthefielddata,creatingthataccessorifneeded.Testaftereachchange.
Ifit’sacomplexrecord,suchasonewithanestedstructure,focusonclientsthatupdatethedatafirst.Considerreturningacopyorread-onlyproxyofthedataforclientsthatonlyreadthedata.
Removetheclass’srawdataaccessorandtheeasilysearchablefunctionsthatreturnedtherawrecord.
Test.
Ifthefieldsoftherecordarethemselvesstructures,considerusingEncapsulateRecordandEncapsulateCollection(168)recursively.
Example
I’llstartwithaconstantthatiswidelyusedacrossaprogram.
constorganization={name:"AcmeGooseberries",country:"GB"};
ThisisaJavaScriptobjectwhichisbeingusedasarecordstructurebyvariouspartsoftheprogram,withaccesseslikethis:
result+=`<h1>${organization.name}</h1>`;
and
organization.name=newName;
ThefirststepisasimpleEncapsulateVariable(132)
functiongetRawDataOfOrganization(){returnorganization;}
examplereader…
result+=`<h1>${getRawDataOfOrganization().name}</h1>`;
examplewriter…
getRawDataOfOrganization().name=newName;
It’snotquiteastandardEncapsulateVariable(132),sinceIgavethegetteranamedeliberatelychosentobebothuglyandeasytosearchfor.ThisisbecauseIintenditslifetobeshort.
Encapsulatingarecordmeansgoingdeeperthanjustthevariableitself;Iwanttocontrolhowit’smanipulated.Icandothisbyreplacingtherecordwithaclass.
classOrganization…
classOrganization{
constructor(data){
this._data=data;
}
}
toplevel
constorganization=newOrganization({name:"AcmeGooseberries",country:"GB"});
functiongetRawDataOfOrganization(){returnorganization._data;}
functiongetOrganization(){returnorganization;}
NowthatIhaveanobjectinplace,Istartlookingattheusersoftherecord.Anyonethatupdatestherecordgetsreplacedwithasetter.
classOrganization…
setname(aString){this._data.name=aString;}
client…
getOrganization().name=newName;
Similarly,Ireplaceanyreaderswiththeappropriategetter.
classOrganization…
getname(){returnthis._data.name;}
client…
result+=`<h1>${getOrganization().name}</h1>`;
AfterI’vedonethat,Icanfollowthroughonmythreattogivetheugly-sounding
functionashortlife.
functiongetRawDataOfOrganization(){returnorganization._data;}
functiongetOrganization(){returnorganization;}
I’dalsobeinclinedtofoldthe_datafielddirectlyintotheobject.
classOrganization{
constructor(data){
this._name=data.name;
this._country=data.country;
}
getname(){returnthis._name;}
setname(aString){this._name=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
Thishastheadvantageofbreakingthelinktotheinputdatarecord.Thismightbeusefulifareferencetoitrunsaround,whichwouldbreakencapsulation.ShouldInotfoldthedataintoindividualfields,Iwouldbewisetocopy_data
whenIassignit.
Example:EncapsulatingaNestedRecord
Theaboveexamplelooksatashallowrecord,butwhatdoIdowithdatathatisdeeplynested,e.g.comingfromaJSONdocument?Thecorerefactoringstepsstillapply,andIhavetobeequallycarefulwithupdates,butIdogetsomeoptionsaroundreads.
Asanexample,hereissomeslightlymorenesteddata:acollectionofcustomers,keptinahashmapindexedbytheircustomerID.
"1920":{
name:"martin",
id:"1920",
usages:{
"2016":{
"1":50,
"2":55,
//remainingmonthsoftheyear
},
"2015":{
"1":70,
"2":63,
//remainingmonthsoftheyear
}
}
},
"38673":{
name:"neal",
id:"38673",
//morecustomersinasimilarform
Withmorenesteddata,readsandwritescanbediggingintothedatastructure.
sampleupdate…
customerData[customerID].usages[year][month]=amount;
sampleread…
functioncompareUsage(customerID,laterYear,month){
constlater=customerData[customerID].usages[laterYear][month];
constearlier=customerData[customerID].usages[laterYear-1][month];
return{laterAmount:later,change:later-earlier};
}
Toencapsulatethisdata,IalsostartwithEncapsulateVariable(132).
functiongetRawDataOfCustomers(){returncustomerData;}
functionsetRawDataOfCustomers(arg){customerData=arg;}
sampleupdate…
getRawDataOfCustomers()[customerID].usages[year][month]=amount;
sampleread…
functioncompareUsage(customerID,laterYear,month){
constlater=getRawDataOfCustomers()[customerID].usages[laterYear][month];
constearlier=getRawDataOfCustomers()[customerID].usages[laterYear-1][month];
return{laterAmount:later,change:later-earlier};
}
Ithenmakeaclassfortheoveralldatastructure.
classCustomerData{
constructor(data){
this._data=data;
}
}
toplevel…
functiongetCustomerData(){returncustomerData;}
functiongetRawDataOfCustomers(){returncustomerData._data;}
functionsetRawDataOfCustomers(arg){customerData=newCustomerData
Themostimportantareatodealwithistheupdates.So,whileIlookatallthecallersofgetRawDataOfCustomers,I’mfocusedonthosewherethedataischanged.Toremindyou,here’stheupdateagain.
sampleupdate…
getRawDataOfCustomers()[customerID].usages[year][month]=amount;
Thegeneralmechanicsnowsaytoreturnthefullcustomeranduseanaccessor,creatingoneifneeded.Idon’thaveasetteronthecustomerforthisupdate,andthisonedigsintothestructure.So,tomakeone,IbeginbyusingExtractFunction(106)onthecodethatdigsintothedatastructure.
sampleupdate…
setUsage(customerID,year,month,amount);
toplevel…
functionsetUsage(customerID,year,month,amount){
getRawDataOfCustomers()[customerID].usages[year][month]=amount;
}
IthenuseMoveFunction(196)tomoveitintothenewcustomerdataclass.
sampleupdate…
getCustomerData().setUsage(customerID,year,month,amount);
classCustomerData…
setUsage(customerID,year,month,amount){
this._data[customerID].usages[year][month]=amount;
}
Whenworkingwithabigdatastructure,Iliketoconcentrateontheupdates.Gettingthemvisibleandgatheredinasingleplaceisthemostimportantpartoftheencapsulation.
Atsomepoint,IwillthinkI’vegotthemall—buthowcanIbesure?There’sacoupleofwaystocheck.OneistomodifygetRawDataOfCustomerstoreturnadeepcopyofthedata;ifmytestcoverageisgood,oneofthetestsshouldbreakifImissedamodification.
toplevel…
functiongetCustomerData(){returncustomerData;}
functiongetRawDataOfCustomers(){returncustomerData.rawData;}
functionsetRawDataOfCustomers(arg){customerData=newCustomerData(arg);}
classCustomerData…
getrawData(){
return_.cloneDeep(this._data);
}
I’musingthelodashlibrarytomakeadeepcopy.
Anotherapproachistoreturnaread-onlyproxyforthedatastructure.Suchaproxycouldraiseanexceptioniftheclientcodetriestomodifytheunderlyingobject.Somelanguagesmakethiseasy,butit’sapaininJavaScript,soI’llleaveitasanexerciseforthereader.Icouldalsotakeacopyandrecursivelyfreezeittodetectanymodifications.
Dealingwiththeupdatesisvaluable,butwhataboutthereaders?Herethereareafewoptions.
ThefirstoptionistodothesamethingasIdidforthesetters.Extractallthereadsintotheirownfunctionsandmovethemintothecustomerdataclass.
classCustomerData…
usage(customerID,year,month){
returnthis._data[customerID].usages[year][month];
}
toplevel…
functioncompareUsage(customerID,laterYear,month){
constlater=getCustomerData().usage(customerID,laterYear,month)
constearlier=getCustomerData().usage(customerID,laterYear-1,month)
return{laterAmount:later,change:later-earlier};
}
ThegreatthingaboutthisapproachisthatitgivescustomerDataanexplicitAPIthatcapturesalltheusesmadeofit.Icanlookattheclassandseealltheirusesofthedata.Butthiscanbealotofcodeforlotsofspecialcases.Modernlanguagesprovidegoodaffordancesfordiggingintoalist-and-hash[bib-list-and-hash]datastructure,soit’susefultogiveclientsjustsuchadatastructuretoworkwith.
Iftheclientwantsadatastructure,Icanjusthandouttheactualdata.Buttheproblemwiththisisthatthere’snowaytopreventclientsfrommodifyingthedatadirectly,whichbreaksthewholepointofencapsulatingalltheupdatesinsidefunctions.Consequently,thesimplestthingtodoistoprovideacopyoftheunderlyingdata,usingtherawDatamethodIwroteearlier.
classCustomerData…
getrawData(){
return_.cloneDeep(this._data);
}
toplevel…
functioncompareUsage(customerID,laterYear,month){
constlater=getCustomerData().rawData[customerID].usages[laterYear][month];
constearlier=getCustomerData().rawData[customerID].usages[laterYear-1][month];
return{laterAmount:later,change:later-earlier};
}
Butalthoughit’ssimple,therearedownsides.Themostobviousproblemisthecostofcopyingalargedatastructure,whichmayturnouttobeaperformanceproblem.Aswithanythinglikethis,however,theperformancecostmightbeacceptable—IwouldwanttomeasureitsimpactbeforeIstarttoworryaboutit.Theremayalsobeconfusionifclientsexpectmodifyingthecopieddatatomodifytheoriginal.Inthosecases,aread-onlyproxyorfreezingthecopieddatamightprovideahelpfulerrorshouldtheydothis.
Anotheroptionismorework,butoffersthemostcontrol:ApplyEncapsulateRecordrecursively.Withthis,Iturnthecustomerrecordintoitsownclass,
applyEncapsulateCollection(168)totheusages,andcreateausageclass.Icanthenenforcecontrolofupdatesbyusingaccessors,perhapsapplyingChangeReferencetoValue(252)ontheusageobjects.Butthiscanbealotofeffortforalargedatastructure—andnotreallyneededifIdon’taccessthatmuchofthedatastructure.Sometimes,ajudiciousmixofgettersandnewclassesmaywork,usingagettertodigdeepintothestructurebutreturninganobjectthatwrapsthestructureratherthantheunencapsulateddata.Iwroteaboutthiskindofthingoninanarticle:RefactoringCodetoLoadaDocument[bib-refact-doc].
EncapsulateCollection
Motivation
Ilikeencapsulatinganymutabledatainmyprograms.Thismakesiteasiertoseewhenandhowdatastructuresaremodified,whichthenmakesiteasiertochangethosedatastructureswhenIneedto.Encapsulationisoftenencouraged,particularlybyobject-orienteddevelopers,butacommonmistakeoccurswhenworkingwithcollections.Accesstoacollectionvariablemaybeencapsulated,butifthegetterreturnsthecollectionitself,thenthatcollection’smembershipcanbealteredwithouttheenclosingclassbeingabletointervene.
Toavoidthis,Iprovidecollectionmodifiermethods—usuallyaddandremove—ontheclassitself.Thisway,changestothecollectiongothroughtheowningclass,givingmetheopportunitytomodifysuchchangesastheprogramevolves.
Ifftheteamhasthehabittonottomodifycollectionsoutsidetheoriginalmodule,justprovidingthesemethodsmaybeenough.However,it’susuallyunwisetorelyonsuchhabits;amistakeherecanleadtobugsthataredifficulttotrackdownlater.Abetterapproachistoensurethatthegetterforthecollectiondoesnotreturntherawcollection,sothatclientscannotaccidentallychangeit.
Onewaytopreventmodificationoftheunderlyingcollectionisbyneverreturningacollectionvalue.Inthisapproach,anyuseofacollectionfieldisdonewithspecificmethodsontheowningclass,replacingaCustomer.orders.sizewithaCustomer.numberOfOrders.Idon’tagreewiththisapproach.Modernlanguageshaverichcollectionclasseswithstandardizedinterfaces,whichcanbecombinedinusefulwayssuchasCollectionPipelines(https://martinfowler.com/bliki/Collection-Pipeline.html).Puttinginspecialmethodstohandlethiskindoffunctionalityaddsalotofextracodeandcripplestheeasycomposabilityofcollectionoperations.
Anotherwayistoallowsomeformofread-onlyaccesstoacollection.Java,forexample,makesiteasytoreturnaread-onlyproxytothecollection.Suchaproxyforwardsallreadstotheunderlyingcollection,butblocksallwrites—inJava’scase,throwinganexception.Asimilarrouteisusedbylibrariesthatbasetheircollectioncompositiononsomekindofiteratororenumerableobject—providingthatiteratorcannotmodifytheunderlyingcollection.
Probablythemostcommonapproachistoprovideagettingmethodforthecollection,butmakeitreturnacopyoftheunderlyingcollection.Thatway,anymodificationstothecopydon’taffecttheencapsulatedcollection.Thismightcausesomeconfusionifprogrammersexpectthereturnedcollectiontomodifythesourcefield—butinmanycodebases,programmersareusedtocollectiongettersprovidingcopies.Ifthecollectionishuge,thismaybeaperformanceissue—butmostlistsaren’tallthatbig,sothegeneralrulesforperformanceshouldapply.(RefactoringandPerformance,p.62)
Anotherdifferencebetweenusingaproxyandacopyisthatamodificationofthesourcedatawillbevisibleintheproxybutnotinacopy.Thisisn’tanissuemostofthetime,becauselistsaccessedinthiswayareusuallyonlyheldfora
shorttime.
What’simportanthereisconsistencywithinacodebase.Useonlyonemechanismsoeveryonecangetusedtohowitbehavesandexpectitwhencallinganycollectionaccessorfunction.
Mechanics
ApplyEncapsulateVariable(132)ifthereferencetothecollectionisn’talreadyencapsulated.
Addfunctionstoaddandremoveelementsfromthecollection.
Ifthereisasetterforthecollection,useRemoveSettingMethod(329)ifpossible.Ifnot,makeittakeacopyoftheprovidedcollection.
Runstaticchecks.
Findallreferencestothecollection.Ifanyonecallsmodifiersonthecollection,changethemtousethenewadd/removefunctions.Testaftereachchange.
Modifythegetterforthecollectiontoreturnaprotectedviewonit,usingaread-onlyproxyoracopy.
Test.
Example
Istartwithapersonclassthathasafieldforalistofcourses.
classPerson…
constructor(name){
this._name=name;
this._courses=[];
}
getname(){returnthis._name;}
getcourses(){returnthis._courses;}
setcourses(aList){this._courses=aList;}
classCourse…
constructor(name,isAdvanced){
this._name=name;
this._isAdvanced=isAdvanced;
}
getname(){returnthis._name;}
getisAdvanced(){returnthis._isAdvanced;}
Clientsusethecoursecollectiontogatherinformationoncourses.
numAdvancedCourses=aPerson.courses
.filter(c=>c.isAdvanced)
.length
;
Anaivedeveloperwouldsaythisclasshasproperdataencapsulation:Afterall,eachfieldisprotectedbyaccessormethods.ButIwouldarguethatthelistofcoursesisn’tproperlyencapsulated.Certainly,anyoneupdatingthecoursesasasinglevaluehaspropercontrolthroughthesetter:
clientcode…
constbasicCourseNames=readBasicCourseNames(filename);
aPerson.courses=basicCourseNames.map(name=>newCourse(name,false));
Butclientsmightfinditeasiertoupdatethecourselistdirectly.
clientcode…
for(constnameofreadBasicCourseNames(filename)){
aPerson.courses.push(newCourse(name,false));
}
Thisviolatesencapsulatingbecausethepersonclasshasnoabilitytotakecontrolwhenthelistisupdatedinthisway.Whilethereferencetothefieldisencapsulated,thecontentofthefieldisnot.
I’llbegincreatingproperencapsulationbyaddingmethodstothepersonclassthatallowaclienttoaddandremoveindividualcourses.
classPerson…
addCourse(aCourse){
this._courses.push(aCourse);
}
removeCourse(aCourse,fnIfAbsent=()=>{thrownewRangeError();}){
constindex=this._courses.indexOf(aCourse);
if(index===-1)fnIfAbsent();
elsethis._courses.splice(index,1);
}
Witharemoval,Ihavetodecidewhattodoifaclientaskstoremoveanelementthatisn’tinthecollection.Icaneithershrug,orraiseanerror.Withthiscode,Idefaulttoraisinganerror,butgivethecallersanopportunitytodosomethingelseiftheywish.
Ithenchangeanycodethatcallsmodifiersdirectlyonthecollectiontousenewmethods.
clientcode…
for(constnameofreadBasicCourseNames(filename)){
aPerson.addCourse(newCourse(name,false));
}
Withindividualaddandremovemethods,thereisusuallynoneedforsetCourses,inwhichcaseI’lluseRemoveSettingMethod(329)onit.ShouldtheAPIneedasettingmethodforsomereason,Iensureitputsacopyofthecollectioninthefield.
classPerson…
setcourses(aList){this._courses=aList.slice();}
Allthisenablestheclientstousetherightkindofmodifiermethods,butIprefertoensurenobodymodifiesthelistwithoutusingthem.Icandothisbyprovidingacopy.
classPerson…
getcourses(){returnthis._courses.slice();}
Ingeneral,IfinditwisetobemoderatelyparanoidaboutcollectionsandI’drathercopythemunnecessarilythandebugerrorsduetounexpectedmodifications.Modificationsaren’talwaysobvious;forexample,sortinganarrayinJavaScriptmodifiestheoriginal,whilemanylanguagesdefaulttomakingacopyforanoperationthatchangesacollection.Anyclassthat’s
responsibleformanagingacollectionshouldalwaysgiveoutcopies—butIalsogetintothehabitofmakingacopyifIdosomethingthat’sliabletochangeacollection.
ReplacePrimitivewithObject
formerly:ReplaceDataValuewithObject
formerly:ReplaceTypeCodewithClass
Motivation
Often,inearlystagesofdevelopmentyoumakedecisionsaboutrepresentingsimplefactsassimpledataitems,suchasnumbersorstrings.Asdevelopmentproceeds,thosesimpleitemsaren’tsosimpleanymore.Atelephonenumbermayberepresentedasastringforawhile,butlateritwillneedspecialbehaviorforformatting,extractingtheareacode,andthelike.Thiskindoflogiccanquicklyendupbeingduplicatedaroundthecodebase,increasingtheeffortwheneveritneedstobeused.
AssoonasIrealizeIwanttodosomethingotherthansimpleprinting,Iliketocreateanewclassforthatbitofdata.Atfirst,suchaclassdoeslittlemorethanwraptheprimitive—butonceIhavethatclass,Ihaveaplacetoputbehavior
specifictoitsneeds.Theselittlevaluesstartveryhumble,butoncenurturedtheycangrowintousefultools.Theymaynotlooklikemuch,butIfindtheireffectsonacodebasecanbesurprisinglylarge.Indeedmanyexperienceddevelopersconsiderthistobeoneofthemostvaluablerefactoringsinthetoolkit—eventhoughitoftenseemscounter-intuitivetoanewprogrammer.
Mechanics
ApplyEncapsulateVariable(132)ifitisn’talready.
Createasimplevalueclassforthedatavalue.Itshouldtaketheexistingvalueinitsconstructorandprovideagetterforthatvalue.
Runstaticchecks.
Changethesettertocreateanewinstanceofthevalueclassandstorethatinthefield,changingthetypeofthefieldifpresent.
Changethegettertoreturntheresultofinvokingthegetterofthenewclass.
Test.
ConsiderusingChangeFunctionDeclaration(124)ontheoriginalaccessorstobetterreflectwhattheydo.
ConsiderclarifyingtheroleofthenewobjectasavalueorreferenceobjectbyapplyingChangeReferencetoValue(252)orChangeValuetoReference(256).
Example
Ibeginwithasimpleorderclassthatreadsitsdatafromasimplerecordstructure.Oneofitspropertiesisapriority,whichitreadsasasimplestring.
classOrder…
constructor(data){
this.priority=data.priority;
//moreinitialization
Someclientcodesusesitlikethis:
client…
highPriorityCount=orders.filter(o=>"high"===o.priority
||"rush"===o.priority)
.length;
WheneverI’mfiddlingwithadatavalue,thefirstthingIdoisuseEncapsulateVariable(132)onit.
classOrder…
getpriority(){returnthis._priority;}
setpriority(aString){this._priority=aString;}
TheconstructorlinethatinitializestheprioritywillnowusethesetterIdefinehere.
Thisself-encapsulatesthefieldsoIcanpreserveitscurrentusewhileImanipulatethedataitself.
Icreateasimplevalueclassforthepriority.Ithasaconstructorforthevalueandaconversionfunctiontoreturnastring.
classPriority{
constructor(value){this._value=value;}
toString(){returnthis._value;}
}
Ipreferusingaconversionfunction(toString)ratherthanagetter(value)here.Forclientsoftheclass,askingforthestringrepresentationshouldfeelmorelikeaconversionthangettingaproperty.
Ithenmodifytheaccessorstousethisnewclass.
classOrder…
getpriority(){returnthis._priority.toString();}
setpriority(aString){this._priority=newPriority(aString);}
NowthatIhaveapriorityclass,Ifindthecurrentgetterontheordertobemisleading.Itdoesn’treturnthepriority—butastringthatdescribesthepriority.MyimmediatemoveistouseChangeFunctionDeclaration(124).
classOrder…
getpriorityString(){returnthis._priority.toString();}
setpriority(aString){this._priority=newPriority(aString);}
client…
highPriorityCount=orders.filter(o=>"high"===o.priorityString
||"rush"===o.priorityString)
.length;
Inthiscase,I’mhappytoretainthenameofthesetter.Thenameoftheargumentcommunicateswhatitexpects.
NowI’mdonewiththeformalrefactoring.ButasIlookatwhousesthepriority,Iconsiderwhethertheyshouldusethepriorityclassthemselves.Asaresult,Iprovideagetteronorderthatprovidesthenewpriorityobjectdirectly.
classOrder…
getpriority(){returnthis._priority;}
getpriorityString(){returnthis._priority.toString();}
setpriority(aString){this._priority=newPriority(aString);}
client…
highPriorityCount=orders.filter(o=>"high"===o.priority.toString()
||"rush"===o.priority.toString())
.length;
Asthepriorityclassbecomesusefulelsewhere,Iwouldallowclientsoftheordertousethesetterwithapriorityinstance,whichIdobyadjustingthepriorityconstructor.
classPriority…
constructor(value){
if(valueinstanceofPriority)returnvalue;
this._value=value;
}
Thepointofallthisisthatnow,mynewpriorityclasscanbeusefulasaplacefornewbehavior—eithernewtothecodeormovedfromelsewhere.Here’ssomesimplecodetoaddvalidationofpriorityvaluesandcomparisonlogic.
classPriority…
constructor(value){
if(valueinstanceofPriority)returnvalue;
if(Priority.legalValues().includes(value))
this._value=value;
else
thrownewError(`<${value}>isinvalidforPriority`);
}
toString(){returnthis._value;}
get_index(){returnPriority.legalValues().findIndex(s=>s===this._value);}
staticlegalValues(){return['low','normal','high','rush'];}
equals(other){returnthis._index===other._index;}
higherThan(other){returnthis._index>other._index;}
lowerThan(other){returnthis._index<other._index;}
AsIdothis,Idecidethatapriorityshouldbeavalueobject,soIprovideanequalsmethodandensurethatitisimmutable.
NowI’veaddedthatbehavior,Icanmaketheclientcodemoremeaningful:
client…
highPriorityCount=orders.filter(o=>o.priority.higherThan(newPriority("normal"))
.length;
ReplaceTempwithQuery
Motivation
Oneuseoftemporaryvariablesistocapturethevalueofsomecodeinordertorefertoitlaterinafunction.Usingatempallowsmetorefertothevaluewhileexplainingitsmeaningandavoidingrepeatingthecodethatcalculatesit.Butwhileusingavariableishandy,itcanoftenbeworthwhiletogoastepfurtheranduseafunctioninstead.
IfI’mworkingonbreakingupalargefunction,turningvariablesintotheirownfunctionsmakesiteasiertoextractpartsofthefunction,sinceInolongerneedtopassinvariablesintotheextractedfunctions.Puttingthislogicintofunctionsoftenalsosetsupastrongerboundarybetweentheextractedlogicandtheoriginalfunction,whichhelpsmespotandavoidawkwarddependenciesandsideeffects.
Usingfunctionsinsteadofvariablesalsoallowsmetoavoidduplicatingthecalculationlogicinsimilarfunctions.WheneverIseevariablescalculatedinthesamewayindifferentplaces,Ilooktoturnthemintoasinglefunction.
ThisrefactoringworksbestifI’minsideaclass,sincetheclassprovidesasharedcontextforthemethodsI’mextracting.Outsideofaclass,I’mliabletohavetoomanyparametersinatop-levelfunctionwhichnegatesmuchofthebenefitofusingafunction.Nestedfunctioncanavoidthis,buttheylimitmyabilitytosharethelogicbetweenrelatedfunctions.
OnlysometemporaryvariablesaresuitableforReplaceTempwithQuery.Thevariableneedstobecalculatedonceandthenonlybereadafterwards.Inthesimplestcase,thismeansthevariableisassignedtoonce,butit’salsopossibletohaveseveralassignmentsinamorecomplicatedlumpofcode—allofwhichhastobeextractedintothequery.Furthermore,thelogicusedtocalculatethevariablemustyieldthesameresultwhenthevariableisusedlater—whichrulesoutvariablesusedassnapshotswithnameslikeoldAddress.
Mechanics
Checkthatthevariableisdeterminedentirelybeforeit’sused,andthecodethatcalculatesitdoesnotyieldadifferentvaluewheneveritisused.
Ifthevariableisn’tread-only,andcanbemaderead-only,doso.
Test.
Extracttheassignmentofthevariableintoafunction.
Ifthevariableandthefunctioncannotshareaname,useatemporarynameforthefunction.
Ensuretheextractedfunctionisfreeofsideeffects.Ifnot,useSeparateQueryfromModifier(304).
Test.
UseInlineVariable(123)toremovethetemp.
Example
Hereisasimpleclass.
classOrder…
constructor(quantity,item){
this._quantity=quantity;
this._item=item;
}
getprice(){
varbasePrice=this._quantity*this._item.price;
vardiscountFactor=0.98;
if(basePrice>1000)discountFactor-=0.03;
returnbasePrice*discountFactor;
}
}
IwanttoreplacethetempsbasePriceanddiscountFactorwithmethods.
StartingwithbasePrice,Imakeitconstandruntests.ThisisagoodwayofcheckingthatIhaven’tmissedareassignment—unlikelyinsuchashortfunctionbutcommonwhenI’mdealingwithsomethinglarger.
classOrder…
constructor(quantity,item){
this._quantity=quantity;
this._item=item;
}
getprice(){
constbasePrice=this._quantity*this._item.price;
vardiscountFactor=0.98;
if(basePrice>1000)discountFactor-=0.03;
returnbasePrice*discountFactor;
}
}
Ithenextracttheright-hand-sideoftheassignmenttoagettingmethod.
classOrder…
getprice(){
constbasePrice=this.basePrice;
vardiscountFactor=0.98;
if(basePrice>1000)discountFactor-=0.03;
returnbasePrice*discountFactor;
}
getbasePrice(){
returnthis._quantity*this._item.price;
}
Itest,andapplyInlineVariable(123)
classOrder…
getprice(){
constbasePrice=this.basePrice;
vardiscountFactor=0.98;
if(this.basePrice>1000)discountFactor-=0.03;
returnthis.basePrice*discountFactor;
}
IthenrepeatthestepswithdiscountFactor,firstusingExtractFunction(106).
classOrder…
getprice(){
constdiscountFactor=this.discountFactor;
returnthis.basePrice*discountFactor;
}
getdiscountFactor(){
vardiscountFactor=0.98;
if(this.basePrice>1000)discountFactor-=0.03;
returndiscountFactor;
}
InthiscaseIneedmyextractedfunctiontocontainbothassignmentstodiscountFactor.Icanalsosettheoriginalvariabletobeconst.
Then,Iinline:
getprice(){
returnthis.basePrice*this.discountFactor;
}
ExtractClass
Motivation
You’veprobablyreadguidelinesthataclassshouldbeacrispabstraction,onlyhandleafewclearresponsibilities,andsoon.Inpractice,classesgrow.Youaddsomeoperationshere,abitofdatathere.Youaddaresponsibilitytoaclassfeelingthatit’snotworthaseparateclass—butasthatresponsibilitygrowsandbreeds,theclassbecomestoocomplicated.Soon,yourclassisascrispasamicrowavedduck.
Imagineaclasswithmanymethodsandquitealotofdata.Aclassthatistoobigtounderstandeasily.Youneedtoconsiderwhereitcanbesplit—andsplitit.Agoodsigniswhenasubsetofthedataandasubsetofthemethodsseemtogotogether.Othergoodsignsaresubsetsofdatathatusuallychangetogetherorareparticularlydependentoneachother.Ausefultestistoaskyourselfwhatwouldhappenifyouremoveapieceofdataoramethod.Whatotherfieldsandmethodswouldbecomenonsense?
Onesignthatoftencropsuplaterindevelopmentisthewaytheclassissub-typed.Youmayfindthatsubtypingaffectsonlyafewfeaturesorthatsomefeaturesneedtobesubtypedonewayandotherfeaturesadifferentway.
Mechanics
Decidehowtosplittheresponsibilitiesoftheclass.
Createanewchildclasstoexpressthesplit-offresponsibilities.
Iftheresponsibilitiesoftheoriginalparentclassnolongermatchitsname,renametheparent.
Createaninstanceofthechildclasswhenconstructingtheparentandaddalinkfromparenttochild.
UseMoveField(205)oneachfieldyouwishtomove.Testaftereachmove.
UseMoveFunction(196)tomovemethodstothenewchild.Startwithlower-levelmethods(thosebeingcalledratherthancalling).Testaftereachmove.
Reviewtheinterfacesofbothclasses,removeunneededmethods,changenamestobetterfitthenewcircumstances.
Decidewhethertoexposethenewchild.Ifso,considerapplyingChangeReferencetoValue(252)tothechildclass.
Example
Istartwithasimplepersonclass:
classPerson…
getname(){returnthis._name;}
setname(arg){this._name=arg;}
gettelephoneNumber(){return`(${this.officeAreaCode})${this.officeNumber}`;}
getofficeAreaCode(){returnthis._officeAreaCode;}
setofficeAreaCode(arg){this._officeAreaCode=arg;}
getofficeNumber(){returnthis._officeNumber;}
setofficeNumber(arg){this._officeNumber=arg;}
Here.Icanseparatethetelephonenumberbehaviorintoitsownclass.Istartbydefininganemptytelephonenumberclass:
classTelephoneNumber{
}
Thatwaseasy!Next,Icreateaninstanceoftelephonenumberwhenconstructingtheperson:
classPerson…
constructor(){
this._telephoneNumber=newTelephoneNumber();
}
classTelephoneNumber…
getofficeAreaCode(){returnthis._officeAreaCode;}
setofficeAreaCode(arg){this._officeAreaCode=arg;}
IthenuseMoveField(205)ononeofthefields.
classPerson…
getofficeAreaCode(){returnthis._telephoneNumber.officeAreaCode;}
setofficeAreaCode(arg){this._telephoneNumber.officeAreaCode=arg;}
Itest,thenmovethenextfield.
classTelephoneNumber…
getofficeNumber(){returnthis._officeNumber;}
setofficeNumber(arg){this._officeNumber=arg;}
classPerson…
getofficeNumber(){returnthis._telephoneNumber.officeNumber;}
setofficeNumber(arg){this._telephoneNumber.officeNumber=arg;}
Testagain,thenmovethetelephonenumbermethod.
classTelephoneNumber…
gettelephoneNumber(){return`(${this.officeAreaCode})${this.officeNumber}`;}
classPerson…
gettelephoneNumber(){returnthis._telephoneNumber.telephoneNumber;
NowIshouldtidythingsup.Having“office”aspartofthetelephonenumbercodemakesnosense,soIrenamethem.
classTelephoneNumber…
getareaCode(){returnthis._areaCode;}
setareaCode(arg){this._areaCode=arg;}
getnumber(){returnthis._number;}
setnumber(arg){this._number=arg;}
classPerson…
getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}
setofficeAreaCode(arg){this._telephoneNumber.areaCode=arg;}
getofficeNumber(){returnthis._telephoneNumber.number;}
setofficeNumber(arg){this._telephoneNumber.number=arg;}
Thetelephonenumbermethodonthetelephonenumberclassalsodoesn’tmakemuchsense,soIapplyChangeFunctionDeclaration(124).
classTelephoneNumber…
toString(){return`(${this.areaCode})${this.number}`;}
classPerson…
gettelephoneNumber(){returnthis._telephoneNumber.toString();}
Telephonenumbersaregenerallyuseful,soIthinkI’llexposethenewobjecttoclients.Icanreplacethose“office”methodswithaccessorsforthetelephonenumber.Butthisway,thetelephonenumberwillworkbetterasaValueObject(https://martinfowler.com/bliki/ValueObject.html),soIwouldapplyChangeReferencetoValue(252)first(thatrefactoring’sexampleshowshowI’ddothatforthetelephonenumber).
InlineClass
Motivation
InlineClassistheinverseofExtractClass(180).IuseInlineClassifaclassisnolongerpullingitsweightandshouldn’tbearoundanymore.Often,thisistheresultofrefactoringthatmovesotherresponsibilitiesoutoftheclasssothereislittleleft.Atthatpoint,Ifoldtheclassintoanother—onethatmakesmostuseoftheruntclass.
AnotherreasontouseInlineClassisifIhavetwoclassesthatIwanttorefactorintoapairofclasseswithadifferentallocationoffeatures.ImayfinditeasiertofirstuseInlineClasstocombinethemintoasingleclass,thenExtractClass(180)tomakethenewseparation.Thisisageneralapproachwhenreorganizingthings:Sometimes,it’seasiertomoveelementsoneatatimefromonecontexttoanother,butsometimesit’sbettertouseaninlinerefactoringtocollapsethecontextstogether,thenuseanextractrefactoringtoseparatethemintodifferentelements.
Mechanics
Inthetargetclass,createfunctionsforallthepublicfunctionsofthesourceclass.Thesefunctionsshouldjustdelegatetothesourceclass.
Changeallreferencestosourceclassmethodssotheyusethetargetclass’sdelegatorsinstead.Testaftereachchange.
Moveallthefunctionsanddatafromthesourceclassintothetarget,testingaftereachmove,untilthesourceclassisempty.
Deletethesourceclassandholdashort,simplefuneralservice.
Example
Here’saclassthatholdsacoupleofpiecesoftrackinginformationforashipment.
classTrackingInformation{
getshippingCompany(){returnthis._shippingCompany;}
setshippingCompany(arg){this._shippingCompany=arg;}
gettrackingNumber(){returnthis._trackingNumber;}
settrackingNumber(arg){this._trackingNumber=arg;}
getdisplay(){
return`${this.shippingCompany}:${this.trackingNumber}`;
}
}
It’susedaspartofashipmentclass.
classShipment…
gettrackingInfo(){
returnthis._trackingInformation.display;
}
gettrackingInformation(){returnthis._trackingInformation;}
settrackingInformation(aTrackingInformation){
this._trackingInformation=aTrackingInformation;
}
Whilethisclassmayhavebeenworthwhileinthepast,Inolongerfeelit’spullingitsweight,soIwanttoinlineitintoShipment.
IstartbylookingatplacesthatareinvokingthemethodsofTrackingInformation.
caller…
aShipment.trackingInformation.shippingCompany=request.vendor;
I’mgoingtomoveallsuchfunctionstoShipment,butIdoitslightlydifferentlytohowIusuallydoMoveFunction(196).Inthiscase,Istartbyputtingadelegatingmethodintotheshipment,andadjustingtheclienttocallthat.
classShipment…
setshippingCompany(arg){this._trackingInformation.shippingCompany=arg;}
caller…
aShipment.trackingInformation.shippingCompany=request.vendor;
Idothisforalltheelementsoftrackinginformationthatareusedbyclients.OnceI’vedonethat,Icanmovealltheelementsofthetrackinginformationoverintotheshipmentclass.
IstartbyapplyingInlineFunction(115)tothedisplaymethod.
classShipment…
gettrackingInfo(){
return`${this.shippingCompany}:${this.trackingNumber}`;
}
Imovetheshippingcompanyfield.
getshippingCompany(){returnthis._trackingInformation._shippingCompany;}
setshippingCompany(arg){this._trackingInformation._shippingCompany=arg;}
Idon’tusethefullmechanicsforMoveField(205)sinceinthiscaseIonlyreferenceshippingCompanyfromShipmentwhichisthetargetofthemove.Ithusdon’tneedthestepsthatputareferencefromthesourcetothetarget.
Icontinueuntileverythingismovedover.OnceI’vedonethat,Icandeletethetrackinginformationclass.
classShipment…
gettrackingInfo(){
return`${this.shippingCompany}:${this.trackingNumber}`;
}
getshippingCompany(){returnthis._shippingCompany;}
setshippingCompany(arg){this._shippingCompany=arg;}
gettrackingNumber(){returnthis._trackingNumber;}
settrackingNumber(arg){this._trackingNumber=arg;}
HideDelegate
inverseof:RemoveMiddleMan(190)
Motivation
Oneofthekeys—ifnotthekey—togoodmodulardesignisencapsulation.Encapsulationmeansthatmodulesneedtoknowlessaboutotherpartsofthesystem.Then,whenthingschange,fewermodulesneedtobetoldaboutthechange—whichmakesthechangeeasiertomake.
Whenwearefirsttaughtaboutobjectorientation,wearetoldthatencapsulationmeanshidingourfields.Aswebecomemoresophisticated,werealizethereismorethatwecanencapsulate.
IfIhavesomeclientcodethatcallsamethoddefinedonanobjectinafieldofaserverobject,theclientneedstoknowaboutthisdelegateobject.Ifthedelegatechangesitsinterface,changespropagatetoalltheclientsoftheserverthatusethedelegate.Icanremovethisdependencybyplacingasimpledelegatingmethodontheserverthathidesthedelegate.ThenanychangesImaketothedelegatepropagateonlytotheserverandnottotheclients.
Mechanics
Foreachmethodonthedelegate,createasimpledelegatingmethodontheserver.
Adjusttheclienttocalltheserver.Testaftereachchange.
Ifnoclientneedstoaccessthedelegateanymore,removetheserver’saccessorforthedelegate.
Test.
Example
Istartwithapersonandadepartment
classPerson…
constructor(name){
this._name=name;
}
getname(){returnthis._name;}
getdepartment(){returnthis._department;}
setdepartment(arg){this._department=arg;}
classDepartment…
getchargeCode(){returnthis._chargeCode;}
setchargeCode(arg){this._chargeCode=arg;}
getmanager(){returnthis._manager;}
setmanager(arg){this._manager=arg;}
Someclientcodewantstoknowthemanagerofaperson.Todothis,itneedstogetthedepartmentfirst.
clientcode…
manager=aPerson.department.manager;
Thisrevealstotheclienthowthedepartmentclassworksandthatthedepartmentisresponsiblefortrackingthemanager.Icanreducethiscouplingbyhidingthedepartmentclassfromtheclient.Idothisbycreatingasimpledelegatingmethodonperson:
classPerson…
getmanager(){returnthis._department.manager;}
Inowneedtochangeallclientsofpersontousethisnewmethod:
clientcode…
manager=aPerson.department.manager;
OnceI’vemadethechangeforallmethodsofdepartmentandforalltheclientsofperson,Icanremovethedepartmentaccessoronperson.
RemoveMiddleMan
inverseof:HideDelegate(187)
Motivation
InthemotivationforHideDelegate(187),Italkedabouttheadvantagesofencapsulatingtheuseofadelegatedobject.Thereisapriceforthis.Everytimetheclientwantstouseanewfeatureofthedelegate,Ihavetoaddasimpledelegatingmethodtotheserver.Afteraddingfeaturesforawhile,Igetirritatedwithallthisforwarding.Theserverclassisjustamiddleman(MiddleMan,p.79),andperhapsit’stimefortheclienttocallthedelegatedirectly.(Thissmelloftenpopsupwhenpeoplegetover-enthusiasticaboutfollowingtheLawofDemeter,whichI’dlikealotmoreifitwerecalledtheOccasionallyUsefulSuggestionofDemeter.)
It’shardtofigureoutwhattherightamountofhidingis.Fortunately,withHideDelegate(187)andRemoveMiddleMan,itdoesn’tmattersomuch.Icanadjustmycodeastimegoeson.Asthesystemchanges,thebasisforhowmuchIhidealsochanges.Agoodencapsulationsixmonthsagomaybeawkwardnow.RefactoringmeansIneverhavetosayI’msorry—Ijustfixit.
Mechanics
Createagetterforthedelegate.
Foreachclientuseofadelegatingmethod,replacethecalltothedelegatingmethodbychainingthroughtheaccessor.Testaftereachreplacement.
Ifallcallstoadelegatingmethodarereplaced,youcandeletethedelegatingmethod.
Withautomatedrefactorings,youcanuseEncapsulateVariable(132)onthedelegatefieldandthenInlineFunction(115)onallthemethodsthatuseit.
Example
Ibeginwithapersonclassthatusesalinkeddepartmentobjecttodetermineamanager.(Ifyou’rereadingthisbooksequentially,thisexamplemaylookeerilyfamiliar.)
clientcode…
manager=aPerson.manager;
classPerson…
getmanager(){returnthis._department.manager;}
classDepartment…
getmanager(){returnthis._manager;}
Thisissimpletouseandencapsulatesthedepartment.However,iflotsofmethodsaredoingthis,Iendupwithtoomanyofthesesimpledelegationsontheperson.That’swhenitisgoodtoremovethemiddleman.First,Imakeanaccessorforthedelegate:
classPerson…
getdepartment(){returnthis._department;}
NowIgotoeachclientatatimeandmodifythemtousethedepartment
directly.
clientcode…
manager=aPerson.department.manager;
OnceI’vedonethiswithalltheclients,IcanremovethemanagermethodfromPerson.IcanrepeatthisprocessforanyothersimpledelegationsonPerson.
Icandoamixturehere.SomedelegationsmaybesocommonthatI’dliketokeepthemtomakeclientcodeeasiertoworkwith.ThereisnoabsolutereasonwhyIshouldeitherhideadelegateorremoveamiddleman—particularcircumstancessuggestwhichapproachtotake,andreasonablepeoplecandifferonwhatworksbest.
IfIhaveautomatedrefactorings,thenthere’sausefulvariationonthesesteps.First,IuseEncapsulateVariable(132)ondepartment.Thischangesthemanagergettertousethepublicdepartmentgetter:
classPerson…
getmanager(){returnthis.department.manager;}
ThechangeisrathertoosubtleinJavaScript,butbyremovingtheunderscorefromdepartmentI’musingthenewgetterratherthanaccessingthefielddirectly.
ThenIapplyInlineFunction(115)onthemanagermethodtoreplaceallthecallersatonce.
SubstituteAlgorithm
Motivation
I’venevertriedtoskinacat.I’mtoldthereareseveralwaystodoit.I’msuresomeareeasierthanothers.Soitiswithalgorithms.IfIfindaclearerwaytodosomething,Ireplacethecomplicatedwaywiththeclearerway.Refactoringcanbreakdownsomethingcomplexintosimplerpieces,butsometimesIjustreachthepointatwhichIhavetoremovethewholealgorithmandreplaceitwithsomethingsimpler.ThisoccursasIlearnmoreabouttheproblemandrealizethatthere’saneasierwaytodoit.ItalsohappensifIstartusingalibrarythatsuppliesfeaturesthatduplicatemycode.
Sometimes,whenIwanttochangethealgorithmtoworkslightlydifferently,it’seasiertostartbyreplacingitwithsomethingthatwouldmakemychangemorestraightforwardtomake.
WhenIhavetotakethisstep,IhavetobesureI’vedecomposedthemethodasmuchasIcan.Replacingalarge,complexalgorithmisverydifficult;onlybymakingitsimplecanImakethesubstitutiontractable.
Mechanics
Arrangethecodetobereplacedsothatitfillsacompletefunction.
Preparetestsusingthisfunctiononly,tocaptureitsbehavior.
Prepareyouralternativealgorithm.
Runstaticchecks.
Runteststocomparetheoutputoftheoldalgorithmtothenewone.Iftheyarethesame,you’redone.Otherwise,usetheoldalgorithmforcomparisonintestinganddebugging.
Chapter8MovingFeaturesSofar,therefactoringshavebeenaboutcreating,removing,andrenamingprogramelements.Anotherimportantpartofrefactoringismovingelementsbetweencontexts.IuseMoveFunction(196)tomovefunctionsbetweenclassesandothermodules.Fieldscanmovetoo,withMoveField(205).
Ialsomoveindividualstatementsaround.IuseMoveStatementsintoFunction(211)andMoveStatementstoCallers(215)tomovetheminoroutoffunctions,aswellasSlideStatements(221)tomovethemwithinafunction.Sometimes,IcantakesomestatementsthatmatchanexisitingfunctionanduseReplaceInlineCodewithFunctionCall(220)toremovetheduplication.
TworefactoringsIoftendowithloopsareSplitLoop(226),toensurealoopdoesonlyonething,andReplaceLoopwithPipeline(230)togetridofaloopentirely.
Andthenthere’sthefavoriterefactoringofmanyafineprogrammer:RemoveDeadCode(236).Nothingisassatisfyingasapplyingthedigitalflamethrowertosuperfluousstatements.
MoveFunction
formerly:MoveMethod
Motivation
Theheartofagoodsoftwaredesignisitsmodularity—whichismyabilitytomakemostmodificationstoaprogramwhileonlyhavingtounderstandasmallpartofit.Togetthismodularity,Ineedtoensurethatrelatedsoftwareelementsaregroupedtogetherandthelinksbetweenthemareeasytofindandunderstand.Butmyunderstandingofhowtodothisisn’tstatic—asIbetterunderstandwhatI’mdoing,Ilearnhowtobestgrouptogethersoftwareelements.Toreflectthatgrowingunderstanding,Ineedtomoveelementsaround.
Allfunctionsliveinsomecontext;itmaybeglobal,butusuallyit’ssomeformofamodule.Inanobject-orientedprogram,thecoremodularcontextisaclass.Nestingafunctionwithinanothercreatesanothercommoncontext.Differentlanguagesprovidevariedformsofmodularity,eachcreatingacontextforafunctiontolivein.
Oneofthemoststraightforwardreasonstomoveafunctioniswhenitreferenceselementsinothercontextsmorethantheoneitcurrentlyresidesin.Movingittogetherwiththoseelementsoftenimprovesencapsulation,allowingotherpartsofthesoftwaretobelessdependentonthedetailsofthismodule.
Similarly,Imaymoveafunctionbecauseofwhereitscallerslive,orwhereIneedtocallitfrominmynextenhancement.Afunctiondefinedasahelperinsideanotherfunctionmayhavevalueonitsown,soit’sworthmovingittosomewheremoreaccessible.Amethodonaclassmaybeeasierformetouseifshiftedtoanother.
Decidingtomoveafunctionisrarelyaneasydecision.Tohelpmedecide,Iexaminethecurrentandcandidatecontextsforthatfunction.Ineedtolookatwhatfunctionscallthisone,whatfunctionsarecalledbythemovingfunction,andwhatdatathatfunctionuses.Often,IseethatIneedanewcontextforagroupoffunctionsandcreateonewithCombineFunctionsintoClass(144)orExtractClass(180).Althoughitcanbedifficulttodecidewherethebestplaceforafunctionis,themoredifficultthischoice,oftenthelessitmatters.Ifinditvaluabletotryworkingwithfunctionsinonecontext,knowingI’lllearnhowwelltheyfit,andiftheydon’tfitIcanalwaysmovethemlater.
Mechanics
Examinealltheprogramelementsusedbythechosenfunctioninitscurrentcontext.Considerwhethertheyshouldmovetoo.
IfIfindacalledfunctionthatshouldalsomove,Iusuallymoveitfirst.Thatway,movingaclustersoffunctionsbeginswiththeonethathastheleastdependencyontheothersinthegroup.
Ifahigh-levelfunctionistheonlycallerofsubfunctions,thenyoucaninlinethosefunctionsintothehigh-levelmethod,move,andre-extractatthedestination.
Checkifthechosenfunctionisapolymorphicmethod.
IfI’minanobject-orientedlanguage,Ihavetotakeaccountofsuper-andsubclassdeclarations.
Copythefunctiontothetargetcontext.Adjustittofitinitsnewhome.
Ifthebodyuseselementsinthesourcecontext,Ineedtoeitherpassthoseelementsasparametersorpassareferencetothatsourcecontext.
MovingafunctionoftenmeansIneedtocomeupwithadifferentnamethatworksbetterinthenewcontext.
Performstaticanalysis.
Figureouthowtoreferencethetargetfunctionfromthesourcecontext.
Turnthesourcefunctionintoadelegatingfunction.
Test.
ConsiderInlineFunction(115)onthesourcefunction.
Thesourcefunctioncanstayindefinitelyasadelegatingfunction.Butifitscallerscanjustaseasilyreachthetargetdirectly,thenit’sbettertoremovethemiddleman.
Example:MovingaNestedFunctiontoTopLevel
I’llbeginwithafunctionthatcalculatesthetotaldistanceforaGPStrackrecord.
functiontrackSummary(points){
consttotalTime=calculateTime();
consttotalDistance=calculateDistance();
constpace=totalTime/60/totalDistance;
return{
time:totalTime,
distance:totalDistance,
pace:pace
};
functioncalculateDistance(){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
}
functiondistance(p1,p2){…}
functionradians(degrees){…}
functioncalculateTime(){…}
}
I’dliketomovecalculateDistancetothetoplevelsoIcancalculatedistancesfortrackswithoutalltheotherpartsofthesummary.
Ibeginbycopyingthefunctiontothetoplevel.
functiontrackSummary(points){
consttotalTime=calculateTime();
consttotalDistance=calculateDistance();
constpace=totalTime/60/totalDistance;
return{
time:totalTime,
distance:totalDistance,
pace:pace
};
functioncalculateDistance(){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
}
…
functiondistance(p1,p2){…}
functionradians(degrees){…}
functioncalculateTime(){…}
}
functiontop_calculateDistance(){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
}
WhenIcopyafunctionlikethis,IliketochangethenamesoIcandistinguishthembothinthecodeandinmyhead.Idon’twanttothinkaboutwhattherightnameshouldberightnow,soIcreateatemporaryname.
Theprogramstillworks,butmystaticanalysisisrightlyratherupset.Thenewfunctionhastwoundefinedsymbols:distanceandpoints.Thenaturalwaytodealwithpointsistopassitinasaparameter.
functiontop_calculateDistance(points){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
}
Icoulddothesamewithdistance,butperhapsitmakessensetomoveittogetherwithcalculateDistance.Here’stherelevantcode:
functiontrackSummary…
functiondistance(p1,p2){
//haversineformulaseehttp://www.movable-type.co.uk/scripts/latlong.html
constEARTH_RADIUS=3959;//inmiles
constdLat=radians(p2.lat)-radians(p1.lat);
constdLon=radians(p2.lon)-radians(p1.lon);
consta=Math.pow(Math.sin(dLat/2),2)
+Math.cos(radians(p2.lat))
*Math.cos(radians(p1.lat))
*Math.pow(Math.sin(dLon/2),2);
constc=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
returnEARTH_RADIUS*c;
}
functionradians(degrees){
returndegrees*Math.PI/180;
}
Icanseethatdistanceonlyusesradiansandradiansdoesn’tuseanythinginsideitscurrentcontext.Soratherthanpassthefunctions,Imightaswellmovethemtoo.IcanmakeasmallstepinthisdirectionbymovingthemfromtheircurrentcontexttonesttheminsidethenestedcalculateDistance.
functiontrackSummary(points){
consttotalTime=calculateTime();
consttotalDistance=calculateDistance();
constpace=totalTime/60/totalDistance;
return{
time:totalTime,
distance:totalDistance,
pace:pace
};
functioncalculateDistance(){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
functiondistance(p1,p2){…}
functionradians(degrees){…}
}
Bydoingthis,Icanusebothstaticanalysisandtestingtotellmeifthereareanycomplications.Inthiscasealliswell,soIcancopythemovertotop_calculateDistance.
functiontop_calculateDistance(points){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
functiondistance(p1,p2){…}
functionradians(degrees){…}
}
Again,thecopydoesn’tchangehowtheprogramruns,butdoesgivemeanopportunityformorestaticanalysis.HadInotspottedthatdistancecallsradians,thelinterwouldhavecaughtitatthisstep.
NowthatIhavepreparedthetable,it’stimeforthemajorchange—thebodyoftheoriginalcalculateDistancewillnowcalltop_calculateDistance:
functiontrackSummary(points){
consttotalTime=calculateTime();
consttotalDistance=calculateDistance();
constpace=totalTime/60/totalDistance;
return{
time:totalTime,
distance:totalDistance,
pace:pace
};
functioncalculateDistance(){
returntop_calculateDistance(points);
}
Thisisthecrucialtimetorunteststofullytestthatthemovedfunctionhasbeddeddowninitsnewhome.
Withthatdone,it’slikeunpackingtheboxesaftermovinghouse.Thefirstthingistodecidewhethertokeeptheoriginalfunctionthat’sjustdelegatingornot.Inthiscase,therearefewcallersand,asusualwithnestedfunctions,theyarehighlylocalized.SoI’mhappytogetridofit.
functiontrackSummary(points){
consttotalTime=calculateTime();
consttotalDistance=top_calculateDistance(points);
constpace=totalTime/60/totalDistance;
return{
time:totalTime,
distance:totalDistance,
pace:pace
};
NowisalsoagoodtimetothinkaboutwhatIwantthenametobe.Sincethetop-levelfunctionhasthehighestvisibility,I’dlikeittohavethebestname.totalDistanceseemslikeagoodchoice.Ican’tusethatimmediatelysinceitwillbeshadowedbythevariableinsidetrackSummary—butIdon’tseeanyreasontokeepthatanyway,soIuseInlineVariable(123)onit.
functiontrackSummary(points){
consttotalTime=calculateTime();
constpace=totalTime/60/totalDistance(points);
return{
time:totalTime,
distance:totalDistance(points),
pace:pace
};
functiontotalDistance(points){
letresult=0;
for(leti=1;i<points.length;i++){
result+=distance(points[i-1],points[i]);
}
returnresult;
IfI’dhadtheneedtokeepthevariable,I’dhaverenamedittosomethingliketotalDistanceCacheordistance.
Sincethefunctionsfordistanceandradiansdon’tdependonanythinginsidetotalDistance,Iprefertomovethemtotopleveltoo,puttingallfourfunctionsatthetoplevel.
functiontrackSummary(points){…}
functiontotalDistance(points){…}
functiondistance(p1,p2){…}
functionradians(degrees){…}
SomepeoplewouldprefertokeepdistanceandradiansinsidetotalDistanceinordertorestricttheirvisibility.Insomelanguagesthatmaybeaconsideration,butwithES2015,JavaScripthasanexcellentmodulemechanismthat’sthebesttoolforcontrollingfunctionvisibility.Ingeneral,I’mwaryofnestedfunctions—theytooeasilysetuphiddendatainterrelationshipsthatcangethardtofollow.
Example:MovingBetweenClasses
ToillustratethisvarietyofMoveFunction,I’llstarthere:
classAccount…
getbankCharge(){
letresult=4.5;
if(this._daysOverdrawn>0)result+=this.overdraftCharge;
returnresult;
}
getoverdraftCharge(){
if(this.type.isPremium){
constbaseCharge=10;
if(this.daysOverdrawn<=7)
returnbaseCharge;
else
returnbaseCharge+(this.daysOverdrawn-7)*0.85;
}
else
returnthis.daysOverdrawn*1.75;
}
Cominguparechangesthatleadtodifferenttypesofaccounthavingadifferentalgorithmsfordeterminingthecharge.ThusitseemsnaturaltomoveoverdraftChargetotheaccounttypeclass.
ThefirststepistolookatthefeaturesthattheoverdraftChargemethodusesandconsiderwhetheritisworthmovingabatchofmethodstogether.InthiscaseIneedthedaysOverdrawnmethodtoremainontheaccountclass,becausethatwillvarywithindividualaccounts.
Next,Icopythemethodbodyovertotheaccounttypeandgetittofit.
classAccountType…
overdraftCharge(daysOverdrawn){
if(this.isPremium){
constbaseCharge=10;
if(daysOverdrawn<=7)
returnbaseCharge;
else
returnbaseCharge+(daysOverdrawn-7)*0.85;
}
else
returndaysOverdrawn*1.75;
}
Inordertogetthemethodtofitinitsnewlocation,Ineedtodealwithtwocalltargetsthatchangetheirscope.isPremiumisnowasimplecallonthis.WithdaysOverdrawnIhavetodecide—doIpassthevalueordoIpasstheaccount?Forthemoment,IjustpassthesimplevaluebutImaywellchangethisinthefutureifIrequiremorethanjustthedaysoverdrawnfromtheaccount—especiallyifwhatIwantfromtheaccountvarieswiththeaccounttype.
Next,Ireplacetheoriginalmethodbodywithadelegatingcall.
classAccount…
getbankCharge(){
letresult=4.5;
if(this._daysOverdrawn>0)result+=this.overdraftCharge;
returnresult;
}
getoverdraftCharge(){
returnthis.type.overdraftCharge(this.daysOverdrawn);
}
ThencomesthedecisionofwhethertoleavethedelegationinplaceortoinlineoverdraftCharge.Inliningresultsin:
classAccount…
getbankCharge(){
letresult=4.5;
if(this._daysOverdrawn>0)
result+=this.type.overdraftCharge(this.daysOverdrawn);
returnresult;
}
Intheearliersteps,IpasseddaysOverdrawnasaparameter—butifthere’salotofdatafromtheaccounttopass,Imightprefertopasstheaccountitself.
classAccount…
getbankCharge(){
letresult=4.5;
if(this._daysOverdrawn>0)result+=this.overdraftCharge;
returnresult;
}
getoverdraftCharge(){
returnthis.type.overdraftCharge(this);
}
classAccountType…
overdraftCharge(account){
if(this.isPremium){
constbaseCharge=10;
if(account.daysOverdrawn<=7)
returnbaseCharge;
else
returnbaseCharge+(account.daysOverdrawn-7)*0.85;
}
else
returnaccount.daysOverdrawn*1.75;
}
MoveField
Motivation
Programminginvolveswritingalotofcodethatimplementsbehavior—butthestrengthofaprogramisreallyfoundedonitsdatastructures.IfIhaveagoodsetofdatastructuresthatmatchtheproblem,thenmybehaviorcodeissimpleandstraightforward.Butpoordatastructuresleadtolotsofcodewhosejobismerelydealingwiththepoordata.Andit’snotjustmessiercodethat’sharderto
understand;italsomeansthedatastructuresobscurewhattheprogramisdoing.
So,datastructuresareimportant—butlikemostaspectsofprogrammingtheyarehardtogetright.Idomakeaninitialanalysistofigureoutthebestdatastructures,andI’vefoundthatexperienceandtechniqueslikedomain-drivendesignhaveimprovedmyabilitytodothat.Butdespiteallmyskillandexperience,IstillfindthatIfrequentlymakemistakesinthatinitialdesign.Intheprocessofprogramming,Ilearnmoreabouttheproblemdomainandmydatastructures.Adesigndecisionthatisreasonableandcorrectoneweekcanbecomewronginanother.
AssoonasIrealizethatadatastructureisn’tright,it’svitaltochangeit.IfIleavemydatastructureswiththeirblemishes,thoseblemisheswillconfusemythinkingandcomplicatemycodefarintothefuture.
ImayseektomovedatabecauseIfindIalwaysneedtopassafieldfromonerecordwheneverIpassanotherrecordtoafunction.Piecesofdatathatarealwayspassedtofunctionstogetherareusuallybestputinasinglerecordinordertoclarifytheirrelationship.Changeisalsoafactor;ifachangeinonerecordcausesafieldinanotherrecordtochangetoo,that’sasignofafieldinthewrongplace.IfIhavetoupdatethesamefieldinmultiplestructures,that’sasignthatitshouldmovetoanotherplacewhereitonlyneedstobeupdatedonce.
IusuallydoMoveFieldinthecontextofabroadersetofchanges.OnceI’vemovedafield,Ifindthatmanyoftheusersofthefieldarebetteroffaccessingthatdatathroughthetargetobjectratherthantheoriginalsource.Ithenchangethesewithlaterrefactorings.Similarly,ImayfindthatIcan’tdoMoveFieldatthemomentduetothewaythedataisused.Ineedtorefactorsomeusagepatternsfirst,thendothemove.
Inmydescriptionsofar,I’msaying“record,”butallthisistrueofclassesandobjectstoo.Aclassisarecordtypewithattachedfunctions—andtheseneedtobekepthealthyjustasmuchasanyotherdata.Theattachedfunctionsdomakeiteasiertomovedataaround,sincethedataisencapsulatedbehindaccessormethods.Icanmovethedata,changetheaccessors,andclientsoftheaccessorswillstillwork.So,thisisarefactoringthat’seasiertodoifyouhaveclasses,andmydescriptionbelowmakesthatassumption.IfI’musingbarerecordsthatdon’tsupportencapsulation,Icanstillmakeachangelikethis,butitismoretricky.
Mechanics
Ensurethesourcefieldisencapsulated.
Test.
Createafield(andaccessors)inthetarget.
Runstaticchecks.
Ensurethereisareferencefromthesourceobjecttothetargetobject.
Anexistingfieldormethodmaygiveyouthetarget.Ifnot,seeifyoucaneasilycreateamethodthatwilldoso.Failingthat,youmayneedtocreateanewfieldinthesourceobjectthatcanstorethetarget.Thismaybeapermanentchange,butyoucanalsodoittemporarilyuntilyouhavedoneenoughrefactoringinthebroadercontext.
Adjustaccessorstousethetargetfield.
Ifthetargetissharedbetweensourceobjects,considerfirstupdatingthesettertomodifybothtargetandsourcefields,followedbyIntroduceAssertion(299)todetectinconsistentupdates.Onceyoudeterminealliswell,finishchangingtheaccessorstousethetargetfield.
Test.
Removethesourcefield.
Test.
Example
I’mstartingherewiththiscustomerandcontract.
classCustomer…
constructor(name,discountRate){
this._name=name;
this._discountRate=discountRate;
this._contract=newCustomerContract(dateToday());
}
getdiscountRate(){returnthis._discountRate;}
becomePreferred(){
this._discountRate+=0.03;
//othernicethings
}
applyDiscount(amount){
returnamount.subtract(amount.multiply(this._discountRate));
}
classCustomerContract…
constructor(startDate){
this._startDate=startDate;
}
Iwanttomovethediscountratefieldfromthecustomertothecustomercontract.
ThefirstthingIneedtouseisEncapsulateVariable(132)toencapsulateaccesstothediscountratefield.
classCustomer…
constructor(name,discountRate){
this._name=name;
this._setDiscountRate(discountRate);
this._contract=newCustomerContract(dateToday());
}
getdiscountRate(){returnthis._discountRate;}
_setDiscountRate(aNumber){this._discountRate=aNumber;}
becomePreferred(){
this._setDiscountRate(this.discountRate+0.03);
//othernicethings
}
applyDiscount(amount){
returnamount.subtract(amount.multiply(this.discountRate));
}
Iuseamethodtoupdatethediscountrate,ratherthanapropertysetter,asIdon’twanttomakeapublicsetterforthediscountrate.
Iaddafieldandaccessorstothecustomercontract.
classCustomerContract…
constructor(startDate,discountRate){
this._startDate=startDate;
this._discountRate=discountRate;
}
getdiscountRate(){returnthis._discountRate;}
setdiscountRate(arg){this._discountRate=arg;}
Inowmodifytheaccessorsoncustomertousethenewfield.WhenIdidthat,Igotanerror:“Cannotsetproperty‘discountRate’ofundefined”.Thiswasbecause_setDiscountRatewascalledbeforeIcreatedthecontractobjectintheconstructor.Tofixthat,Ifirstrevertedtothepreviousstate,thenusedSlideStatements(221)tomovethe_setDiscountRateaftercreatingthecontract.
classCustomer…
constructor(name,discountRate){
this._name=name;
this._setDiscountRate(discountRate);
this._contract=newCustomerContract(dateToday());
}
Itestedthat,thenchangedtheaccessorsagaintousethecontract.
classCustomer…
getdiscountRate(){returnthis._contract.discountRate;}
_setDiscountRate(aNumber){this._contract.discountRate=aNumber;}
SinceI’musingJavaScript,thereisnodeclaredsourcefield,soIdon’tneedtoremoveanythingfurther.
ChangingaBareRecord
Thisrefactoringisgenerallyeasierwithobjects,sinceencapsulationprovidesanaturalwaytowrapdataaccessinmethods.IfIhavemanyfunctionsaccessingabarerecord,then,whileit’sstillavaluablerefactoring,itisdecidedlymoretricky.
Icancreateaccessorfunctionsandmodifyallthereadsandwritestousethem.Ifthefieldthat’sbeingmovedisimmutable,IcanupdateboththesourceandthetargetfieldswhenIsetitsvalueandgraduallymigratereads.Still,ifpossible,myfirstmovewouldbetouseEncapsulateRecord(160)toturntherecordinto
aclasssoIcanmakethechangemoreeasily.
Example:MovingtoaSharedObject
Now,let’sconsideradifferentcase.Here’sanaccountwithaninterestrate.
classAccount…
constructor(number,type,interestRate){
this._number=number;
this._type=type;
this._interestRate=interestRate;
}
getinterestRate(){returnthis._interestRate;}
classAccountType…
constructor(nameString){
this._name=nameString;
}
Iwanttochangethingssothatanaccount’sinterestrateisdeterminedfromitsaccounttype.
Theaccesstotheinterestrateisalreadynicelyencapsulated,soI’lljustcreatethefieldandanappropriateaccessorontheaccounttype.
classAccountType…
constructor(nameString,interestRate){
this._name=nameString;
this._interestRate=interestRate;
}
getinterestRate(){returnthis._interestRate;}
ButthereisapotentialproblemwhenIupdatetheaccessesfromAccount.Beforethisrefactoring,eachaccounthaditsowninterestrate.Now,Iwantallaccountstosharetheinterestratesoftheiraccounttype.Ifalltheaccountsofthesametypealreadyhavethesameinterestrate,thenthere’snochangeinobservablebehavior,soI’mfinewiththerefactoring.Butifthere’sanaccountwithadifferentinterestrate,it’snolongerarefactoring.Ifmyaccountdataisheldinadatabase,Ishouldcheckthedatabasetoensurethatallmyaccountshavetheratematchingtheirtype.IcanalsoIntroduceAssertion(299)inthe
accountclass.
classAccount…
constructor(number,type,interestRate){
this._number=number;
this._type=type;
assert(interestRate===this._type.interestRate);
this._interestRate=interestRate;
}
getinterestRate(){returnthis._interestRate;}
ImightrunthesystemforawhilewiththisassertioninplacetoseeifIgetanerror.Or,insteadofaddinganassertion,Imightlogtheproblem.OnceI’mconfidentthatI’mnotintroducinganobservablechange,Icanchangetheaccess,removingtheupdatefromtheaccountcompletely.
classAccount…
constructor(number,type){
this._number=number;
this._type=type;
}
getinterestRate(){returnthis._type.interestRate;}
MoveStatementsintoFunction
inverseof:MoveStatementstoCallers(215)
Motivation
Removingduplicationisoneofthebestrulesofthumbofhealthycode.IfIseethesamecodeexecutedeverytimeIcallaparticularfunction,Ilooktocombinethatrepeatingcodeintothefunctionitself.Thatway,anyfuturemodificationstotherepeatingcodecanbedoneinoneplaceandusedbyallthecallers.Shouldthecodevaryinthefuture,Icaneasilymoveit(orsomeofit)outagainwith
MoveStatementstoCallers(215).
ImovestatementsintoafunctionwhenIcanbestunderstandthesestatementsaspartofthecalledfunction.Iftheydon’tmakesenseaspartofthecalledfunction,butstillshouldbecalledwithit,I’llsimplyuseExtractFunction(106)onthestatementsandthecalledfunction.That’sessentiallythesameprocessasIdescribebelow,butwithouttheinlineandrenamesteps.It’snotunusualtodothatandthen,afterlaterreflection,carryoutthethosefinalsteps.
Mechanics
Iftherepetitivecodeisn’tadjacenttothecallofthetargetfunction,useSlideStatements(221)togetitadjacent.
Ifthetargetfunctionisonlycalledbythesourcefunction,justcutthecodefromthesource,pasteitintothetarget,test,andignoretherestofthesemechanics.
Ifyouhavemorecallers,useExtractFunction(106)ononeofthecallsitestoextractboththecalltothetargetfunctionandthestatementsyouwishtomoveintoit.Giveitanamethat’stransient,buteasytogrep.
Converteveryothercalltousethenewfunction.Testaftereachconversion.
Whenalltheoriginalcallsusethenewfunction,useInlineFunction(115)toinlinetheoriginalfunctioncompletelyintothenewfunction,removingtheoriginalfunction.
ChangeFunctionDeclaration(124)tochangethenameofthenewfunctiontothesamenameastheoriginalfunction.
Ortoabettername,ifthereisone.
Example
I’llstartwiththiscodetoemitHTMLfordataaboutaphoto.
functionrenderPerson(outStream,person){
constresult=[];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>title:${person.photo.title}</p>`);
result.push(emitPhotoData(person.photo));
returnresult.join("\n");
}
functionphotoDiv(p){
return[
"<div>",
`<p>title:${p.title}</p>`,
emitPhotoData(p),
"</div>",
].join("\n");
}
functionemitPhotoData(aPhoto){
constresult=[];
result.push(`<p>location:${aPhoto.location}</p>`);
result.push(`<p>date:${aPhoto.date.toDateString()}</p>`);
returnresult.join("\n");
}
ThiscodeshowstwocallstoemitPhotoData,eachprecededbyalineofcodethatissemanticallyequivalent.I’dliketoremovethisduplicationbymovingthetitleprintingintoemitPhotoData.IfIhadjusttheonecaller,Iwouldjustcutandpastethecode,butthemorecallersIhave,themoreI’minclinedtouseasaferprocedure.
IbeginbyusingExtractFunction(106)ononeofthecallers.I’mextractingthestatementsIwanttomoveintoemitPhotoData,togetherwiththecalltoemitPhotoDataitself.
functionphotoDiv(p){
return[
"<div>",
zznew(p),
"</div>",
].join("\n");
}
functionzznew(p){
return[
`<p>title:${p.title}</p>`,
emitPhotoData(p),
].join("\n");
}
IcannowlookattheothercallersofemitPhotoDataand,onebyone,replacethecallsandtheprecedingstatementswithcallstothenewfunction.
functionrenderPerson(outStream,person){
constresult=[];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(zznew(person.photo));
returnresult.join("\n");
}
NowthatI’vedoneallthecallers,IuseInlineFunction(115)onemitPhotoData.
functionzznew(p){
return[
`<p>title:${p.title}</p>`,
`<p>location:${p.location}</p>`,
`<p>date:${p.date.toDateString()}</p>`,
].join("\n");
}
AndfinishwithChangeFunctionDeclaration(124)
functionrenderPerson(outStream,person){
constresult=[];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(emitPhotoData(person.photo));
returnresult.join("\n");
}
functionphotoDiv(aPhoto){
return[
"<div>",
emitPhotoData(aPhoto),
"</div>",
].join("\n");
}
functionemitPhotoData(aPhoto){
return[
`<p>title:${aPhoto.title}</p>`,
`<p>location:${aPhoto.location}</p>`,
`<p>date:${aPhoto.date.toDateString()}</p>`,
].join("\n");
}
IalsomaketheparameternamesfitmyconventionwhileI’matit.
MoveStatementstoCallers
inverseof:MoveStatementsintoFunction(211)
Motivation
Functionsarethebasicbuildingblockoftheabstractionswebuildasprogrammers.And,aswithanyabstraction,wedon’talwaysgettheboundariesright.Asacodebasechangesitscapabilities—asmostusefulsoftwaredoes—weoftenfindourabstractionboundariesshift.Forfunctions,thatmeansthatwhatmightoncehavebeenacohesive,atomicunitofbehaviorbecomesamixoftwoormoredifferentthings.
Onetriggerforthisiswhencommonbehaviorusedinseveralplacesneedstovaryinsomeofitscalls.Now,weneedtomovethevaryingbehavioroutofthefunctiontoitscallers.Inthiscase,I’lluseSlideStatements(221)togetthevaryingbehaviortothebeginningorendofthefunctionandthenMoveStatementstoCallers.Oncethevaryingcodeisinthecaller,Icanchangeitwhennecessary.
MoveStatementstoCallersworkswellforsmallchanges,butsometimestheboundariesbetweencallerandcalleeneedcompletereworking.Inthatcase,mybestmoveistouseInlineFunction(115)andthenslideandextractnewfunctionstoformbetterboundaries.
Mechanics
Insimplecircumstances,whereyouhaveonlyoneortwocallersandasimplefunctiontocallfrom,justcutthefirstlinefromthecalledfunctionandpaste(andperhapsfit)itintothecallers.Testandyou’redone.
Otherwise,applyExtractFunction(106)toallthestatementsthatyoudon’twishtomove;giveitatemporarybuteasilysearchablename.
Ifthefunctionisamethodthatisoverriddenbysubclasses,dotheextractiononallofthemsothattheremainingmethodisidenticalinallclasses.Thenremovethesubclassmethods.
UseInlineFunction(115)ontheoriginalfunction.
ApplyChangeFunctionDeclaration(124)ontheextractedfunctiontorenameittotheoriginalname.
Ortoabettername,ifyoucanthinkofone.
Example
Here’sasimplecase:afunctionwithtwocallers.
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
emitPhotoData(outStream,person.photo);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
emitPhotoData(outStream,p);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
outStream.write(`<p>location:${photo.location}</p>\n`);
}
IneedtomodifythesoftwaresothatlistRecentPhotosrendersthelocationinformationdifferentlywhilerenderPersonstaysthesame.Tomakethischangeeasier,I’lluseMoveStatementstoCallersonthefinalline.
Usually,whenfacedwithsomethingthissimple,I’lljustcutthelastlinefromrenderPersonandpasteitbelowthetwocalls.ButsinceI’mexplainingwhattodoinmoretrickycases,I’llgothroughthemoreelaboratebutsaferprocedure.
MyfirststepistouseExtractFunction(106)onthecodethatwillremaininemitPhotoData.
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
emitPhotoData(outStream,person.photo);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
emitPhotoData(outStream,p);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
zztmp(outStream,photo);
outStream.write(`<p>location:${photo.location}</p>\n`);
}
functionzztmp(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
}
Usually,thenameoftheextractedfunctionisonlytemporary,soIdon’tworryaboutcomingupwithanythingmeaningful.However,itishelpfultousesomethingthat’seasytogrep.Icantestatthispointtoensurethecodeworksoverthefunctioncallboundary.
NowIuseInlineFunction(115),onecallatatime.IstartwithrenderPerson.
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
zztmp(outStream,person.photo);
outStream.write(`<p>location:${person.photo.location}</p>\n`);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
emitPhotoData(outStream,p);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
zztmp(outStream,photo);
outStream.write(`<p>location:${photo.location}</p>\n`);
}
functionzztmp(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
}
Itestagaintoensurethiscallisworkingproperly,thenmoveontothenext.
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
zztmp(outStream,person.photo);
outStream.write(`<p>location:${person.photo.location}</p>\n`);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
zztmp(outStream,p);
outStream.write(`<p>location:${p.location}</p>\n`);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
zztmp(outStream,photo);
outStream.write(`<p>location:${photo.location}</p>\n`);
}
functionzztmp(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
}
ThenIcandeletetheouterfunction,completingInlineFunction(115).
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
zztmp(outStream,person.photo);
outStream.write(`<p>location:${person.photo.location}</p>\n`);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
zztmp(outStream,p);
outStream.write(`<p>location:${p.location}</p>\n`);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
zztmp(outStream,photo);
outStream.write(`<p>location:${photo.location}</p>\n`);
}
functionzztmp(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
}
Ithenrenamezztmpbacktotheoriginalname.
functionrenderPerson(outStream,person){
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream,person.photo);
emitPhotoData(outStream,person.photo);
outStream.write(`<p>location:${person.photo.location}</p>\n`);
}
functionlistRecentPhotos(outStream,photos){
photos
.filter(p=>p.date>recentDateCutoff())
.forEach(p=>{
outStream.write("<div>\n");
emitPhotoData(outStream,p);
outStream.write(`<p>location:${p.location}</p>\n`);
outStream.write("</div>\n");
});
}
functionemitPhotoData(outStream,photo){
outStream.write(`<p>title:${photo.title}</p>\n`);
outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);
}
ReplaceInlineCodewithFunctionCall
Motivation
Functionsallowmetopackageupbitsofbehavior.Thisisusefulforunderstanding—anamedfunctioncanexplainthepurposeofthecoderatherthanitsmechanics.It’salsovaluabletoremoveduplication:Insteadofwritingthesamecodetwice,Ijustcallthefunction.Then,shouldIneedtochangethefunction’simplementation,Idon’thavetotrackdownsimilar-lookingcodetoupdateallthechanges.(Imayhavetolookatthecallers,toseeiftheyshouldallusethenewcode,butthat’sbothlesscommonandmucheasier.)
IfIseeinlinecodethat’sdoingthesamethingthatIhaveinanexistingfunction,I’llusuallywanttoreplacethatinlinecodewithafunctioncall.TheexceptionisifIconsiderthesimilaritytobecoincidental—sothat,ifIchangethefunctionbody,Idon’texpectthebehaviorinthisinlinecodetochange.Aguidetothisisthenameofthefunction.AgoodnameshouldmakesenseinplaceofinlinecodeIhave.Ifthenamedoesn’tmakesense,thatmaybebecauseit’sapoorname(inwhichcaseIuseChangeFunctionDeclaration(124)tofixit)orbecausethefunction’spurposeisdifferenttowhatIwantinthiscase—soIshouldn’tcallit.
Ifinditparticularlysatisfyingtodothiswithcallstolibraryfunctions—thatway,Idon’tevenhavetowritethefunctionbody.
Mechanics
Replacetheinlinecodewithacalltotheexistingfunction.
Test.
SlideStatements
formerly:ConsolidateDuplicateConditionalFragments
Motivation
Codeiseasiertounderstandwhenthingsthatarerelatedtoeachotherappeartogether.Ifseverallinesofcodeaccessthesamedatastructure,it’sbestforthemtobetogetherratherthanintermingledwithcodeaccessingotherdatastructures.Atitssimplest,IuseSlideStatementstokeepsuchcodetogether.Averycommoncaseofthisisdeclaringandusingvariables.Somepeopleliketodeclarealltheirvariablesatthetopofafunction.IprefertodeclarethevariablejustbeforeIfirstuseit.
Usually,Imoverelatedcodetogetherasapreparatorystepforanother
refactoring,oftenanExtractFunction(106).Puttingrelatedcodeintoaclearlyseparatedfunctionisabetterseparationthanjustmovingasetoflinestogether,butIcan’tdotheExtractFunction(106)unlessthecodeistogetherinthefirstplace.
Mechanics
Identifythetargetpositiontomovefragmentto.Examinestatementsbetweensourceandtargettoseeifthereisinterferenceforthecandidatefragment.Abandonactionifthereisanyinterference.
Afragmentcannotslidebackwardsearlierthananyelementitreferencesisdeclared.
Afragmentcannotslideforwardsbeyondanyelementthatreferencesit.
Afragmentcannotslideoveranystatementthatmodifiesanelementitreferences.
Afragmentthatmodifiesanelementcannotslideoveranyotherelementthatreferencesthemodifiedelement.
Cutfragmentfromthesourceandpasteintothetargetposition.
Test.
Ifthetestfails,trybreakingdowntheslideintosmallersteps.Eitherslideoverlesscodeorreducetheamountofcodeinthefragmentyou’removing.
Example
Whenslidingcodefragments,therearetwodecisionsinvolved:whatslideI’dliketodoandwhetherIcandoit.Thefirstdecisionisverycontext-specific.Onthesimplestlevel,IliketodeclareelementsclosetowhereIusethem,soI’lloftenslideadeclarationdowntoitsusage.ButalmostalwaysIslidesomecodebecauseIwanttodoanotherrefactoring—perhapstogetaclumpofcodetogethertoExtractFunction(106).
OnceIhaveasenseofwhereI’dliketomovesomecode,thenextpartis
decidingifIcandoit.ThisinvolveslookingatthecodeI’mslidingandthecodeI’mslidingover:Dotheyinterferewitheachotherinawaythatwouldchangetheobservablebehavioroftheprogram?
Considerthefollowingfragmentofcode.
1constpricingPlan=retrievePricingPlan();
2constorder=retreiveOrder();
3constbaseCharge=pricingPlan.base;
4letcharge;
5constchargePerUnit=pricingPlan.unit;
6constunits=order.units;
7letdiscount;
8charge=baseCharge+units*chargePerUnit;
9letdiscountableUnits=Math.max(units-pricingPlan.discountThreshold,0);
10discount=discountableUnits*pricingPlan.discountFactor;
11if(order.isRepeat)discount+=20;
12charge=charge-discount;
13chargeOrder(charge);
Thefirstsevenlinesaredeclarations,andit’srelativelyeasytomovethese.Forexample,Imaywanttomoveallthecodedealingwithdiscountstogether,whichwouldinvolvemovingline7(`letdiscount`)toaboveline10(`discount=…`).Sinceadeclarationhasnosideeffectsandreferstonoothervariable,Icansafelymovethisforwardsasfarasthefirstlinethatreferencesdiscountitself.Thisisalsoacommonmove—ifIwanttouseExtractFunction(106)onthediscountlogic,I’llneedtomovethedeclarationdownfirst.
Idosimilaranalysiswithanycodethatdoesn’thaveside-effects.SoIcantakeline2(`constorder=…`)andmoveitdowntoaboveline6(`constunits=…`withouttrouble.
Inthiscase,I’malsohelpedbythefactthatthecodeI’mmovingoverdoesn’thavesideeffectseither.Indeed,Icanfreelyrearrangecodethatlackssideeffectstomyheart’scontent,whichisoneofthereasonswhywiseprogrammersprefertouseside-effect-freecodeasmuchaspossible.
Thereisawrinklehere,however.HowdoIknowthatline2isside-effect-free?Tobesure,I’dneedtolookinside(retrieveOrder())toensuretherearenosideeffectsthere(andinsideanyfunctionsitcalls,andinsideanyfunctionsitsfunctionscall,andsoon).Inpractice,whenworkingonmyowncode,IknowthatIgenerallyfollowtheCommand-QuerySeparation
(https://martinfowler.com/bliki/Command-QuerySeparation.html)principle,soanyfunctionthatreturnsavalueisfreeofsideeffects.ButIcanonlybeconfidentofthatbecauseIknowthecodebase;ifIwereworkinginanunknowncodebase,I’dhavetobemorecautious.ButIdotrytofollowtheCommand-QuerySeparationinmyowncodebecauseit’ssovaluabletoknowthatcodeisfreeofsideeffects.
Whenslidingcodethathasasideeffect,orslidingovercodewithsideeffects,Ihavetobemuchmorecareful.WhatI’mlookingforisinterferencebetweenthetwocodefragments.So,let’ssayIwanttoslideline11(`if(order.isRepeat)`…)downtotheend.I’mpreventedfromdoingthatbyline12becauseitreferencesthevariablewhosestateI’mchanginginline11.Similarly,Ican’ttakeline13(`chargeOrder(charge)`)andmoveitupbecauseline12modifiessomestatethatline13references.However,Icanslideline8(`charge=baseCharge+…`)overlines9–11becausetheretheydon’tmodifyanycommonstate.
ThemoststraightforwardruletofollowisthatIcan’tslideonefragmentofcodeoveranotherifanydatathatbothfragmentsrefertoismodifiedbyeitherone.Butthat’snotacomprehensiverule;Icanhappilyslideeitherofthefollowingtwolinesovertheother.
a=a+10;
a=a+5;
ButjudgingwhetheraslideissafemeansIhavetoreallyunderstandtheoperationsinvolvedandhowtheycompose.
SinceIneedtoworrysomuchaboutupdatingstate,IlooktoremoveasmuchofitasIcan.Sowiththiscode,I’dbelookingtoapplySplitVariable(240)onchargebeforeIindulgeinanyslidingaroundofthatcode.
Here,theanalysisisrelativelysimplebecauseI’mmostlyjustmodifyinglocalvariables.Withmorecomplexdatastructures,it’smuchhardertobesurewhenIgetinterference.Sotestsplayanimportantrole:Slidethefragment,runtests,seeifthingsbreak.Ifmytestcoverageisgood,Icanfeelhappywiththerefactoring.Butiftestsaren’treliable,Ineedtobemorewary—or,morelikely,toimprovethetestsforthecodeI’mworkingon.
Themostimportantconsequenceofatestfailureafteraslideistousesmaller
slides:Insteadofslidingovertenlines,I’lljustpickfive,orslideuptowhatIreckonisadangerousline.Itmayalsomeanthattheslideisn’tworthit,andIneedtoworkonsomethingelsefirst.
Example:Slidingwithconditionals
Icanalsodoslideswithconditionals.ThiswilleitherinvolveremovingduplicatelogicwhenIslideoutofaconditional,oraddingduplicatelogicwhenIslidein.
Here’sacasewhereIhavethesamestatementsinbothlegsofaconditional.
letresult;
if(availableResources.length===0){
result=createResource();
allocatedResources.push(result);
}else{
result=availableResources.pop();
allocatedResources.push(result);
}
returnresult;
Icanslidetheseoutoftheconditional,inwhichcasetheyturnintoasinglestatementoutsideoftheconditionalblock.
letresult;
if(availableResources.length===0){
result=createResource();
}else{
result=availableResources.pop();
}
allocatedResources.push(result);
returnresult;
Inthereversecase,slidingafragmentintoaconditionalmeansrepeatingitineverylegoftheconditional.
FurtherReading
I’veseenanalmostidenticalrefactoringunderthenameofSwapStatement(https://www.industriallogic.com/blog/swap-statement-refactoring/).SwapStatementmovesadjacentfragments,butitonlyworkswithsingle-
statementfragments.YoucanthinkofitasSlideStatementswhereboththeslidingfragmentandtheslid-overfragmentaresinglestatements.Thisrefactoringappealstome;afterall,I’malwaysgoingonabouttakingsmallsteps—stepsthatmayseemridiculouslysmalltothosenewtorefactoring.
ButIendedupwritingthisrefactoringwithlargerfragmentsbecausethatiswhatIdo.IonlymoveonestatementatatimeifI’mhavingdifficultywithalargerslide,andIrarelyrunintoproblemswithlargerslides.Withmoremessycode,however,smallerslidesendupbeingeasier.
SplitLoop
Motivation
Youoftenseeloopsthataredoingtwodifferentthingsatoncejustbecausetheycandothatwithonepassthroughaloop.Butifyou’redoingtwodifferentthingsinthesameloop,thenwheneveryouneedtomodifytheloopyouhavetounderstandboththings.Bysplittingtheloop,youensureyouonlyneedtounderstandthebehavioryouneedtomodify.
Splittingaloopcanalsomakeiteasiertouse.Aloopthatcalculatesasinglevaluecanjustreturnthatvalue.Loopsthatdomanythingsneedtoreturnstructuresorpopulatelocalvariables.IfrequentlyfollowasequenceofSplitLoopfollowedbyExtractFunction(106).
Manyprogrammersareuncomfortablewiththisrefactoring,asitforcesyoutoexecutethelooptwice.Myreminder,asusual,istoseparaterefactoringfromoptimization(RefactoringandPerformance,p.62).OnceIhavemycodeclear,I’lloptimizeit,andifthelooptraversalisabottleneck,it’seasytoslamtheloopsbacktogether.Buttheactualiterationthroughevenalargelistisrarelyabottleneck,andsplittingtheloopsoftenenablesother,morepowerful,optimizations.
Mechanics
Copytheloop.
Identifyandeliminateduplicatesideeffects.
Test.
Whendone,considerExtractFunction(106)oneachloop.
Example
I’llstartwithalittlebitofcodethatcalculatesthetotalsalaryandyoungestage.
letyoungest=people[0]?people[0].age:Infinity;
lettotalSalary=0;
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
totalSalary+=p.salary;
}
return`youngestAge:${youngest},totalSalary:${totalSalary}`;
It’saverysimpleloop,butit’sdoingtwodifferentcalculations.Tosplitthem,Ibeginwithjustcopyingtheloop.
letyoungest=people[0]?people[0].age:Infinity;
lettotalSalary=0;
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
totalSalary+=p.salary;
}
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
totalSalary+=p.salary;
}
return`youngestAge:${youngest},totalSalary:${totalSalary}`;
Withtheloopcopied,Ineedtoremovetheduplicationthatwouldotherwiseresultinwrongresults.Ifsomethingintheloophasnosideeffects,Icanleaveittherefornow,butit’snotthecasewiththisexample.
letyoungest=people[0]?people[0].age:Infinity;
lettotalSalary=0;
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
totalSalary+=p.salary;
}
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
totalSalary+=p.salary;
}
return`youngestAge:${youngest},totalSalary:${totalSalary}`;
Officially,that’stheendoftheSplitLooprefactoring.ButthepointofSplitLoopisn’twhatitdoesonitsownbutwhatitsetsupforthenextmove—andI’musuallylookingtoextracttheloopsintotheirownfunctions.I’lluseSlideStatements(221)toreorganizethecodeabitfirst.
lettotalSalary=0;
for(constpofpeople){
totalSalary+=p.salary;
}
letyoungest=people[0]?people[0].age:Infinity;
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
}
return`youngestAge:${youngest},totalSalary:${totalSalary}`;
ThenIdoacoupleofExtractFunction(106)
return`youngestAge:${youngestAge()},totalSalary:${totalSalary()}`;
functiontotalSalary(){
lettotalSalary=0;
for(constpofpeople){
totalSalary+=p.salary;
}
returntotalSalary;
}
functionyoungestAge(){
letyoungest=people[0]?people[0].age:Infinity;
for(constpofpeople){
if(p.age<youngest)youngest=p.age;
}
returnyoungest;
}
IcanrarelyresistReplaceLoopwithPipeline(230)forthetotalsalary,andthere’sanobviousSubstituteAlgorithm(193)fortheyoungestage.
return`youngestAge:${youngestAge()},totalSalary:${totalSalary()}`;
functiontotalSalary(){
returnpeople.reduce((total,p)=>total+p.salary,0);
}
functionyoungestAge(){
returnMath.min(...people.map(p=>p.age));
}
ReplaceLoopwithPipeline
Motivation
Likemostprogrammers,Iwastaughttouseloopstoiterateoveracollectionofobjects.Increasingly,however,languageenvironmentsprovideabetterconstruct:thecollectionpipeline.Collectionpipelines[bib-coll-pipe]allowmetodescribemyprocessingasaseriesofoperations,eachconsumingandemittingacollection.Themostcommonoftheseoperationsaremap,whichusesafunctiontotransformeachelementoftheinputcollection,andfilterwhichusesafunctiontoselectasubsetoftheinputcollectionforlaterstepsinthepipeline.Ifindlogicmucheasiertofollowifitisexpressedasapipeline—Icanthenreadfromtoptobottomtoseehowobjectsflowthroughthepipeline.
Mechanics
Createanewvariablefortheloop’scollection.
Thismaybeasimplecopyofanexistingvariable.
Startingatthetop,takeeachbitofbehaviorintheloopandreplaceitwitha
collectionpipelineoperationinthederivationoftheloopcollectionvariable.Testaftereachchange.
Onceallbehaviorisremovedfromtheloop,removeit.
Ifitassignstoanaccumulator,assignthepipelineresulttotheaccumulator.
Example
I’llbeginwithsomedata:aCSVfileofdataaboutouroffices.
office,country,telephone
Chicago,USA,+13123731000
Beijing,China,+864008900505
Bangalore,India,+918040649570
PortoAlegre,Brazil,+555130793550
Chennai,India,+914466044766
...(moredatafollows)
ThefollowingfunctionpicksouttheofficesinIndiaandreturnstheircitiesandtelephonenumbers.
functionacquireData(input){
constlines=input.split("\n");
letfirstLine=true;
constresult=[];
for(constlineoflines){
if(firstLine){
firstLine=false;
continue;
}
if(line.trim()==="")continue;
constrecord=line.split(",");
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
Iwanttoreplacethatloopwithacollectionpipeline.
Myfirststepistocreateaseparatevariableforthelooptoworkover.
functionacquireData(input){
constlines=input.split("\n");
letfirstLine=true;
constresult=[];
constloopItems=lines
for(constlineofloopItems){
if(firstLine){
firstLine=false;
continue;
}
if(line.trim()==="")continue;
constrecord=line.split(",");
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
ThefirstpartoftheloopisallaboutskippingthefirstlineoftheCSVfile.Thiscallsforaslice,soIremovethatfirstsectionoftheloopandaddasliceoperationtotheformationoftheloopvariable.
functionacquireData(input){
constlines=input.split("\n");
letfirstLine=true;
constresult=[];
constloopItems=lines
.slice(1);
for(constlineofloopItems){
if(firstLine){
firstLine=false;
continue;
}
if(line.trim()==="")continue;
constrecord=line.split(",");
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
Asabonus,thisletsmedeletefirstLine—andIparticularlyenjoydeletingcontrolvariables.
Thenextbitofbehaviorremovesanyblanklines.Icanreplacethiswithafilter
operation.
functionacquireData(input){
constlines=input.split("\n");
constresult=[];
constloopItems=lines
.slice(1)
.filter(line=>line.trim()!=="")
;
for(constlineofloopItems){
if(line.trim()==="")continue;
constrecord=line.split(",");
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
Whenwritingapipeline,Ifinditbesttoputtheterminalsemicolononitsownline.
Iusethemapoperationtoturnlinesintoanarrayofstrings—misleadinglycalledrecordintheoriginalfunction,butit’ssafertokeepthenamefornowandrenamelater.
functionacquireData(input){
constlines=input.split("\n");
constresult=[];
constloopItems=lines
.slice(1)
.filter(line=>line.trim()!=="")
.map(line=>line.split(","))
;
for(constlineofloopItems){
constrecord=line;.split(",");
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
FilteragaintojustgettheIndiarecords.
functionacquireData(input){
constlines=input.split("\n");
constresult=[];
constloopItems=lines
.slice(1)
.filter(line=>line.trim()!=="")
.map(line=>line.split(","))
.filter(record=>record[1].trim()==="India")
;
for(constlineofloopItems){
constrecord=line;
if(record[1].trim()==="India"){
result.push({city:record[0].trim(),phone:record[2].trim()});
}
}
returnresult;
}
Maptotheoutputrecordform.
functionacquireData(input){
constlines=input.split("\n");
constresult=[];
constloopItems=lines
.slice(1)
.filter(line=>line.trim()!=="")
.map(line=>line.split(","))
.filter(record=>record[1].trim()==="India")
.map(record=>({city:record[0].trim(),phone:record[2].trim()}))
;
for(constlineofloopItems){
constrecord=line;
result.push(line);
}
returnresult;
}
Now,alltheloopdoesisassignvaluestotheaccumulator.SoIcanremoveitandassigntheresultofthepipelinetotheaccumulator:
functionacquireData(input){
constlines=input.split("\n");
constresult=lines
.slice(1)
.filter(line=>line.trim()!=="")
.map(line=>line.split(","))
.filter(record=>record[1].trim()==="India")
.map(record=>({city:record[0].trim(),phone:record[2].trim()}))
;
for(constlineofloopItems){
constrecord=line;
result.push(line);
}
returnresult;
}
That’sthecoreoftherefactoring.ButIdohavesomecleanupI’dliketodo.Iinlinedresult,renamedsomelambdavariables,andmadethelayoutreadmorelikeatable.
functionacquireData(input){
constlines=input.split("\n");
returnlines
.slice(1)
.filter(line=>line.trim()!=="")
.map(line=>line.split(","))
.filter(fields=>fields[1].trim()==="India")
.map(fields=>({city:fields[0].trim(),phone:fields[2].trim()}))
;
}
Ithoughtaboutinlininglinestoo,butfeltthatitspresenceexplainswhat’shappening.
FurtherReading
Formoreexamplesonturningloopsintopipelines,seemyessayRefactoringwithLoopsandCollectionPipelines[bib-loop-pipe-article].
RemoveDeadCode
Motivation
Whenweputcodeintoproduction,evenonpeople’sdevices,wearen’tchargedbyweight.Afewunusedlinesofcodedon’tslowdownoursystemsnortakeupsignificantmemory;indeed,decentcompilerswillinstinctivelyremovethem.Butunusedcodeisstillasignificantburdenwhentryingtounderstandhowthesoftwareworks.Itdoesn’tcarryanywarningsignstellingprogrammersthattheycanignorethisfunctionasit’snevercalledanymore,sotheystillhavetospendtimeunderstandingwhatit’sdoingandwhychangingitdoesn’tseemtoaltertheoutputastheyexpected.
Oncecodeisn’tusedanymore,weshoulddeleteit.Idon’tworrythatImayneeditsometimeinthefuture;shouldthathappen,IhavemyversioncontrolsystemsoIcanalwaysdigitoutagain.Ifit’ssomethingIreallythinkImayneedoneday,Imightputacommentintothecodethatmentionsthelostcodeandwhichrevisionitwasremovedin—but,honestly,Ican’trememberthelasttimeIdidthat,orregrettedthatIhadn’tdoneit.
Commentingoutdeadcodewasonceacommonhabit.Thiswasusefulinthedaysbeforeversioncontrolsystemswerewidelyused,orwhentheywereinconvenient.Now,whenIcanputeventhesmallestcodebaseunderversioncontrol,that’snolongerneeded.
Mechanics
Ifthedeadcodecanbereferencedfromoutside,e.g.whenit’safullfunction,doasearchtocheckforcallers.
Removethedeadcode.
Test.
Chapter9OrganizingDataDatastructuresplayanimportantroleinourprograms,soit’snogreatshockthatIhaveaclutchofrefactoringsthatfocusonthem.Avaluethat’susedfordifferentpurposesisabreedinggroundforconfusionandbugs—so,whenIseeone,IuseSplitVariable(240)toseparatetheusages.Aswithanyprogramelement,gettingavariable’snamerightistrickyandimportant,soRenameVariable(137)isoftenmyfriend.ButsometimesthebestthingIcandowithavariableistogetridofitcompletely—withReplaceDerivedVariablewithQuery(248).
Ioftenfindproblemsinacodebaseduetoaconfusionbetweenreferencesandvalues,soIuseChangeReferencetoValue(252)andChangeValuetoReference(256)tochangebetweenthesestyles.
SplitVariable
formerly:RemoveAssignmentstoParameters
formerly:SplitTemp
Motivation
Variableshavevarioususes.Someoftheseusesnaturallyleadtothevariablebeingassignedtoseveraltimes.Loopvariableschangeforeachrunofaloop(suchastheiinfor(leti=0;i<10;i++)).Collectingvariablesstoreavaluethatisbuiltupduringthemethod.
Manyothervariablesareusedtoholdtheresultofalong-windedbitofcodeforeasyreferencelater.Thesekindsofvariablesshouldbesetonlyonce.Iftheyaresetmorethanonce,itisasignthattheyhavemorethanoneresponsibilitywithinthemethod.Anyvariablewithmorethanoneresponsibilityshouldbereplacedwithmultiplevariables,oneforeachresponsibility.Usingavariablefortwodifferentthingsisveryconfusingforthereader.
Mechanics
Changethenameofthevariableatitsdeclarationandfirstassignment.
Ifthelaterassignmentsareoftheformi=i+something,thatisacollectingvariable,sodon’tsplitit.Acollectingvariableisoftenusedforcalculatingsums,stringconcatenation,writingtoastream,oraddingtoacollection.
Ifpossible,declarethenewvariableasimmutable.
Changeallreferencesofthevariableuptoitssecondassignment.
Test.
Repeatinstages,ateachstagerenamingthevariableatthedeclarationandchangingreferencesuntilthenextassignment,untilyoureachthefinalassignment.
Example
Forthisexample,Icomputethedistancetraveledbyahaggis.Fromastanding
start,ahaggisexperiencesaninitialforce.Afteradelay,asecondaryforcekicksintofurtheracceleratethehaggis.Usingthecommonlawsofmotion,Icancomputethedistancetraveledasfollows:
functiondistanceTravelled(scenario,time){
letresult;
letacc=scenario.primaryForce/scenario.mass;
letprimaryTime=Math.min(time,scenario.delay);
result=0.5*acc*primaryTime*primaryTime;
letsecondaryTime=time-scenario.delay;
if(secondaryTime>0){
letprimaryVelocity=acc*scenario.delay;
acc=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;
result+=primaryVelocity*secondaryTime+0.5*acc*secondaryTime*secondaryTime;
}
returnresult;
}
Aniceawkwardlittlefunction.Theinterestingthingforourexampleisthewaythevariableaccissettwice.Ithastworesponsibilities:onetoholdtheinitialaccelerationfromthefirstforceandanotherlatertoholdtheaccelerationfrombothforces.Iwanttosplitthisvariable.
Whentryingtounderstandhowavariableisused,it’shandyifmyeditorcanhighlightalloccurrencesofasymbolwithinafunctionorfile.Mostmoderneditorscandothisprettyeasily.
Istartatthebeginningbychangingthenameofthevariableanddeclaringthenewnameasconst.Then,Ichangeallreferencestothevariablefromthatpointuptothenextassignment.Atthenextassignment,Ideclareit:
functiondistanceTravelled(scenario,time){
letresult;
constprimaryAcceleration=scenario.primaryForce/scenario.mass;
letprimaryTime=Math.min(time,scenario.delay);
result=0.5*primaryAcceleration*primaryTime*primaryTime;
letsecondaryTime=time-scenario.delay;
if(secondaryTime>0){
letprimaryVelocity=primaryAcceleration*scenario.delay;
letacc=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;
result+=primaryVelocity*secondaryTime+0.5*acc*secondaryTime*secondaryTime;
}
returnresult;
}
Ichoosethenewnametorepresentonlythefirstuseofthevariable.Imakeitconsttoensureitisonlyassignedonce.Icanthendeclaretheoriginalvariableatitssecondassignment.NowIcancompileandtest,andallshouldwork.
Icontinueonthesecondassignmentofthevariable.Thisremovestheoriginalvariablenamecompletely,replacingitwithanewvariablenamedfortheseconduse.
functiondistanceTravelled(scenario,time){
letresult;
constprimaryAcceleration=scenario.primaryForce/scenario.mass;
letprimaryTime=Math.min(time,scenario.delay);
result=0.5*primaryAcceleration*primaryTime*primaryTime;
letsecondaryTime=time-scenario.delay;
if(secondaryTime>0){
letprimaryVelocity=primaryAcceleration*scenario.delay;
constsecondaryAcceleration=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;
result+=primaryVelocity*secondaryTime+
0.5*secondaryAcceleration*secondaryTime*secondaryTime;
}
returnresult;
}
I’msureyoucanthinkofalotmorerefactoringtobedonehere.Enjoyit.(I’msureit’sbetterthaneatingthehaggis—doyouknowwhattheyputinthosethings?)
Example:AssigningtoanInputParameter
Anothercaseofsplittingavariableiswherethevariableisdeclaredasaninputparameter.Considersomethinglike
functiondiscount(inputValue,quantity){
if(inputValue>50)inputValue=inputValue-2;
if(quantity>100)inputValue=inputValue-1;
returninputValue;
}
HereinputValueisusedbothtosupplyaninputtothefunctionandtoholdtheresultforthecaller.(SinceJavaScripthascall-by-valueparameters,anymodificationofinputValueisn’tseenbythecaller.)
Inthissituation,Iwouldsplitthatvariable.
functiondiscount(originalInputValue,quantity){
letinputValue=originalInputValue;
if(inputValue>50)inputValue=inputValue-2;
if(quantity>100)inputValue=inputValue-1;
returninputValue;
}
IthenperformRenameVariable(137)twicetogetbetternames.
functiondiscount(inputValue,quantity){
letresult=inputValue;
if(inputValue>50)result=result-2;
if(quantity>100)result=result-1;
returnresult;
}
You’llnoticethatIchangedthesecondlinetouseinputValueasitsdatasource.Althoughthetwoarethesame,Ithinkthatlineisreallyaboutapplyingthemodificationtotheresultvaluebasedontheoriginalinputvalue,notthe(coincidentallysame)valueoftheresultaccumulator.
RenameField
Motivation
Namesareimportant,andfieldnamesinrecordstructurescanbeespeciallyimportantwhenthoserecordstructuresarewidelyusedacrossaprogram.Datastructuresplayaparticularlyimportantroleinunderstanding.ManyyearsagoFredBrookssaid,“Showmeyourflowchartsandconcealyourtables,andIshallcontinuetobemystified.Showmeyourtables,andIwon’tusuallyneedyourflowcharts;they’llbeobvious.”WhileIdon’tseemanypeopledrawingflowchartsthesedays,theadageremainsvalid.Datastructuresarethekeytounderstandingwhat’sgoingon.
Sincethesedatastructuresaresoimportant,it’sessentialtokeepthemclear.Likeanythingelse,myunderstandingofdataimprovesthemoreIworkonthesoftware,soit’svitalthatthisimprovedunderstandingisembeddedintotheprogram.
Youmaywanttorenameafieldinarecordstructure,buttheideaalsoappliestoclasses.Getterandsettermethodsformaneffectivefieldforusersoftheclass.Renamingthemisjustasimportantaswithbarerecordstructures.
Mechanics
Iftherecordhaslimitedscope,renameallaccessestothefieldandtest;noneedtodotherestofthemechanics.
Iftherecordisn’talreadyencapsulated,applyEncapsulateRecord(160).
Renametheprivatefieldinsidetheobject,adjustinternalmethodstofit.
Test.
Iftheconstructorusesthename,applyChangeFunctionDeclaration(124)torenameit.
ApplyChangeFunctionDeclaration(124)totheaccessors.
Example:RenamingaField
I’llstartwithaconstant.
constorganization={name:"AcmeGooseberries",country:"GB"};
Iwanttochange“name”to“title”.Theobjectiswidelyusedinthecodebase,andthereareupdatestothetitleinthecode.SomyfirstmoveistoapplyEncapsulateRecord(160)
classOrganization{
constructor(data){
this._name=data.name;
this._country=data.country;
}
getname(){returnthis._name;}
setname(aString){this._name=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
constorganization=newOrganization({name:"AcmeGooseberries",country:"GB"});
NowthatI’veencapsulatedtherecordstructureintotheclass,therearefourplacesIneedtolookatforrenaming:thegettingfunction,thesettingfunction,theconstructor,andtheinternaldatastructure.WhilethatmaysoundlikeI’veincreasedmyworkload,itactuallymakesmyworkeasiersinceIcannowchangetheseindependentlyinsteadofallatonce,takingsmallersteps.Smallerstepsmeanfewerthingstogowrongineachstep—therefore,lesswork.Itwouldn’tbelessworkifInevermademistakes—butnotmakingmistakesisafantasyIgaveuponalongtimeago.
SinceI’vecopiedtheinputdatastructureintotheinternaldatastructure,IneedtoseparatethemsoIcanworkonthemindependently.Icandothisbydefiningaseparatefieldandadjustingtheconstructorandaccessorstouseit.
classOrganization…
classOrganization{
constructor(data){
this._title=data.name;
this._country=data.country;
}
getname(){returnthis._title;}
setname(aString){this._title=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
Next,Iaddsupportforusing“title”intheconstructor.
classOrganization…
classOrganization{
constructor(data){
this._title=(data.title!==undefined)?data.title:data.name;
this._country=data.country;
}
getname(){returnthis._title;}
setname(aString){this._title=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
Now,callersofmyconstructorcanuseeithernameortitle(withtitletakingprecedence).Icannowgothroughallconstructorcallersandchangethemoneby-onetousethenewname.
constorganization=newOrganization({title:"AcmeGooseberries",country:"GB"});
OnceI’vedoneallofthem,Icanremovethesupportforthename.
classOrganization…
classOrganization{
constructor(data){
this._title=data.title;
this._country=data.country;
}
getname(){returnthis._title;}
setname(aString){this._title=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
Nowthattheconstructoranddatausethenewname,Icanchangetheaccessors,whichisassimpleasapplyingChangeFunctionDeclaration(124)toeachone.
classOrganization…
classOrganization{
constructor(data){
this._title=data.title;
this._country=data.country;
}
gettitle(){returnthis._title;}
settitle(aString){this._title=aString;}
getcountry(){returnthis._country;}
setcountry(aCountryCode){this._country=aCountryCode;}
}
I’veshownthisprocessinitsmostheavyweightformneededforawidelyuseddatastructure.Ifit’sbeingusedonlylocally,asinasinglefunction,Icanprobablyjustrenamethevariouspropertiesinonegowithoutdoingencapsulation.It’samatterofjudgmentwhentoapplytothefullmechanicshere—but,asusualwithrefactoring,ifmytestsbreak,that’sasignIneedtousethemoregradualprocedure.
Somelanguagesallowmetomakeadatastructureimmutable.Inthiscase,ratherthanencapsulatingit,Icancopythevaluetothenewname,graduallychangetheusers,thenremovetheoldname.Duplicatingdataisarecipefordisasterwithmutabledatastructures;removingsuchdisastersiswhyimmutabledataissopopular.
ReplaceDerivedVariablewithQuery
Motivation
Oneofthebiggestsourcesofproblemsinsoftwareismutabledata.Datachangescanoftencoupletogetherpartsofcodeinawkwardways,withchangesinonepartofleadingtoknock-oneffectsthatarehardtospot.Inmanysituationsit’snotrealistictoentirelyremovemutabledata—butIdoadvocateminimizingthescopeofmutabledataatmuchaspossible.
OnewayIcanmakeabigimpactisbyremovinganyvariablesthatIcouldjustaseasilycalculate.Acalculationoftenmakesitclearerwhatthemeaningofthedatais,anditisprotectedfrombeingcorruptedwhenyoufailtoupdatethevariableasthesourcedatachanges.
Areasonableexceptiontothisiswhenthesourcedataforthecalculationisimmutableandwecanforcetheresulttobeingimmutabletoo.Transformationoperationsthatcreatenewdatastructuresarethusreasonabletokeepeveniftheycouldbereplacedwithcalculations.Indeed,thereisadualityherebetweenobjectsthatwrapadatastructurewithaseriesofcalculatedpropertiesandfunctionsthattransformonedatastructureintoanother.Theobjectrouteisclearlybetterwhenthesourcedatachangesandyouwouldhavetomanagethelifetimeofthederiveddatastructures.Butifthesourcedataisimmutable,orthederiveddataisverytransient,thenbothapproachesareeffective.
Mechanics
Identifyallpointsofupdateforthevariable.Ifnecessary,useSplitVariable(240)toseparateeachpointofupdate.
Createafunctionthatcalculatesthevalueofthevariable.
UseIntroduceAssertion(299)toassertthatthevariableandthecalculationgivethesameresultwheneverthevariableisused.
Ifnecessary,useEncapsulateVariable(132)toprovideahomefortheassertion.
Test.
Replaceanyreaderofthevariablewithacalltothenewfunction.
Test.
ApplyRemoveDeadCode(236)tothedeclarationandupdatestothevariable.
Example
Here’sasmallbutperfectlyformedexampleofugliness.
classProductionPlan…
getproduction(){returnthis._production;}
applyAdjustment(anAdjustment){
this._adjustments.push(anAdjustment);
this._production+=anAdjustment.amount;
}
Uglinessisintheeyeofbeholder;here,Iseeuglinessinduplication—notthecommonduplicationofcodebutduplicationofdata.WhenIapplyanadjustment,I’mnotjuststoringthatadjustmentbutalsousingittomodifyanaccumulator.Icanjustcalculatethatvalue,withouthavingtoupdateit.
ButI’macautiousfellow.ItismyhypothesisisthatIcanjustcalculateit—IcantestthathypothesisbyusingIntroduceAssertion(299):
classProductionPlan…
getproduction(){
assert(this._production===this.calculatedProduction);
returnthis._production;
}
getcalculatedProduction(){
returnthis._adjustments
.reduce((sum,a)=>sum+a.amount,0);
}
Withtheassertioninplace,Irunmytests.Iftheassertiondoesn’tfail,Icanreplacereturningthefieldwithreturningthecalculation:
classProductionPlan…
getproduction(){
assert(this._production===this.calculatedProduction);
returnthis.calculatedProduction;
}
ThenInlineFunction(115):
classProductionPlan…
getproduction(){
returnthis._adjustments
.reduce((sum,a)=>sum+a.amount,0);
}
IcleanupanyreferencestotheoldvariablewithRemoveDeadCode(236):
classProductionPlan…
applyAdjustment(anAdjustment){
this._adjustments.push(anAdjustment);
this._production+=anAdjustment.amount;
}
Example:MoreThanOneSource
Theaboveexampleisniceandeasybecausethere’sclearlyasinglesourceforthevalueofproduction.Butsometimes,morethanoneelementcancombineintheaccumulator.
classProductionPlan…
constructor(production){
this._production=production;
this._adjustments=[];
}
getproduction(){returnthis._production;}
applyAdjustment(anAdjustment){
this._adjustments.push(anAdjustment);
this._production+=anAdjustment.amount;
}
IfIdothesameIntroduceAssertion(299)thatIdidabove,itwillnowfailforanycasewheretheinitialvalueoftheproductionisn’tzero.
ButIcanstillreplacethederiveddata.TheonlydifferenceisthatImustfirstapplySplitVariable(240).
constructor(production){
this._initialProduction=production;
this._productionAccumulator=0;
this._adjustments=[];
}
getproduction(){
returnthis._initialProduction+this._productionAccumulator;
}
NowIcanIntroduceAssertion(299)
classProductionPlan…
getproduction(){
assert(this._productionAccumulator===this.calculatedProductionAccumulator);
returnthis._initialProduction+this._productionAccumulator;
}
getcalculatedProductionAccumulator(){
returnthis._adjustments
.reduce((sum,a)=>sum+a.amount,0);
}
Andcontinueprettymuchasbefore.I’dbeinclined,however,toleavetotalProductionAjustmentsasitsownproperty,withoutinliningit.
ChangeReferencetoValue
inverseof:ChangeValuetoReference(256)
Motivation
WhenInestanobject,ordatastructure,withinanotherIcantreattheinnerobjectasareferenceorasavalue.ThedifferenceismostobviouslyvisibleinhowIhandleupdatesoftheinnerobject’sproperties.IfItreatitasareference,I’llupdatetheinnerobject’spropertykeepingthesameinnerobject.IfItreatitasavalue,Iwillreplacetheentireinnerobjectwithanewonethathasthedesiredproperty.
IfItreatafieldasavalue,IcanchangetheclassoftheinnerobjecttomakeitaValueObject(https://martinfowler.com/bliki/ValueObject.html).Valueobjectsaregenerallyeasiertoreasonabout,particularlybecausetheyareimmutable.Ingeneral,immutabledatastructuresareeasiertodealwith.Icanpassanimmutabledatavalueouttootherpartsoftheprogramandnotworrythatitmightchangewithouttheenclosingobjectbeingawareofthechange.Icanreplicatevaluesaroundmyprogramandnotworryaboutmaintainingmemorylinks.Valueobjectsareespeciallyusefulindistributedandconcurrentsystems.
ThisalsosuggestswhenIshouldn’tdothisrefactoring.IfIwanttoshareanobjectbetweenseveralobjectssothatanychangetothesharedobjectisvisibletoallitscollaborators,thenIneedthesharedobjecttobeareference.
Mechanics
Checkthatthecandidateclassisimmutableorcanbecomeimmutable.
Foreachsetter,applyRemoveSettingMethod(329).
Provideavalue-basedequalitymethodthatusesthefieldsofthevalueobject.
Mostlanguageenvironmentsprovideanoverridableequalityfunctionforthispurpose.Usuallyyoumustoverrideahashcodegeneratormethodaswell.
Example
Imaginewehaveapersonobjectthatholdsontoacrudetelephonenumber.
classPerson…
constructor(){
this._telephoneNumber=newTelephoneNumber();
}
getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}
setofficeAreaCode(arg){this._telephoneNumber.areaCode=arg;}
getofficeNumber(){returnthis._telephoneNumber.number;}
setofficeNumber(arg){this._telephoneNumber.number=arg;}
classTelephoneNumber…
getareaCode(){returnthis._areaCode;}
setareaCode(arg){this._areaCode=arg;}
getnumber(){returnthis._number;}
setnumber(arg){this._number=arg;}
ThissituationistheresultofanExtractClass(180)wheretheoldparentstillholdsupdatemethodsforthenewobject.ThisisagoodtimetoapplyChangeReferencetoValuesincethereisonlyonereferencetothenewclass.
ThefirstthingIneedtodoistomakethetelephonenumberimmutable.IdothisbyapplyingRemoveSettingMethod(329)tothefields.ThefirststepofRemoveSettingMethod(329)istouseChangeFunctionDeclaration(124)toaddthetwofieldstotheconstructorandenhancetheconstructortocallthesetters.
classTelephoneNumber…
constructor(areaCode,number){
this._areaCode=areaCode;
this._number=number;
}
NowIlookatthecallersofthesetters.Foreachone,Ineedtochangeittoare-assignment.Istartwiththeareacode.
classPerson…
getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}
setofficeAreaCode(arg){
this._telephoneNumber=newTelephoneNumber(arg,this.officeNumber);
}
getofficeNumber(){returnthis._telephoneNumber.number;}
setofficeNumber(arg){this._telephoneNumber.number=arg;}
Ithenrepeatthatstepwiththeremainingfield.
classPerson…
getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}
setofficeAreaCode(arg){
this._telephoneNumber=newTelephoneNumber(arg,this.officeNumber);
}
getofficeNumber(){returnthis._telephoneNumber.number;}
setofficeNumber(arg){
this._telephoneNumber=newTelephoneNumber(this.officeAreaCode,arg);
}
Nowthetelephonenumberisimmutable,itisreadytobecomeatruevalue.Thecitizenshiptestforavalueobjectisthatitusesvalue-basedequality.ThisisanareawhereJavaScriptfallsdown,asthereisnothinginthelanguageandcorelibrariesthatunderstandsreplacingareference-basedequalitywithavalue-basedone.ThebestIcandoistocreatemyownequalsmethod.
classTelephoneNumber…
equals(other){
if(!(otherinstanceofTelephoneNumber))returnfalse;
returnthis.areaCode===other.areaCode&&
this.number===other.number;
}
It’salsoimportanttotestitwithsomethinglike
it('telephoneequals',function(){
assert(newTelephoneNumber("312","555-0142")
.equals(newTelephoneNumber("312","555-0142")));
});
TheunusualformattingIusehereshouldmakeitobviousthattheyarethesameconstructorcall.
ThevitalthingIdointhetestiscreatetwoindependentobjectsandtestthattheymatchasequal.
Inmostobject-orientedlanguages,thereisabuilt-inequalitytestthatissupposedtooverriddenforvalue-basedequality.InRuby,Icanoverridethe==operator;inJava,IoverridetheObject.equals()method.AndwheneverIoverrideanequalitymethod,Iusuallyneedtooverrideahashcodegeneratingmethodtoo(e.g.Object.hashCode()inJava)toensurecollectionsthatusehashingworkproperlywithmynewvalue.
Ifthetelephonenumberisusedbymorethanoneclient,theprocedureisstillthesame.AsIapplyRemoveSettingMethod(329),I’llbemodifyingseveralclientsinsteadofjustone.Testsfornon-equaltelephonenumbers,aswellascomparisonstonon-telephone-numbersandnullvalues,arealsoworthwhile.
ChangeValuetoReference
inverseof:ChangeReferencetoValue(252)
Motivation
Adatastructuremayhaveseveralrecordslinkedtothesamelogicaldatastructure.Imightreadinalistoforders,someofwhichareforthesamecustomer.WhenIhavesharinglikethis,Icanrepresentitbytreatingthecustomereitherasavalueorasareference.Withavalue,thecustomerdataiscopiedintoeachorder;withareference,thereisonlyonedatastructurethatmultipleorderslinkto.
Ifthecustomerneverneedstobeupdated,thenbothapproachesarereasonable.Itis,perhaps,abitconfusingtohavemultiplecopiesofthesamedata,butit’scommonenoughtonotbeaproblem.Insomecases,theremaybeissueswithmemoryduetomultiplecopies—but,likeanyperformanceissue,that’srelativelyrare.
ThebiggestdifficultyinhavingphysicalcopiesofthesamelogicaldataoccurswhenIneedtoupdatetheshareddata.Ithenhavetofindallthecopiesandupdatethemall.IfImissone,I’llgetatroublinginconsistencyinmydata.Inthiscase,it’softenworthwhiletochangethecopieddataintoasinglereference.Thatway,anychangeisvisibletoallthecustomer’sorders.
Changingavaluetoareferenceresultsinonlyoneobjectbeingpresentforanentity,anditusuallymeansIneedsomekindofrepositorywhereIcanaccesstheseobjects.Ithenonlycreatetheobjectforanentityonce,andeverywhereelseIretrieveitfromtherepository.
Mechanics
Createarepositoryforinstancesoftherelatedobject(ifoneisn’talreadypresent).
Ensuretheconstructorhasawayoflookingupthecorrectinstanceoftherelatedobject.
Changetheconstructorsforthehostobjecttousetherepositorytoobtaintherelatedobject.Testaftereachchange.
Example
I’llbeginwithaclassthatrepresentsorders,whichImightcreatefromanincomingJSONdocument.PartoftheorderdataisacustomerIDfromwhichI’mcreatingacustomerobject.
classOrder…
constructor(data){
this._number=data.number;
this._customer=newCustomer(data.customer);
//loadotherdata
}
getcustomer(){returnthis._customer;}
classCustomer…
constructor(id){
this._id=id;
}
getid(){returnthis._id;}
ThecustomerobjectIcreatethiswayisavalue.IfIhavefiveordersthatrefertothecustomerIDof123,I’llhavefiveseparatecustomerobjects.AnychangeImaketooneofthemwillnotbereflectedintheothers.ShouldIwanttoenrich
thecustomerobjects,perhapsbygatheringdatafromacustomerservice,I’dhavetoupdateallfivecustomerswiththesamedata.Havingduplicateobjectslikethisalwaysmakesmenervous—it’sconfusingtohavemultipleobjectsrepresentingthesameentity,suchasacustomer.Thisproblemisparticularlyawkwardifthecustomerobjectismutable,whichcanleadtoinconsistenciesbetweenthecustomerobjects.
IfIwanttousethesamecustomerobjecteachtime,I’llneedaplacetostoreit.Exactlywheretostoreentitieslikethiswillvaryfromapplicationtoapplication,butforasimplecaseIliketousearepositoryobject[bib-repository].
let_repositoryData;
exportfunctioninitialize(){
_repositoryData={};
_repositoryData.customers=newMap();
}
exportfunctionregisterCustomer(id){
if(!_repositoryData.customers.has(id))
_repositoryData.customers.set(id,newCustomer(id));
returnfindCustomer(id);
}
exportfunctionfindCustomer(id){
return_repositoryData.customers.get(id);
}
TherepositoryallowsmetoregistercustomerobjectswithanIDandensuresIonlycreateonecustomerobjectwiththesameID.Withthisinplace,Icanchangetheorder’sconstructortouseit.
Often,whendoingthisrefactoring,therepositoryalreadyexists,soIcanjustuseit.
Thenextstepistofigureouthowtheconstructorfortheordercanobtainthecorrectcustomerobject.Inthiscaseit’seasy,sincethecustomer’sIDispresentintheinputdatastream.
classOrder…
constructor(data){
this._number=data.number;
this._customer=registerCustomer(data.customer);
//loadotherdata
}
getcustomer(){returnthis._customer;}
Now,anychangesImaketothecustomerofoneorderwillbesynchronizedacrossalltheorderssharingthesamecustomer.
Forthisexample,Icreatedanewcustomerobjectwiththefirstorderthatreferencedit.Anothercommonapproachistogetalistofcustomers,populatetherepositorywiththem,andthenlinktothemasIreadtheorders.Inthatcase,anorderthatcontainsacustomerIDnotintherepositorywouldindicateanerror.
Oneproblemwiththiscodeisthattheconstructorbodyiscoupledtotheglobalrepository.Globalsshouldbetreatedwithcare—likeapowerfuldrug,theycanbebeneficialinsmalldosesbutapoisonifusedtoomuch.IfI’mconcernedaboutit,Icanpasstherepositoryasaparametertotheconstructor.
Chapter10SimplifyingConditionalLogicMuchofthepowerofprogramscomesfromtheirabilitytoimplementconditionallogic—but,sadly,muchofthecomplexityofprogramsliesintheseconditionals.Ioftenuserefactoringtomakeconditionalsectionseasiertounderstand.IregularlyapplyDecomposeConditional(260)tocomplicatedconditionals,andIuseConsolidateConditionalExpression(263)tomakelogicalcombinationsclearer.IuseReplaceNestedConditionalwithGuardClauses(266)toclarifycaseswhereIwanttorunsomepre-checksbeforemymainprocessing.IfIseeseveralconditionsusingthesameswitchinglogic,it’sagoodtimetopullReplaceConditionalwithPolymorphism(271)outthebox.
Alotofconditionalsareusedtohandlespecialcases,suchasnulls;ifthatlogicismostlythesame,thenIntroduceSpecialCase(287)(oftenreferredtoasIntroduceSpecialCase(287))canremovealotofduplicatecode.And,althoughIliketoremoveconditionsalot,ifIwanttocommunicate(andcheck)aprogram’sstate,IfindIntroduceAssertion(299)aworthwhileaddition.
DecomposeConditional
Motivation
Oneofthemostcommonsourcesofcomplexityinaprogramiscomplexconditionallogic.AsIwritecodetodovariousthingsdependingonvariousconditions,Icanquicklyendupwithaprettylongfunction.Lengthofafunctionisinitselfafactorthatmakesithardertoread,butconditionsincreasethedifficulty.Theproblemusuallyliesinthefactthatthecode,bothintheconditionchecksandintheactions,tellsmewhathappensbutcaneasilyobscurewhyithappens.
Aswithanylargeblockofcode,Icanmakemyintentionclearerbydecomposingitandreplacingeachchunkofcodewithafunctioncallnamedaftertheintentionofthatchunk.Withconditions,Iparticularlylikedoingthisfortheconditionalpartandeachofthealternatives.Thisway,IhighlighttheconditionandmakeitclearwhatI’mbranchingon.Ialsohighlightthereasonforthebranching.
ThisisreallyjustaparticularcaseofapplyingExtractFunction(106)tomycode,butIliketohighlightthiscaseasonewhereI’veoftenfoundaremarkablygoodvaluefortheexercise.
Mechanics
ApplyExtractFunction(106)ontheconditionandeachlegoftheconditional.
Example
SupposeI’mcalculatingthechargeforsomethingthathasseparateratesforwinterandsummer:
if(!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd))
charge=quantity*plan.summerRate;
else
charge=quantity*plan.regularRate+plan.regularServiceCharge;
Iextracttheconditionintoitsownfunction.
if(summer())
charge=quantity*plan.summerRate;
else
charge=quantity*plan.regularRate+plan.regularServiceCharge;
functionsummer(){
return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);
}
ThenIdothethenleg:
if(summer())
charge=summerCharge();
else
charge=quantity*plan.regularRate+plan.regularServiceCharge;
functionsummer(){
return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);
}
functionsummerCharge(){
returnquantity*plan.summerRate;
}
Finally,theelseleg:
if(summer())
charge=summerCharge();
else
charge=regularCharge();
functionsummer(){
return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);
}
functionsummerCharge(){
returnquantity*plan.summerRate;
}
functionregularCharge(){
returnquantity*plan.regularRate+plan.regularServiceCharge;
}
Withthatdone,Iliketoreformattheconditionalusingtheternaryoperator.
charge=summer()?summerCharge():regularCharge();
functionsummer(){
return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);
}
functionsummerCharge(){
returnquantity*plan.summerRate;
}
functionregularCharge(){
returnquantity*plan.regularRate+plan.regularServiceCharge;
}
ConsolidateConditionalExpression
Motivation
Sometimes,Irunintoaseriesofconditionalcheckswhereeachcheckisdifferentyettheresultingactionisthesame.WhenIseethis,Iuseandandoroperatorstoconsolidatethemintoasingleconditionalcheckwithasingleresult.
Consolidatingtheconditionalcodeisimportantfortworeasons.First,itmakesitclearerbyshowingthatI’mreallymakingasinglecheckthatcombinesotherchecks.Thesequencehasthesameeffect,butitlookslikeI’mcarryingoutasequenceofseparatechecksthatjusthappentobeclosetogether.ThesecondreasonIliketodothisisthatitoftensetsmeupforExtractFunction(106).ExtractingaconditionisoneofthemostusefulthingsIcandotoclarifymycode.ItreplacesastatementofwhatI’mdoingwithwhyI’mdoingit.
Thereasonsinfavorofconsolidatingconditionalsalsopointtothereasonsagainstdoingit.IfIconsiderittobetrulyindependentchecksthatshouldn’tbethoughtofasasinglecheck,Idon’tdotherefactoring.
Mechanics
Ensurethatnoneoftheconditionalshaveanysideeffects.
Ifanydo,useSeparateQueryfromModifier(304)onthemfirst.
Taketwooftheconditionalstatementsandcombinetheirconditionsusingalogicaloperator.
Sequencescombinewithor,nestedifstatementscombinewithand.
Test.
Repeatcombiningconditionalsuntiltheyareallinasinglecondition.
ConsiderusingExtractFunction(106)ontheresultingcondition.
Example
Perusingsomecode,Iseethefollowing:
functiondisabilityAmount(anEmployee){
if(anEmployee.seniority<2)return0;
if(anEmployee.monthsDisabled>12)return0;
if(anEmployee.isPartTime)return0;
//computethedisabilityamount
It’sasequenceofconditionalcheckswhichallhavethesameresult.Sincetheresultisthesame,Ishouldcombinetheseconditionsintoasingleexpression.Forasequencelikethis,Idoitusinganoroperator.
functiondisabilityAmount(anEmployee){
if((anEmployee.seniority<2)
||(anEmployee.monthsDisabled>12))return0;
if(anEmployee.isPartTime)return0;
//computethedisabilityamount
Itest,thenfoldintheothercondition:
functiondisabilityAmount(anEmployee){
if((anEmployee.seniority<2)
||(anEmployee.monthsDisabled>12)
||(anEmployee.isPartTime))return0;
//computethedisabilityamount
OnceIhavethemalltogether,IuseExtractFunction(106)onthecondition.
functiondisabilityAmount(anEmployee){
if(isNotEligableForDisability())return0;
//computethedisabilityamount
functionisNotEligableForDisability(){
return((anEmployee.seniority<2)
||(anEmployee.monthsDisabled>12)
||(anEmployee.isPartTime));
}
Example:Usingands
Theexampleaboveshowedcombiningstatementswithanor,butImayrunintocasesthatneedandsaswell.Suchacaseusesnestedifstatements:
if(anEmployee.onVacation)
if(anEmployee.seniority>10)
return1;
return0.5;
Icombinetheseusingandoperators.
if((anEmployee.onVacation)
&&(anEmployee.seniority>10))return1;
return0.5;
IfIhaveamixofthese,Icancombineusingandandoroperatorsasneeded.Whenthishappens,thingsarelikelytogetmessy,soIuseExtractFunction(106)liberallytomakeitallunderstandable.
ReplaceNestedConditionalwithGuardClauses
Motivation
Ioftenfindthatconditionalexpressionscomeintwostyles.Inthefirststyle,bothlegsoftheconditionalarepartofnormalbehavior,whileinthesecondstyle,onelegisnormalandtheotherindicatesanunusualcondition.
Thesekindsofconditionalshavedifferentintentions—andtheseintentionsshouldcomethroughinthecode.Ifbotharepartofnormalbehavior,Iuseaconditionwithanifandanelseleg.Iftheconditionisanunusualcondition,Ichecktheconditionandreturnifit’strue.Thiskindofcheckisoftencalledaguardclause.
ThekeypointofReplaceNestedConditionalwithGuardClausesisemphasis.IfI’musinganif-then-elseconstruct,I’mgivingequalweighttotheiflegandtheelseleg.Thiscommunicatestothereaderthatthelegsareequallylikelyandimportant.Instead,theguardclausesays,“Thisisn’tthecoretothisfunction,andifithappens,dosomethingandgetout.”
IoftenfindIuseReplaceNestedConditionalwithGuardClauseswhenI’mworkingwithaprogrammerwhohasbeentaughttohaveonlyoneentrypointandoneexitpointfromamethod.Oneentrypointisenforcedbymodernlanguages,butoneexitpointisreallynotausefulrule.Clarityisthekeyprinciple:Ifthemethodisclearerwithoneexitpoint,useoneexitpoint;otherwisedon’t.
Mechanics
Selectoutermostconditionthatneedstobereplaced,andchangeitintoaguardclause.
Test.
Repeatasneeded.
Ifalltheguardclausesreturnthesameresult,useConsolidateConditionalExpression(263).
Example
Here’ssomecodetocalculateapaymentamountforanemployee.It’sonlyrelevantiftheemployeeisstillwiththecompany,soithastocheckforthetwoothercases.
functionpayAmount(employee){
letresult;
if(employee.isSeparated){
result={amount:0,reasonCode:"SEP"};
}
else{
if(employee.isRetired){
result={amount:0,reasonCode:"RET"};
}
else{
//logictocomputeamount
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result=someFinalComputation();
}
}
returnresult;
}
Nestingtheconditionalsheremasksthetruemeaningofwhatitgoingon.Theprimarypurposeofthiscodeonlyappliesiftheseconditionsaren’tthecase.Inthissituation,theintentionofthecodereadsmoreclearlywithguardclauses.
Aswithanyrefactoringchange,Iliketotakesmallsteps,soIbeginwiththetopmostcondition.
functionpayAmount(employee){
letresult;
if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};
if(employee.isRetired){
result={amount:0,reasonCode:"RET"};
}
else{
//logictocomputeamount
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result=someFinalComputation();
}
returnresult;
}
Itestthatchangeandmoveontothenextone.
functionpayAmount(employee){
letresult;
if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};
if(employee.isRetired)return{amount:0,reasonCode:"RET"};
//logictocomputeamount
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result=someFinalComputation();
returnresult;
}
Atwhichpointtheresultvariableisn’treallydoinganythinguseful,soIremoveit.
functionpayAmount(employee){
letresult;
if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};
if(employee.isRetired)return{amount:0,reasonCode:"RET"};
//logictocomputeamount
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
returnsomeFinalComputation();
}
Theruleisthatyoualwaysgetanextrastrawberrywhenyouremoveamutablevariable.
Example:ReversingtheConditions
Whenreviewingthemanuscriptofthefirsteditionofthisbook,JoshuaKerievskypointedoutthatweoftendoReplaceNestedConditionalwithGuardClausesbyreversingtheconditionalexpressions.Evenbetter,hegavemeanexamplesoIdidn’thavetofurthertaxmyimagination.
functionadjustedCapital(anInstrument){
letresult=0;
if(anInstrument.capital>0){
if(anInstrument.interestRate>0&&anInstrument.duration>0){
result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
}
}
returnresult;
}
Again,Imakethereplacementsoneatatime,butthistimeIreversetheconditionasIputintheguardclause.
functionadjustedCapital(anInstrument){
letresult=0;
if(anInstrument.capital<=0)returnresult;
if(anInstrument.interestRate>0&&anInstrument.duration>0){
result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
}
returnresult;
}
Thenextconditionalisabitmorecomplicated,soIdoitintwosteps.First,Isimplyaddanot.
functionadjustedCapital(anInstrument){
letresult=0;
if(anInstrument.capital<=0)returnresult;
if(!(anInstrument.interestRate>0&&anInstrument.duration>0))returnresult;
result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
returnresult;
}
Leavingnotsinaconditionallikethattwistsmymindaroundatapainfulangle,soIsimplifyit:
functionadjustedCapital(anInstrument){
letresult=0;
if(anInstrument.capital<=0)returnresult;
if(anInstrument.interestRate<=0||anInstrument.duration<=0)returnresult;
result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
returnresult;
}
Bothofthoselineshaveconditionswiththesameresult,soIapplyConsolidateConditionalExpression(263).
functionadjustedCapital(anInstrument){
letresult=0;
if(anInstrument.capital<=0
||anInstrument.interestRate<=0
||anInstrument.duration<=0)returnresult;
result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
returnresult;
}
Theresultvariableisdoingtwothingshere.Itsfirstsettingtozeroindicateswhattoreturnwhentheguardclausetriggers;itssecondvalueisthefinalcomputation.Icangetridofit,whichbotheliminatesitsdoubleusageandgetsmeastrawberry.
functionadjustedCapital(anInstrument){
if(anInstrument.capital<=0
||anInstrument.interestRate<=0
||anInstrument.duration<=0)return0;
return(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;
}
ReplaceConditionalwithPolymorphism
Motivation
Complexconditionallogicisoneofthehardestthingstoreasonaboutinprogramming,soIalwayslookforwaystoaddstructuretoconditionallogic.Often,IfindIcanseparatethelogicintodifferentcircumstances—high-levelcases—todividetheconditions.Sometimesit’senoughtorepresentthisdivisionwithinthestructureofaconditionalitself,butusingclassesandpolymorphismcanmaketheseparationmoreexplicit.
AcommoncaseforthisiswhereIcanformasetoftypes,eachhandlingtheconditionallogicdifferently.Imightnoticethatbooks,music,andfoodvaryinhowtheyarehandledbecauseoftheirtype.Thisismademostobviouswhenthereareseveralfunctionsthathaveaswitchstatementonatypecode.Inthatcase,Iremovetheduplicationofthecommonswitchlogicbycreatingclassesforeachcaseandusingpolymorphismtobringoutthetype-specificbehavior.
AnothersituationiswhereIcanthinkofthelogicasabasecasewithvariants.Thebasecasemaybethemostcommonormoststraightforward.Icanputthislogicintoasuperclasswhichallowsmetoreasonaboutitwithouthavingtoworryaboutthevariants.Ithenputeachvariantcaseintoasubclass,whichIexpresswithcodethatemphasizesitsdifferencefromthebasecase.
Polymorphismisoneofthekeyfeaturesofobject-orientedprogramming—and,likeanyusefulfeature,it’spronetooveruse.I’vecomeacrosspeoplewhoarguethatallexamplesofconditionallogicshouldbereplacedwithpolymorphism.Idon’tagreewiththatview.Mostofmyconditionallogicusesbasicconditionalstatements—if/elseandswitch/case.ButwhenIseecomplexconditionallogicthatcanbeimprovedasdiscussedabove,Ifindpolymorphismapowerfultool.
Mechanics
Ifclassesdonotexistforpolymorphicbehavior,createthemtogetherwithafactoryfunctiontoreturnthecorrectinstance.
Usethefactoryfunctionincallingcode.
Movetheconditionalfunctiontothesuperclass.
Iftheconditionallogicisnotaself-containedfunction,useExtractFunction(106)tomakeitso.
Pickoneofthesubclasses.Createasubclassmethodthatoverridestheconditionalstatementmethod.Copythebodyofthatlegoftheconditionalstatementintothesubclassmethodandadjustittofit.
Repeatforeachlegoftheconditional.
Leaveadefaultcaseforthesuperclassmethod.Or,ifsuperclassshouldbe
abstract,declarethatmethodasabstractorthrowanerrortoshowitshouldbetheresponsibilityofasubclass.
Example
Myfriendhasacollectionofbirdsandwantstoknowhowfasttheycanflyandwhattheyhaveforplumage.Sowehaveacoupleofsmallprogramstodeterminetheinformation.
functionplumages(birds){
returnnewMap(birds.map(b=>[b.name,plumage(b)]));
}
functionspeeds(birds){
returnnewMap(birds.map(b=>[b.name,airSpeedVelocity(b)]));
}
functionplumage(bird){
switch(bird.type){
case'EuropeanSwallow':
return"average";
case'AfricanSwallow':
return(bird.numberOfCoconuts>2)?"tired":"average";
case'NorwegianBlueParrot':
return(bird.voltage>100)?"scorched":"beautiful";
default:
return"unknown";
}
}
functionairSpeedVelocity(bird){
switch(bird.type){
case'EuropeanSwallow':
return35;
case'AfricanSwallow':
return40-2*bird.numberOfCoconuts;
case'NorwegianBlueParrot':
return(bird.isNailed)?0:10+bird.voltage/10;
default:
returnnull;
}
}
Wehaveacoupleofdifferentoperationsthatvarywiththetypeofbird,soitmakessensetocreateclassesandusepolymorphismforanytype-specificbehavior.
IbeginbyusingCombineFunctionsintoClass(144)onairSpeedVelocityandplumage
functionplumage(bird){
returnnewBird(bird).plumage;
}
functionairSpeedVelocity(bird){
returnnewBird(bird).airSpeedVelocity;
}
classBird{
constructor(birdObject){
Object.assign(this,birdObject);
}
getplumage(){
switch(this.type){
case'EuropeanSwallow':
return"average";
case'AfricanSwallow':
return(this.numberOfCoconuts>2)?"tired":"average";
case'NorwegianBlueParrot':
return(this.voltage>100)?"scorched":"beautiful";
default:
return"unknown";
}
}
getairSpeedVelocity(){
switch(this.type){
case'EuropeanSwallow':
return35;
case'AfricanSwallow':
return40-2*this.numberOfCoconuts;
case'NorwegianBlueParrot':
return(this.isNailed)?0:10+this.voltage/10;
default:
returnnull;
}
}
}
Inowaddsubclassesforeachkindofbird,togetherwithafactoryfunctiontoinstantiatetheappropriatesubclass.
functionplumage(bird){
returncreateBird(bird).plumage;
}
functionairSpeedVelocity(bird){
returncreateBird(bird).airSpeedVelocity;
}
functioncreateBird(bird){
switch(bird.type){
case'EuropeanSwallow':
returnnewEuropeanSwallow(bird);
case'AfricanSwallow':
returnnewAfricanSwallow(bird);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrot(bird);
default:
returnnewBird(bird);
}
}
classEuropeanSwallowextendsBird{
}
classAfricanSwallowextendsBird{
}
classNorwegianBlueParrotextendsBird{
}
NowthatI’vecreatedtheclassstructurethatIneed,Icanbeginonthetwoconditionalmethods.I’llbeginwithplumage.Itakeonelegoftheswitchstatementandoverrideitintheappropriatesubclass.
classEuropeanSwallow…
getplumage(){
return"average";
}
classBird…
getplumage(){
switch(this.type){
case'EuropeanSwallow':
throw"oops";
case'AfricanSwallow':
return(this.numberOfCoconuts>2)?"tired":"average";
case'NorwegianBlueParrot':
return(this.voltage>100)?"scorched":"beautiful";
default:
return"unknown";
}
}
IputinthethrowbecauseI’mparanoid.
Icancompileandtestatthispoint.Then,ifalliswell,Idothenextleg.
classAfricanSwallow…
getplumage(){
return(this.numberOfCoconuts>2)?"tired":"average";
}
Then,theNorwegianBlue.
classNorwegianBlueParrot…
getplumage(){
return(this.voltage>100)?"scorched":"beautiful";
}
Ileavethesuperclassmethodforthedefaultcase.
classBird…
getplumage(){
return"unknown";
}
IrepeatthesameprocessforairSpeedVelocity.OnceI’mdone,Iendupwiththefollowingcode(Ialsoinlinedthetop-levelfunctionsforairSpeedVelocityandplumage):
functionplumages(birds){
returnnewMap(birds
.map(b=>createBird(b))
.map(bird=>[bird.name,bird.plumage]));
}
functionspeeds(birds){
returnnewMap(birds
.map(b=>createBird(b))
.map(bird=>[bird.name,bird.airSpeedVelocity]));
}
functioncreateBird(bird){
switch(bird.type){
case'EuropeanSwallow':
returnnewEuropeanSwallow(bird);
case'AfricanSwallow':
returnnewAfricanSwallow(bird);
case'NorwegianBlueParrot':
returnnewNorwegianBlueParrot(bird);
default:
returnnewBird(bird);
}
}
classBird{
constructor(birdObject){
Object.assign(this,birdObject);
}
getplumage(){
return"unknown";
}
getairSpeedVelocity(){
returnnull;
}
}
classEuropeanSwallowextendsBird{
getplumage(){
return"average";
}
getairSpeedVelocity(){
return35;
}
}
classAfricanSwallowextendsBird{
getplumage(){
return(this.numberOfCoconuts>2)?"tired":"average";
}
getairSpeedVelocity(){
return40-2*this.numberOfCoconuts;
}
}
classNorwegianBlueParrotextendsBird{
getplumage(){
return(this.voltage>100)?"scorched":"beautiful";
}
getairSpeedVelocity(){
return(this.isNailed)?0:10+this.voltage/10;
}
}
Lookingatthisfinalcode,IcanseethatthesuperclassBirdisn’tstrictlyneeded.
InJavaScript,Idon’tneedatypehierarchyforpolymorphism;aslongasmyobjectsimplementtheappropriatelynamedmethods,everythingworksfine.Inthissituation,however,Iliketokeeptheunnecessarysuperclassasithelpsexplainthewaytheclassesarerelatedinthedomain.
Example:UsingPolymorphismforVariation
Withthebirdsexample,I’musingacleargeneralizationhierarchy.That’showsubclassingandpolymorphismisoftendiscussedintextbooks(includingmine)—butit’snottheonlywayinheritanceisusedinpractice;indeed,itprobablyisn’tthemostcommonorbestway.AnothercaseforinheritanceiswhenIwishtoindicatethatoneobjectismostlysimilartoanother,butwithsomevariations.
Asanexampleofthiscase,considersomecodeusedbyaratingagencytocomputeaninvestmentratingforthevoyagesofsailingships.Theratingagencygivesouteitheran“A”or“B”rating,dependingofvariousfactorsduetoriskandprofitpotential.Theriskcomesfromassessingthenatureofthevoyageaswellasthehistoryofthecaptain’spriorvoyages.
functionrating(voyage,history){
constvpf=voyageProfitFactor(voyage,history);
constvr=voyageRisk(voyage);
constchr=captainHistoryRisk(voyage,history);
if(vpf*3>(vr+chr*2))return"A";
elsereturn"B";
}
functionvoyageRisk(voyage){
letresult=1;
if(voyage.length>4)result+=2;
if(voyage.length>8)result+=voyage.length-8;
if(["china","east-indies"].includes(voyage.zone))result+=4;
returnMath.max(result,0);
}
functioncaptainHistoryRisk(voyage,history){
letresult=1;
if(history.length<5)result+=4;
result+=history.filter(v=>v.profit<0).length;
if(voyage.zone==="china"&&hasChina(history))result-=2;
returnMath.max(result,0);
}
functionhasChina(history){
returnhistory.some(v=>"china"===v.zone);
}
functionvoyageProfitFactor(voyage,history){
letresult=2;
if(voyage.zone==="china")result+=1;
if(voyage.zone==="east-indies")result+=1;
if(voyage.zone==="china"&&hasChina(history)){
result+=3;
if(history.length>10)result+=1;
if(voyage.length>12)result+=1;
if(voyage.length>18)result-=1;
}
else{
if(history.length>8)result+=1;
if(voyage.length>14)result-=1;
}
returnresult;
}
ThefunctionsvoyageRiskandcaptainHistoryRiskscorepointsforrisk,voyageProfitFactorscorespointsforthepotentialprofit,andratingcombinesthesetogivetheoverallratingforthevoyage.
Thecallingcodewouldlooksomethinglikethis:
constvoyage={zone:"west-indies",length:10};
consthistory=[
{zone:"east-indies",profit:5},
{zone:"west-indies",profit:15},
{zone:"china",profit:-2},
{zone:"west-africa",profit:7},
];
constmyRating=rating(voyage,history);
WhatIwanttofocusonhereishowacoupleofplacesuseconditionallogictohandlethecaseofavoyagetoChinawherethecaptainhasbeentoChinabefore.
functionrating(voyage,history){
constvpf=voyageProfitFactor(voyage,history);
constvr=voyageRisk(voyage);
constchr=captainHistoryRisk(voyage,history);
if(vpf*3>(vr+chr*2))return"A";
elsereturn"B";
}
functionvoyageRisk(voyage){
letresult=1;
if(voyage.length>4)result+=2;
if(voyage.length>8)result+=voyage.length-8;
if(["china","east-indies"].includes(voyage.zone))result+=4;
returnMath.max(result,0);
}
functioncaptainHistoryRisk(voyage,history){
letresult=1;
if(history.length<5)result+=4;
result+=history.filter(v=>v.profit<0).length;
if(voyage.zone==="china"&&hasChina(history))result-=2;
returnMath.max(result,0);
}
functionhasChina(history){
returnhistory.some(v=>"china"===v.zone);
}
functionvoyageProfitFactor(voyage,history){
letresult=2;
if(voyage.zone==="china")result+=1;
if(voyage.zone==="east-indies")result+=1;
if(voyage.zone==="china"&&hasChina(history)){
result+=3;
if(history.length>10)result+=1;
if(voyage.length>12)result+=1;
if(voyage.length>18)result-=1;
}
else{
if(history.length>8)result+=1;
if(voyage.length>14)result-=1;
}
returnresult;
}
Iwilluseinheritanceandpolymorphismtoseparateoutthelogicforhandlingthesecasesfromthebaselogic.ThisisaparticularlyusefulrefactoringifI’mabouttointroducemorespeciallogicforthiscase—andthelogicfortheserepeatChinavoyagescanmakeithardertounderstandthebasecase.
I’mbeginningwithasetoffunctions.Tointroducepolymorphism,Ineedtocreateaclassstructure,soIbeginbyapplyingCombineFunctionsintoClass(144).Thisresultsinthefollowingcode.
functionrating(voyage,history){
returnnewRating(voyage,history).value;
}
classRating{
constructor(voyage,history){
this.voyage=voyage;
this.history=history;
}
getvalue(){
constvpf=this.voyageProfitFactor;
constvr=this.voyageRisk;
constchr=this.captainHistoryRisk;
if(vpf*3>(vr+chr*2))return"A";
elsereturn"B";
}
getvoyageRisk(){
letresult=1;
if(this.voyage.length>4)result+=2;
if(this.voyage.length>8)result+=this.voyage.length-8;
if(["china","east-indies"].includes(this.voyage.zone))result+=4;
returnMath.max(result,0);
}
getcaptainHistoryRisk(){
letresult=1;
if(this.history.length<5)result+=4;
result+=this.history.filter(v=>v.profit<0).length;
if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;
returnMath.max(result,0);
}
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
if(this.voyage.zone==="china"&&this.hasChinaHistory){
result+=3;
if(this.history.length>10)result+=1;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
}
else{
if(this.history.length>8)result+=1;
if(this.voyage.length>14)result-=1;
}
returnresult;
}
gethasChinaHistory(){
returnthis.history.some(v=>"china"===v.zone);
}
}
That’sgivenmetheclassforthebasecase.Inowneedtocreateanemptysubclasstohousethevariantbehavior.
classExperiencedChinaRatingextendsRating{
}
Ithencreateafactoryfunctiontoreturnthevariantclasswhenneeded.
functioncreateRating(voyage,history){
if(voyage.zone==="china"&&history.some(v=>"china"===v.zone))
returnnewExperiencedChinaRating(voyage,history);
elsereturnnewRating(voyage,history);
}
Ineedtomodifyanycallerstousethefactoryfunctioninsteadofdirectlyinvokingtheconstructor,whichinthiscaseisjusttheratingfunction.
functionrating(voyage,history){
returncreateRating(voyage,history).value;
}
TherearetwobitsofbehaviorIneedtomoveintoasubclass.IbeginwiththelogicincaptainHistoryRisk:
classRating…
getcaptainHistoryRisk(){
letresult=1;
if(this.history.length<5)result+=4;
result+=this.history.filter(v=>v.profit<0).length;
if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;
returnMath.max(result,0);
}
Iwritetheoverridingmethodinthesubclass:
classExperiencedChinaRating
getcaptainHistoryRisk(){
constresult=super.captainHistoryRisk-2;
returnMath.max(result,0);
}
classRating…
getcaptainHistoryRisk(){
letresult=1;
if(this.history.length<5)result+=4;
result+=this.history.filter(v=>v.profit<0).length;
if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;
returnMath.max(result,0);
}
SeparatingthevariantbehaviorfromvoyageProfitFactorisabitmoremessy.Ican’tsimplyremovethevariantbehaviorandcallthesuperclassmethodsincethereisanalternativepathhere.Ialsodon’twanttocopythewholesuperclassmethoddowntothesubclass.
classRating…
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
if(this.voyage.zone==="china"&&this.hasChinaHistory){
result+=3;
if(this.history.length>10)result+=1;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
}
else{
if(this.history.length>8)result+=1;
if(this.voyage.length>14)result-=1;
}
returnresult;
}
SomyresponseistofirstuseExtractFunction(106)ontheentireconditionalblock.
classRating…
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
result+=this.voyageAndHistoryLengthFactor;
returnresult;
}
getvoyageAndHistoryLengthFactor(){
letresult=0;
if(this.voyage.zone==="china"&&this.hasChinaHistory){
result+=3;
if(this.history.length>10)result+=1;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
}
else{
if(this.history.length>8)result+=1;
if(this.voyage.length>14)result-=1;
}
returnresult;
}
Afunctionnamewithan“And”initisaprettybadsmell,butI’llletitsitandreekforamoment,whileIapplythesubclassing.
classRating…
getvoyageAndHistoryLengthFactor(){
letresult=0;
if(this.history.length>8)result+=1;
if(this.voyage.length>14)result-=1;
returnresult;
}
classExperiencedChinaRating…
getvoyageAndHistoryLengthFactor(){
letresult=0;
result+=3;
if(this.history.length>10)result+=1;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
That’s,formally,theendoftherefactoring—I’veseparatedthevariantbehavioroutintothesubclass.Thesuperclass’slogicissimplertounderstandandworkwith,andIonlyneedtodealwithvariantcasewhenI’mworkingonthesubclasscode,whichisexpressedintermsofitsdifferencewiththesuperclass.
ButIfeelIshouldatleastoutlinewhatI’ddowiththeawkwardnewmethod.Introducingamethodpurelyforoverridingbyasubclassisacommonthingtodowhendoingthiskindofbase-and-variationinheritance.Butacrudemethodlikethisobscureswhat’sgoingon,insteadofrevealing.
The“and”givesawaythattherearereallytwoseparatemodificationsgoingonhere—soIthinkit’swisetoseparatethem.I’lldothisbyusingExtractFunction
(106)onthehistorylengthmodification,bothinthesuperclassandsubclass.Istartwithjustthesuperclass:
classRating…
getvoyageAndHistoryLengthFactor(){
letresult=0;
result+=this.historyLengthFactor;
if(this.voyage.length>14)result-=1;
returnresult;
}
gethistoryLengthFactor(){
return(this.history.length>8)?1:0;
}
Idothesamewiththesubclass:
classExperiencedChinaRating…
getvoyageAndHistoryLengthFactor(){
letresult=0;
result+=3;
result+=this.historyLengthFactor;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
gethistoryLengthFactor(){
return(this.history.length>10)?1:0;
}
IcanthenuseMoveStatementstoCallers(215)onthesuperclasscase.
classRating…
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
result+=this.historyLengthFactor;
result+=this.voyageAndHistoryLengthFactor;
returnresult;
}
getvoyageAndHistoryLengthFactor(){
letresult=0;
result+=this.historyLengthFactor;
if(this.voyage.length>14)result-=1;
returnresult;
}
classExperiencedChinaRating…
getvoyageAndHistoryLengthFactor(){
letresult=0;
result+=3;
result+=this.historyLengthFactor;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
I’dthenuseChangeFunctionDeclaration(124).
classRating…
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
result+=this.historyLengthFactor;
result+=this.voyageLengthFactor;
returnresult;
}
getvoyageLengthFactor(){
return(this.voyage.length>14)?-1:0;
}
ChangingtoaternarytosimplifyvoyageLengthFactor.
classExperiencedChinaRating…
getvoyageLengthFactor(){
letresult=0;
result+=3;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
Onelastthing.Idon’tthinkadding3pointsmakessenseaspartofthevoyagelengthfactor—it’sbetteraddedtotheoverallresult.
classExperiencedChinaRating…
getvoyageProfitFactor(){
returnsuper.voyageProfitFactor+3;
}
getvoyageLengthFactor(){
letresult=0;
result+=3;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
Attheendoftherefactoring,Ihavethefollowingcode.First,thereisthebasicratingclasswhichcanignoreanycomplicationsoftheexperiencedChinacase:
classRating{
constructor(voyage,history){
this.voyage=voyage;
this.history=history;
}
getvalue(){
constvpf=this.voyageProfitFactor;
constvr=this.voyageRisk;
constchr=this.captainHistoryRisk;
if(vpf*3>(vr+chr*2))return"A";
elsereturn"B";
}
getvoyageRisk(){
letresult=1;
if(this.voyage.length>4)result+=2;
if(this.voyage.length>8)result+=this.voyage.length-8;
if(["china","east-indies"].includes(this.voyage.zone))result+=4;
returnMath.max(result,0);
}
getcaptainHistoryRisk(){
letresult=1;
if(this.history.length<5)result+=4;
result+=this.history.filter(v=>v.profit<0).length;
returnMath.max(result,0);
}
getvoyageProfitFactor(){
letresult=2;
if(this.voyage.zone==="china")result+=1;
if(this.voyage.zone==="east-indies")result+=1;
result+=this.historyLengthFactor;
result+=this.voyageLengthFactor;
returnresult;
}
getvoyageLengthFactor(){
return(this.voyage.length>14)?-1:0;
}
gethistoryLengthFactor(){
return(this.history.length>8)?1:0;
}
}
ThecodefortheexperiencedChinacasereadsasasetofvariationsonthebase:
classExperiencedChinaRatingextendsRating{
getcaptainHistoryRisk(){
constresult=super.captainHistoryRisk-2;
returnMath.max(result,0);
}
getvoyageLengthFactor(){
letresult=0;
if(this.voyage.length>12)result+=1;
if(this.voyage.length>18)result-=1;
returnresult;
}
gethistoryLengthFactor(){
return(this.history.length>10)?1:0;
}
getvoyageProfitFactor(){
returnsuper.voyageProfitFactor+3;
}
}
IntroduceSpecialCase
formerly:IntroduceNullObject
Motivation
Acommoncaseofduplicatedcodeiswhenmanyusersofadatastructurecheckaspecificvalue,andthenmostofthemdothesamething.IfIfindmanypartsofthecodebasehavingthesamereactiontoaparticularvalue,Iwanttobringthatreactionintoasingleplace.
AgoodmechanismforthisistheSpecialCasepatternwhereIcreateaspecial-caseelementthatcapturesallthecommonbehavior.Thisallowsmetoreplacemostofthespecial-casecheckswithsimplecalls.
Aspecialcasecanmanifestitselfinseveralways.IfallI’mdoingwiththeobjectisreadingdata,IcansupplyaliteralobjectwithallthevaluesIneedfilledin.IfIneedmorebehaviorthansimplevalues,Icancreateaspecialobjectwithmethodsforallthecommonbehavior.Thespecial-caseobjectcanbereturnedbyanencapsulatingclass,orinsertedintoadatastructurewithatransform.
Acommonvaluethatneedsspecial-caseprocessingisnull,whichiswhythispatternisoftencalledtheNullObjectpattern.Butit’sthesameapproachforanyspecialcase—IliketosaythatNullObjectisaspecialcaseofSpecialCase.
Mechanics
Beginwithacontainerdatastructure(orclass)thatcontainsapropertywhichisthesubjectoftherefactoring.Clientsofthecontainercomparethesubjectpropertyofthecontainertoaspecial-casevalue.Wewishtoreplacethespecial-casevalueofthesubjectwithaspecialcaseclassordatastructure.
Addaspecial-casecheckpropertytothesubject,returningfalse.
Createaspecial-caseobjectwithonlythespecial-casecheckproperty,returningtrue.
ApplyExtractFunction(106)tothespecial-casecomparisoncode.Ensurethatallclientsusethenewfunctioninsteadofdirectlycomparingit.
Introducethenewspecial-casesubjectintothecode,eitherbyreturningitfromafunctioncallorbyapplyingatransformfunction.
Changethebodyofthespecial-casecomparisonfunctionsothatitusesthespecial-casecheckproperty.
Test.
UseCombineFunctionsintoClass(144)orCombineFunctionsintoTransform(149)tomoveallofthecommonspecial-casebehaviorintothenewelement.
Sincethespecial-caseclassusuallyreturnsfixedvaluestosimplerequests,thesemaybehandledbymakingthespecialcasealiteralrecord.
UseInlineFunction(115)onthespecial-casecomparisonfunctionfortheplaceswhereit’sstillneeded.
Example
Autilitycompanyinstallsitsservicesinsites.
classSite…
getcustomer(){returnthis._customer;}
Therearevariouspropertiesofthecustomerclass;I’llconsiderthreeofthem.
classCustomer…
getname(){…}
getbillingPlan(){…}
setbillingPlan(arg){…}
getpaymentHistory(){…}
Mostofthetime,asitehasacustomer,butsometimesthereisn’tone.SomeonemayhavemovedoutandIdon’tyetknowwho,ifanyone,hasmovedin.Whenthishappens,thedatarecordfillsthecustomerfieldwiththestring“unknown”.Becausethiscanhappen,clientsofthesiteneedtobeabletohandleanunknowncustomer.Herearesomeexamplefragments.
client1…
constaCustomer=site.customer;
//…lotsofinterveningcode…
letcustomerName;
if(aCustomer==="unknown")customerName="occupant";
elsecustomerName=aCustomer.name;
client2…
constplan=(aCustomer==="unknown")?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
if(aCustomer!=="unknown")aCustomer.billingPlan=newPlan;
client4…
constweeksDelinquent=(aCustomer==="unknown")?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
Lookingthroughthecodebase,Iseemanyclientsofthesiteobjectthathavetodealwithanunknowncustomer.Mostofthemdothesamethingwhentheygetone:Theyuse“occupant”asthename,givethemabasicbillingplan,andclassthemaszero-weeksdelinquent.Thiswidespreadtestingforaspecialcase,plusacommonresponse,iswhattellsmeit’stimeforaSpecialCaseObject.
Ibeginbyaddingamethodtothecustomertoindicateitisunknown.
classCustomer…
getisUnknown(){returnfalse;}
IthenaddanUnknownCustomerclass.
classUnknownCustomer{
getisUnknown(){returntrue;}
}
NotethatIdon’tmakeUnknownCustomerasubclassofCustomer.Inotherlanguages,particularlythosestaticallytyped,Iwould,butJavaScript’srulesforsubclassing,aswellasitsdynamictyping,makeitbettertonotdothathere.
Nowcomesthetrickybit.Ihavetoreturnthisnewspecial-caseobjectwheneverIexpect"unknown"andchangeeachtestforanunknownvaluetousethenewisUnknownmethod.Ingeneral,IalwayswanttoarrangethingssoIcanmakeonesmallchangeatatime,thentest.ButifIchangethecustomerclasstoreturnanunknowncustomerinsteadof“unknown”,Ihavetomakeeveryclienttestingfor“unknown”tocallisUnknown—andIhavetodoitallatonce.Ifindthatasappealingaseatingliver(i.e.notatall).
ThereisacommontechniquetousewheneverIfindmyselfinthisbind.IuseExtractFunction(106)onthecodethatI’dhavetochangeinlotsofplaces—inthiscase,thespecial-casecomparisoncode.
functionisUnknown(arg){
if(!((arginstanceofCustomer)||(arg==="unknown")))
thrownewError(`investigatebadvalue:<${arg}>`);
return(arg==="unknown");
}
I’veputatrapinhereforanunexpectedvalue.ThiscanhelpmetospotanymistakesoroddbehaviorasI’mdoingthisrefactoring.
IcannowusethisfunctionwheneverI’mtestingforanunknowncustomer.Icanchangethesecallsoneatatime,testingaftereachchange.
client1…
letcustomerName;
if(isUnknown(aCustomer))customerName="occupant";
elsecustomerName=aCustomer.name;
Afterawhile,Ihavedonethemall.
client2…
constplan=(isUnknown(aCustomer))?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
if(!isUnknown(aCustomer))aCustomer.billingPlan=newPlan;
client4…
constweeksDelinquent=isUnknown(aCustomer)?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
OnceI’vechangedallthecallerstouseisUnknown,Icanchangethesiteclasstoreturnanunknowncustomer.
classSite…
getcustomer(){
return(this._customer==="unknown")?newUnknownCustomer():this._customer;
}
IcancheckthatI’mnolongerusingthe“unknown”stringbychangingisUnknowntousetheunknownvalue.
client1…
functionisUnknown(arg){
if(!(arginstanceofCustomer||arginstanceofUnknownCustomer))
thrownewError(`investigatebadvalue:<${arg}>`);
returnarg.isUnknown;
}
Itesttoensurethat’sallworking.
Nowthefunbegins.IcanuseCombineFunctionsintoClass(144)totakeeach
client’sspecial-casecheckandseeifIcanreplaceitwithacommonlyexpectedvalue.Atthemoment,Ihavevariousclientsusing“occupant”forthenameofanunknowncustomer,likethis:
client1…
letcustomerName;
if(isUnknown(aCustomer))customerName="occupant";
elsecustomerName=aCustomer.name;
Iaddasuitablemethodtotheunknowncustomer:
classUnknownCustomer…
getname(){return"occupant";}
NowIcanmakeallthatconditionalcodegoaway.
client1…
constcustomerName=aCustomer.name;
OnceI’vetestedthatthisworks,I’llprobablybeabletouseInlineVariable(123)onthatvariabletoo.
Nextisthebillingplanproperty.
client2…
constplan=(isUnknown(aCustomer))?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
if(!isUnknown(aCustomer))aCustomer.billingPlan=newPlan;
Forreadbehavior,IdothesamethingIdidwiththename—takethecommonresponseandreplywithit.Withthewritebehavior,thecurrentcodedoesn’tcallthesetterforanunknowncustomer—soforthespecialcase,Iletthesetterbecalled,butitdoesnothing.
classUnknownCustomer…
getbillingPlan(){returnregistry.billingPlans.basic;}
setbillingPlan(arg){/*ignore*/}
clientreader…
constplan=aCustomer.billingPlan;
clientwriter…
aCustomer.billingPlan=newPlan;
Special-caseobjectsarevalueobjects,andthusshouldalwaysbeimmutable,eveniftheobjectstheyaresubstitutingforarenot.
Thelastcaseisabitmoreinvolvedbecausethespecialcaseneedstoreturnanotherobjectthathasitsownproperties.
client…
constweeksDelinquent=isUnknown(aCustomer)?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
Thegeneralrulewithaspecial-caseobjectisthatifitneedstoreturnrelatedobjects,theyareusuallyspecialcasesthemselves.SohereIneedtocreateanullpaymenthistory.
classUnknownCustomer…
getpaymentHistory(){returnnewNullPaymentHistory();}
classNullPaymentHistory…
getweeksDelinquentInLastYear(){return0;}
client…
constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;
Icarryon,lookingatalltheclientstoseeifIcanreplacethemwiththepolymorphicbehavior.Buttherewillbeexceptions—clientsthatwanttodosomethingdifferentwiththespecialcase.Imayhave23clientsthatuse“occupant”forthenameofanunknowncustomer,butthere’salwaysonethat
needssomethingdifferent.
client…
constname=!isUnknown(aCustomer)?aCustomer.name:"unknownoccupant";
Inthatcase,Ineedtoretainaspecial-casecheck.Iwillchangeittousethemethodoncustomer,essentiallyusingInlineFunction(115)onisUnknown
client…
constname=aCustomer.isUnknown?"unknownoccupant":aCustomer.name;
WhenI’mdonewithalltheclients,IshouldbeabletouseRemoveDeadCode(236)ontheglobalisPresentfunction,asnobodyshouldbecallingitanymore.
Example:UsinganObjectLiteral
Creatingaclasslikethisisafairbitofworkforwhatisreallyasimplevalue.ButfortheexampleIgave,Ihadtomaketheclasssincethecustomercouldbeupdated.If,however,Ionlyreadthedatastructure,Icanusealiteralobjectinstead.
Hereistheopeningcaseagain—justthesame,exceptthistimethereisnoclientthatupdatesthecustomer.
classSite…
getcustomer(){returnthis._customer;}
classCustomer…
getname(){…}
getbillingPlan(){…}
setbillingPlan(arg){…}
getpaymentHistory(){…}
client1…
constaCustomer=site.customer;
//…lotsofinterveningcode…
letcustomerName;
if(aCustomer==="unknown")customerName="occupant";
elsecustomerName=aCustomer.name;
client2…
constplan=(aCustomer==="unknown")?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
constweeksDelinquent=(aCustomer==="unknown")?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
Aswiththepreviouscase,IstartbyaddinganisUnknownpropertytothecustomerandcreatingaspecial-caseobjectwiththatfield.Thedifferenceisthatthistime,thespecialcaseisaliteral.
classCustomer…
getisUnknown(){returnfalse;}
toplevel…
functioncreateUnknownCustomer(){
return{
isUnknown:true,
};
}
IapplyExtractFunction(106)tothespecialcaseconditiontest.
functionisUnknown(arg){
return(arg==="unknown");
}
client1…
letcustomerName;
if(isUnknown(aCustomer))customerName="occupant";
elsecustomerName=aCustomer.name;
client2…
constplan=isUnknown(aCustomer)?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
constweeksDelinquent=isUnknown(aCustomer)?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
Ichangethesiteclassandtheconditiontesttoworkwiththespecialcase.
classSite…
getcustomer(){
return(this._customer==="unknown")?createUnknownCustomer():this._customer;
}
toplevel…
functionisUnknown(arg){
returnarg.isUnknown;
}
ThenIreplaceeachstandardresponsewiththeappropriateliteralvalue.Istartwiththename:
functioncreateUnknownCustomer(){
return{
isUnknown:true,
name:"occupant",
};
}
client1…
constcustomerName=aCustomer.name;
Then,thebillingplan:
functioncreateUnknownCustomer(){
return{
isUnknown:true,
name:"occupant",
billingPlan:registry.billingPlans.basic,
};
}
client2…
constplan=aCustomer.billingPlan;
Similarly,Icancreateanestednullpaymenthistorywiththeliteral:
functioncreateUnknownCustomer(){
return{
isUnknown:true,
name:"occupant",
billingPlan:registry.billingPlans.basic,
paymentHistory:{
weeksDelinquentInLastYear:0,
},
};
}
client3…
constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;
IfIusealiterallikethis,Ishouldmakeitimmutable,whichImightdowithfreeze.Usually,I’dratheruseaclass.
Example:UsingaTransform
Bothpreviouscasesinvolveaclass,butthesameideacanbeappliedtoarecordbyusingatransformstep.
Let’sassumeourinputisasimplerecordstructurethatlookssomethinglikethis:
{
name:"AcmeBoston",
location:"MaldenMA",
//moresitedetails
customer:{
name:"AcmeIndustries",
billingPlan:"plan-451",
paymentHistory:{
weeksDelinquentInLastYear:7
//more
},
//more
}
}
Insomecases,thecustomerisn’tknown,andsuchcasesaremarkedinthesameway:
{
name:"WarehouseUnit15",
location:"MaldenMA",
//moresitedetails
customer:"unknown",
}
Ihavesimilarclientcodethatchecksfortheunknowncustomer:
client1…
constsite=acquireSiteData();
constaCustomer=site.customer;
//…lotsofinterveningcode…
letcustomerName;
if(aCustomer==="unknown")customerName="occupant";
elsecustomerName=aCustomer.name;
client2…
constplan=(aCustomer==="unknown")?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
constweeksDelinquent=(aCustomer==="unknown")?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
Myfirststepistorunthesitedatastructurethroughatransformthat,currently,doesnothingbutadeepcopy.
client1…
constrawSite=acquireSiteData();
constsite=enrichSite(rawSite);
constaCustomer=site.customer;
//…lotsofinterveningcode…
letcustomerName;
if(aCustomer==="unknown")customerName="occupant";
elsecustomerName=aCustomer.name;
functionenrichSite(inputSite){
return_.cloneDeep(inputSite);
}
IapplyExtractFunction(106)tothetestforanunknowncustomer.
functionisUnknown(aCustomer){
returnaCustomer==="unknown";
}
client1…
constrawSite=acquireSiteData();
constsite=enrichSite(rawSite);
constaCustomer=site.customer;
//…lotsofinterveningcode…
letcustomerName;
if(isUnknown(aCustomer))customerName="occupant";
elsecustomerName=aCustomer.name;
client2…
constplan=(isUnknown(aCustomer))?
registry.billingPlans.basic
:aCustomer.billingPlan;
client3…
constweeksDelinquent=(isUnknown(aCustomer))?
0
:aCustomer.paymentHistory.weeksDelinquentInLastYear;
IbegintheenrichmentbyaddinganisUnknownpropertytothecustomer.
functionenrichSite(aSite){
constresult=_.cloneDeep(aSite);
constunknownCustomer={
isUnknown:true,
};
if(isUnknown(result.customer))result.customer=unknownCustomer;
elseresult.customer.isUnknown=false;
returnresult;
}
Icanthenmodifythespecial-caseconditiontesttoincludeprobingforthisnewproperty.Ikeeptheoriginaltestaswell,sothatthetestwillworkonbothrawandenrichedsites.
functionisUnknown(aCustomer){
if(aCustomer==="unknown")returntrue;
elsereturnaCustomer.isUnknown;
}
Itesttoensurethat’sallOK,thenstartapplyingCombineFunctionsintoTransform(149)onthespecialcase.First,Imovethechoiceofnameintotheenrichmentfunction.
functionenrichSite(aSite){
constresult=_.cloneDeep(aSite);
constunknownCustomer={
isUnknown:true,
name:"occupant",
};
if(isUnknown(result.customer))result.customer=unknownCustomer;
elseresult.customer.isUnknown=false;
returnresult;
}
client1…
constrawSite=acquireSiteData();
constsite=enrichSite(rawSite);
constaCustomer=site.customer;
//…lotsofinterveningcode…
constcustomerName=aCustomer.name;
Itest,thendothebillingplan.
functionenrichSite(aSite){
constresult=_.cloneDeep(aSite);
constunknownCustomer={
isUnknown:true,
name:"occupant",
billingPlan:registry.billingPlans.basic,
};
if(isUnknown(result.customer))result.customer=unknownCustomer;
elseresult.customer.isUnknown=false;
returnresult;
}
client2…
constplan=aCustomer.billingPlan;
Itestagain,thendothelastclient.
functionenrichSite(aSite){
constresult=_.cloneDeep(aSite);
constunknownCustomer={
isUnknown:true,
name:"occupant",
billingPlan:registry.billingPlans.basic,
paymentHistory:{
weeksDelinquentInLastYear:0,
}
};
if(isUnknown(result.customer))result.customer=unknownCustomer;
elseresult.customer.isUnknown=false;
returnresult;
}
client3…
constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;
IntroduceAssertion
Motivation
Often,sectionsofcodeworkonlyifcertainconditionsaretrue.Thismaybeassimpleasasquarerootcalculationonlyworkingonapositiveinputvalue.Withanobject,itmayrequirethatatleastoneofagroupoffieldshasavalueinit.
Suchassumptionsareoftennotstatedbutcanonlybededucedbylookingthroughanalgorithm.Sometimes,theassumptionsarestatedwithacomment.Abettertechniqueistomaketheassumptionexplicitbywritinganassertion.
Anassertionisaconditionalstatementthatisassumedtobealwaystrue.Failureofanassertionindicatesaprogrammererror.Assertionfailuresshouldneverbecheckedbyotherpartsofthesystem.Assertionsshouldbewrittensothattheprogramfunctionsequallycorrectlyiftheyareallremoved;indeed,somelanguagesprovideassertionsthatcanbedisabledbyacompile-timeswitch.
Ioftenseepeopleencourageusingassertionsinordertofinderrors.WhilethisiscertainlyaGoodThing,it’snottheonlyreasontousethem.Ifindassertionstobeavaluableformofcommunication—theytellthereadersomethingabouttheassumedstateoftheprogramatthispointofexecution.Ialsofindthemhandyfordebugging,andtheircommunicationvaluemeansI’minclinedtoleavetheminonceI’vefixedtheerrorI’mchasing.Self-testingcodereducestheirvaluefordebugging,assteadilynarrowingunittestsoftendothejobbetter,butIstilllikeassertionsforcommunication.
Mechanics
Whenyouseethataconditionisassumedtobetrue,addanassertiontostateit.
Sinceassertionsshouldnotaffecttherunningofasystem,addingoneisalwaysbehavior-preserving.
Example
Here’sasimpletaleofdiscounts.Acustomercanbegivenadiscountratetoapplytoalltheirpurchases:
classCustomer…
applyDiscount(aNumber){
return(this.discountRate)
?aNumber-(this.discountRate*aNumber)
:aNumber;
}
There’sanassumptionherethatthediscountrateisapositivenumber.Icanmakethatassumptionexplicitbyusinganassertion.ButIcan’teasilyplaceanassertionintoaternaryexpression,sofirstI’llreformulateitasanif-thenstatement.
classCustomer…
applyDiscount(aNumber){
if(!this.discountRate)returnaNumber;
elsereturnaNumber-(this.discountRate*aNumber);
}
NowIcaneasilyaddtheassertion.
classCustomer…
applyDiscount(aNumber){
if(!this.discountRate)returnaNumber;
else{
assert(this.discountRate>=0);
returnaNumber-(this.discountRate*aNumber);
}
}
Inthiscase,I’dratherputthisassertionintothesettingmethod.IftheassertionfailsinapplyDiscount,myfirstpuzzleishowitgotintothefieldinthefirstplace.
classCustomer…
setdiscountRate(aNumber){
assert(null===aNumber||aNumber>=0);
this._discountRate=aNumber;
}
Anassertionlikethiscanbeparticularlyvaluableifit’shardtospottheerrorsource—whichmaybeanerrantminussigninsomeinputdataorsomeinversionelsewhereinthecode.
Thereisarealdangerofoverusingassertions.Idon’tuseassertionstocheckeverythingthatIthinkistrue,butonlytocheckthingsthatneedtobetrue.Duplicationisaparticularproblem,asit’scommontotweakthesekindsofconditions.SoIfindit’sessentialtoremoveanyduplicationintheseconditions,usuallybyaliberaluseofExtractFunction(106).
Ionlyuseassertionsforthingsthatareprogrammererrors.IfI’mreadingdatafromanexternalsource,anyvaluecheckingshouldbeafirst-classpartoftheprogram,notanassertion—unlessI’mreallyconfidentintheexternalsource.Assertionsarealastresorttohelptrackbugs—though,ironically,IonlyusethemwhenIthinktheyshouldneverfail.
Chapter11RefactoringAPIsModulesandtheirfunctionsarethebuildingblocksofoursoftware.APIsarethejointsthatweusetoplugthemtogether.MakingtheseAPIseasytounderstandanduseisimportantbutalsodifficult:IneedtorefactorthemasIlearnhowtoimprovethem.
AgoodAPIclearlyseparatesanyfunctionsthatupdatedatafromthosethatonlyreaddata.IfIseethemcombined,IuseSeparateQueryfromModifier(304)toteasethemapart.IcanunifyfunctionsthatonlyvaryduetoavaluewithParameterizeFunction(308).Someparameters,however,arereallyjustasignalofanentirelydifferentbehaviorandarebestexcisedwithRemoveFlagArgument(312).
Datastructuresareoftenunpackedunnecessarilywhenpassedbetweenfunctions;IprefertokeepthemtogetherwithPreserveWholeObject(317).Decisionsonwhatshouldbepassedasaparameter,andwhatcanberesolvedbythecalledfunction,areonesIoftenneedtorevisitwithReplaceParameterwithQuery(322)andReplaceQuerywithParameter(325).
Aclassisacommonformofmodule.Iprefermyobjectstobeasimmutableaspossible,soIuseRemoveSettingMethod(329)wheneverIcan.Often,whenacallerasksforanewobject,Ineedmoreflexibilitythanasimpleconstructorgives,whichIcangetbyusingReplaceConstructorwithFactoryFunction(332).
Thelasttworefactoringsaddressthedifficultyofbreakingdownaparticularlycomplexfunctionthatpassesalotofdataaround.IcanturnthatfunctionintoanobjectwithReplaceFunctionwithCommand(335),whichmakesiteasiertouseExtractFunction(106)onthefunction’sbody.IfIlatersimplifythefunctionandnolongerneeditasacommandobject,IturnitbackintoafunctionwithReplaceCommandwithFunction(342).
SeparateQueryfromModifier
Motivation
WhenIhaveafunctionthatgivesmeavalueandhasnoobservablesideeffects,Ihaveaveryvaluablething.IcancallthisfunctionasoftenasIlike.Icanmovethecalltootherplacesinacallingfunction.It’seasiertotest.Inshort,Ihavealotlesstoworryabout.
Itisagoodideatoclearlysignalthedifferencebetweenfunctionswithsideeffectsandthosewithout.Agoodruletofollowisthatanyfunctionthatreturnsavalueshouldnothaveobservablesideeffects—thecommand-queryseparation[bib-cqs].Someprogrammerstreatthisasanabsoluterule.I’mnot100percentpureonthis(asonanything),butItrytofollowitmostofthetime,andithasservedmewell.
IfIcomeacrossamethodthatreturnsavaluebutalsohassideeffects,Ialwaystrytoseparatethequeryfromthemodifier.
NotethatIusethephraseobservablesideeffects.Acommonoptimizationisto
cachethevalueofaqueryinafieldsothatrepeatedcallsgoquicker.Althoughthischangesthestateoftheobjectwiththecache,thechangeisnotobservable.Anysequenceofquerieswillalwaysreturnthesameresultsforeachquery.
Mechanics
Copythefunction,nameitasaquery.
Lookintothefunctiontoseewhatisreturned.Ifthequeryisusedtopopulateavariable,thevariable’snameshouldprovideagoodclue.
Removeanysideeffectsfromthenewqueryfunction.
Runstaticchecks.
Findeachcalloftheoriginalmethod.Ifthatcallusesthereturnvalue,replacetheoriginalcallwithacalltothequeryandinsertacalltotheoriginalmethodbelowit.Testaftereachchange.
Removereturnvaluesfromoriginal.
Test.
Oftenafterdoingthistherewillbeduplicationbetweenthequeryandtheoriginalmethodthatcanbetidiedup.
Example
Hereisafunctionthatscansalistofnamesforamiscreant.Ifitfindsone,itreturnsthenameofthebadguyandsetsoffthealarms.Itonlydoesthisforthefirstmiscreantitfinds(Iguessoneisenough).
functionalertForMiscreant(people){
for(constpofpeople){
if(p==="Don"){
setOffAlarms();
return"Don";
}
if(p==="John"){
setOffAlarms();
return"John";
}
}
return"";
}
Ibeginbycopyingthefunction,namingitafterthequeryaspectofthefunction.
functionfindMiscreant(people){
for(constpofpeople){
if(p==="Don"){
setOffAlarms();
return"Don";
}
if(p==="John"){
setOffAlarms();
return"John";
}
}
return"";
}
Iremovethesideeffectsfromthisnewquery.
functionfindMiscreant(people){
for(constpofpeople){
if(p==="Don"){
setOffAlarms();
return"Don";
}
if(p==="John"){
setOffAlarms();
return"John";
}
}
return"";
}
Inowgotoeachcallerandreplaceitwithacalltothequery,followedbyacalltothemodifier.So
constfound=alertForMiscreant(people);
changesto
constfound=findMiscreant(people);
alertForMiscreant(people);
Inowremovethereturnvaluesfromthemodifier.
functionalertForMiscreant(people){
for(constpofpeople){
if(p==="Don"){
setOffAlarms();
return;
}
if(p==="John"){
setOffAlarms();
return;
}
}
return;
}
NowIhavealotofduplicationbetweentheoriginalmodifierandthenewquery,soIcanuseSubstituteAlgorithm(193)sothatthemodifierusesthequery.
functionalertForMiscreant(people){
if(findMiscreant(people)!=="")setOffAlarms();
}
ParameterizeFunction
formerly:ParameterizeMethod
Motivation
IfIseetwofunctionsthatcarryoutverysimilarlogicwithdifferentliteralvalues,Icanremovetheduplicationbyusingasinglefunctionwithparametersforthedifferentvalues.Thisincreasestheusefulnessofthefunction,sinceIcanapplyitelsewherewithdifferentvalues.
Mechanics
Selectoneofthesimilarmethods.
UseChangeFunctionDeclaration(124)toaddanyliteralsthatneedtoturnintoparameters.
Foreachcallerofthefunction,addtheliteralvalue.
Test.
Changethebodyofthefunctiontousethenewparameters.Testaftereachchange.
Foreachsimilarfunction,replacethecallwithacalltotheparameterizedfunction.Testaftereachone.
Iftheoriginalparameterizedfunctiondoesn’tworkforasimilarfunction,adjustitforthenewfunctionbeforemovingontothenext.
Example
Anobviousexampleissomethinglikethis:
functiontenPercentRaise(aPerson){
aPerson.salary=aPerson.salary.multiply(1.1);
}
functionfivePercentRaise(aPerson){
aPerson.salary=aPerson.salary.multiply(1.05);
}
Hopefullyit’sobviousthatIcanreplacethesewith
functionraise(aPerson,factor){
aPerson.salary=aPerson.salary.multiply(1+factor);
}
Butitcanbeabitmoreinvolvedthanthat.Considerthiscode:
functionbaseCharge(usage){
if(usage<0)returnusd(0);
constamount=
bottomBand(usage)*0.03
+middleBand(usage)*0.05
+topBand(usage)*0.07;
returnusd(amount);
}
functionbottomBand(usage){
returnMath.min(usage,100);
}
functionmiddleBand(usage){
returnusage>100?Math.min(usage,200)-100:0;
}
functiontopBand(usage){
returnusage>200?usage-200:0;
}
Herethelogicisclearlyprettysimilar—butisitsimilarenoughtosupportcreatingaparameterizedmethodforthebands?Itis,butmaybeatouchlessobviousthanthetrivialcaseabove.
Whenlookingtoparameterizesomerelatedfunctions,myapproachistotakeoneofthefunctionsandaddparameterstoit,withaneyetotheothercases.Withrange-orientedthingslikethis,usuallytheplacetostartiswiththemiddlerange.SoI’llworkonmiddleBandtochangeittouseparameters,andthenadjustothercallerstofit.
middleBandusestwoliteralvalues:100and200.Theserepresentthebottomandtopofthismiddleband.IbeginbyusingChangeFunctionDeclaration(124)toaddthemtothecall.WhileI’matit,I’llalsochangethenameofthefunctiontosomethingthatmakessensewiththeparameterization.
functionwithinBand(usage,bottom,top){
returnusage>100?Math.min(usage,200)-100:0;
}
functionbaseCharge(usage){
if(usage<0)returnusd(0);
constamount=
bottomBand(usage)*0.03
+withinBand(usage,100,200)*0.05
+topBand(usage)*0.07;
returnusd(amount);
}
Ireplaceeachliteralwithareferencetotheparameter.
functionwithinBand(usage,bottom,top){
returnusage>bottom?Math.min(usage,200)-bottom:0;
}
then
functionwithinBand(usage,bottom,top){
returnusage>bottom?Math.min(usage,top)-bottom:0;
}
Ireplacethecalltothebottombandwithacalltothenewlyparameterizedfunction.
functionbaseCharge(usage){
if(usage<0)returnusd(0);
constamount=
withinBand(usage,0,100)*0.03
+withinBand(usage,100,200)*0.05
+topBand(usage)*0.07;
returnusd(amount);
}
functionbottomBand(usage){
returnMath.min(usage,100);
}
Toreplacethecalltothetopband,Ineedtomakeuseofinfinity.
functionbaseCharge(usage){
if(usage<0)returnusd(0);
constamount=
withinBand(usage,0,100)*0.03
+withinBand(usage,100,200)*0.05
+withinBand(usage,200,Infinity)*0.07;
returnusd(amount);
}
functiontopBand(usage){
returnusage>200?usage-200:0;
}
Withthelogicworkingthewayitdoesnow,Icouldremovetheinitialguardclause.Butalthoughit’slogicallyunnecessarynow,Iliketokeepitasitdocumentshowtohandlethatcase.
RemoveFlagArgument
formerly:ReplaceParameterwithExplicitMethods
Motivation
Aflagargumentisafunctionargumentthatthecallerusestoindicatewhichlogicthecalledfunctionshouldexecute.Imaycallafunctionthatlookslikethis:
functionbookConcert(aCustomer,isPremium){
if(isPremium){
//logicforpremiumbooking
}else{
//logicforregularbooking
}
}
Tobookapremiumconcert,Iissuethecalllikeso:
bookConcert(aCustomer,true);
Flagargumentscanalsocomeasenums:
bookConcert(aCustomer,CustomerType.PREMIUM);
orstrings(orsymbolsinlanguagesthatusethem):
bookConcert(aCustomer,"premium");
Idislikeflagargumentsbecausetheycomplicatetheprocessofunderstandingwhatfunctioncallsareavailableandhowtocallthem.MyfirstrouteintoanAPIisusuallythelistofavailablefunctions,andflagargumentshidethedifferencesinthefunctioncallsthatareavailable.OnceIselectafunction,Ihavetofigureoutwhatvaluesareavailablefortheflagarguments.Booleanflagsareevenworsesincetheydon’tconveytheirmeaningtothereader—inafunctioncall,Ican’tfigureoutwhattruemeans.It’sclearertoprovideanexplicitfunctionforthetaskIwanttodo.
premiumBookConcert(aCustomer);
Notallargumentslikethisareflagarguments.Tobeaflagargument,thecallersmustbesettingthebooleanvaluetoaliteralvalue,notdatathat’sflowingthroughtheprogram.Also,theimplementationfunctionmustbeusingtheargumenttoinfluenceitscontrolflow,notasdatathatitpassestofurtherfunctions.
Removingflagargumentsdoesn’tjustmakethecodeclearer—italsohelpsmytooling.Codeanalysistoolscannowmoreeasilyseethedifferencebetweencallingthepremiumlogicandcallingregularlogic.
Flagargumentscanhaveaplaceifthere’smorethanoneoftheminthefunction,sinceotherwiseIwouldneedexplicitfunctionsforeverycombinationoftheirvalues.Butthat’salsoasignalofafunctiondoingtoomuch,andIshouldlookforawaytocreatesimplerfunctionsthatIcancomposeforthislogic.
Mechanics
Createanexplicitfunctionforeachvalueoftheparameter.
Ifthemainfunctionhasacleardispatchconditional,useDecomposeConditional(260)tocreatetheexplicitfunctions.Otherwise,createwrappingfunctions.
Foreachcallerthatusesaliteralvaluefortheparameter,replaceitwithacalltotheexplicitfunction.
Example
Lookingthroughsomecode,Iseecallstocalculateadeliverydateforashipment.Someofthecallslooklike
aShipment.deliveryDate=deliveryDate(anOrder,true);
andsomelooklike
aShipment.deliveryDate=deliveryDate(anOrder,false);
Facedwithcodelikethis,Iimmediatelybegintowonderaboutthemeaningofthebooleanvalue.Whatisitdoing?
ThebodyofdeliveryDatelookslikethis:
functiondeliveryDate(anOrder,isRush){
if(isRush){
letdeliveryTime;
if(["MA","CT"].includes(anOrder.deliveryState))deliveryTime=1;
elseif(["NY","NH"].includes(anOrder.deliveryState))deliveryTime=2;
elsedeliveryTime=3;
returnanOrder.placedOn.plusDays(1+deliveryTime);
}
else{
letdeliveryTime;
if(["MA","CT","NY"].includes(anOrder.deliveryState))deliveryTime=2;
elseif(["ME","NH"].includes(anOrder.deliveryState))deliveryTime=3;
elsedeliveryTime=4;
returnanOrder.placedOn.plusDays(2+deliveryTime);
}
}
Here,thecallerisusingaliteralbooleanvaluetodeterminewhichcodeshouldrun—aclassicflagargument.Butthewholepointofusingafunctionistofollowthecaller’sinstructions,soitisbettertoclarifythecaller’sintentwithexplicitfunctions.
Inthiscase,IcandothisbyusingDecomposeConditional(260),whichgivesmethis:
functiondeliveryDate(anOrder,isRush){
if(isRush)returnrushDeliveryDate(anOrder);
elsereturnregularDeliveryDate(anOrder);
}
functionrushDeliveryDate(anOrder){
letdeliveryTime;
if(["MA","CT"].includes(anOrder.deliveryState))deliveryTime=1;
elseif(["NY","NH"].includes(anOrder.deliveryState))deliveryTime=2;
elsedeliveryTime=3;
returnanOrder.placedOn.plusDays(1+deliveryTime);
}
functionregularDeliveryDate(anOrder){
letdeliveryTime;
if(["MA","CT","NY"].includes(anOrder.deliveryState))deliveryTime=2;
elseif(["ME","NH"].includes(anOrder.deliveryState))deliveryTime=3;
elsedeliveryTime=4;
returnanOrder.placedOn.plusDays(2+deliveryTime);
}
Thetwonewfunctionscapturetheintentofthecallbetter,soIcanreplaceeachcallof
aShipment.deliveryDate=deliveryDate(anOrder,true);
with
aShipment.deliveryDate=rushDeliveryDate(anOrder);
andsimilarlywiththeothercase.
WhenI’vereplacedallthecallers,IremovedeliveryDate.
Aflagargumentisn’tjustthepresenceofabooleanvalue;it’sthatthebooleanissetwithaliteralratherthandata.IfallthecallersofdeliveryDatewerelikethis
constisRush=determineIfRush(anOrder);
aShipment.deliveryDate=deliveryDate(anOrder,isRush);
thenI’dhavenoproblemwithdeliveryDate’ssignature(althoughI’dstillwanttoapplyDecomposeConditional(260)).
Itmaybethatsomecallersusetheargumentasaflagargumentbysettingitwithaliteral,whileotherssettheargumentwithdata.Inthiscase,I’dstilluseRemoveFlagArgument,butnotchangethedatacallersandnotremovedeliveryDateattheend.ThatwayIsupportbothinterfacesforthedifferentuses.
Decomposingtheconditionallikethisisagoodwaytocarryoutthisrefactoring,butitonlyworksifthedispatchontheparameteristheouterpartofthefunction(orIcaneasilyrefactorittomakeitso).It’salsopossiblethattheparameterisusedinamuchmoretangledway,suchasthisalternativeversionofdeliveryDate:
functiondeliveryDate(anOrder,isRush){
letresult;
letdeliveryTime;
if(anOrder.deliveryState==="MA"||anOrder.deliveryState==="CT")
deliveryTime=isRush?1:2;
elseif(anOrder.deliveryState==="NY"||anOrder.deliveryState==="NH"){
deliveryTime=2;
if(anOrder.deliveryState==="NH"&&!isRush)
deliveryTime=3;
}
elseif(isRush)
deliveryTime=3;
elseif(anOrder.deliveryState==="ME")
deliveryTime=3;
else
deliveryTime=4;
result=anOrder.placedOn.plusDays(2+deliveryTime);
if(isRush)result=result.minusDays(1);
returnresult;
}
Inthiscase,teasingoutisRushintoatop-leveldispatchconditionalislikelymoreworkthanIfancy.Soinstead,IcanlayerfunctionsoverthedeliveryDate:
functionrushDeliveryDate(anOrder){returndeliveryDate(anOrder,true);}
functionregularDeliveryDate(anOrder){returndeliveryDate(anOrder,false);}
ThesewrappingfunctionsareessentiallypartialapplicationsofdeliveryDate,althoughtheyaredefinedinprogramtextratherthanbycompositionoffunctions.
IcanthendothesamereplacementofcallersthatIdidwiththedecomposedconditionalearlieron.Iftherearen’tanycallersusingtheparameterasdata,Iliketorestrictitsvisibilityorrenameittoanamethatconveysthatitshouldn’tbeuseddirectly(e.g.,deliveryDateHelperOnly)
PreserveWholeObject
Motivation
IfIseecodethatderivesacoupleofvaluesfromarecordandthenpassesthesevaluesintoafunction,Iliketoreplacethosevalueswiththewholerecorditself,lettingthefunctionbodyderivethevaluesitneeds.
Passingthewholerecordhandleschangebettershouldthecalledfunctionneedmoredatafromthewholeinthefuture—thatchangewouldnotrequiremetoaltertheparameterlist.Italsoreducesthesizeoftheparameterlist,whichusuallymakesthefunctioncalleasiertounderstand.Ifmanyfunctionsarecalledwiththeparts,theyoftenduplicatethelogicthatmanipulatestheseparts—logicthatcanoftenbemovedtothewhole.
ThemainreasonIwouldn’tdothisisifIdon’twantthecalledfunctiontohaveadependencyonthewhole—whichtypicallyoccurswhentheyareindifferentmodules.
Pullingseveralvaluesfromanobjecttodosomelogiconthemaloneisasmell(FeatureEnvy,p.75),andusuallyasignalthatthislogicshouldbemovedintothewholeitself.PreserveWholeObjectisparticularlycommonafterI’vedoneIntroduceParameterObject(140),asIhuntdownanyoccurrencesoftheoriginaldataclumptoreplacethemwiththenewobject.
Ifseveralbitsofcodeonlyusethesamesubsetofanobject’sfeatures,thenthatmayindicateagoodopportunityforExtractClass(180).
Onecasethatmanypeoplemissiswhenanobjectcallsanotherobjectwithseveralofitsowndatavalues.IfIseethis,Icanreplacethosevalueswithaself-reference(thisinJavaScript).
Mechanics
Createanemptyfunctionwiththedesiredparameters.
Givethefunctionaneasilysearchablenamesoitcanbereplacedattheend.
Fillthebodyofthenewfunctionwithacalltotheoldfunction,mappingfromthenewparameterstotheoldones.
Runstaticchecks.
Adjusteachcallertousethenewfunction,testingaftereachchange.
Thismaymeanthatsomecodethatderivestheparameterisn’tneeded,socanfalltoRemoveDeadCode(236).
Oncealloriginalcallershavebeenchanged,useInlineFunction(115)ontheoriginalfunction.
Changethenameofthenewfunctionandallitscallers.
Example
Consideraroommonitoringsystem.Itcomparesitsdailytemperaturerangewitharangeinapredefinedheatingplan.
caller…
constlow=aRoom.daysTempRange.low;
consthigh=aRoom.daysTempRange.high;
if(!aPlan.withinRange(low,high))
alerts.push("roomtemperaturewentoutsiderange");
classHeatingPlan…
withinRange(bottom,top){
return(bottom>=this._temperatureRange.low)&&(top<=this._temperatureRange.high);
}
InsteadofunpackingtherangeinformationwhenIpassitin,Icanpassinthewholerangeobject.
IbeginbystatingtheinterfaceIwantasanemptyfunction.
classHeatingPlan…
xxNEWwithinRange(aNumberRange){
}
SinceIintendittoreplacetheexistingwithinRange,Inameitthesamebutwithaneasilyreplaceableprefix.
Ithenaddthebodyofthefunction,whichreliesoncallingtheexisting
withinRange.Thebodythusconsistsofamappingfromthenewparametertotheexistingones.
classHeatingPlan…
xxNEWwithinRange(aNumberRange){
returnthis.withinRange(aNumberRange.low,aNumberRange.high);
}
NowIcanbegintheseriouswork,takingtheexistingfunctioncallsandhavingthemcallthenewfunction.
caller…
constlow=aRoom.daysTempRange.low;
consthigh=aRoom.daysTempRange.high;
if(!aPlan.xxNEWwithinRange(aRoom.daysTempRange))
alerts.push("roomtemperaturewentoutsiderange");
WhenI’vechangedthecalls,Imayseethatsomeoftheearliercodeisn’tneededanymore,soIwieldRemoveDeadCode(236).
caller…
constlow=aRoom.daysTempRange.low;
consthigh=aRoom.daysTempRange.high;
if(!aPlan.xxNEWwithinRange(aRoom.daysTempRange))
alerts.push("roomtemperaturewentoutsiderange");
Ireplacetheseoneatatime,testingaftereachchange.
OnceI’vereplacedthemall,IcanuseInlineFunction(115)ontheoriginalfunction.
classHeatingPlan…
xxNEWwithinRange(aNumberRange){
return(aNumberRange.low>=this._temperatureRange.low)&&
(aNumberRange.high<=this._temperatureRange.high);
}
AndIfinallyremovethatuglyprefixfromthenewfunctionandallitscallers.Theprefixmakesitasimpleglobalreplace,evenifIdon’thavearobustrenamesupportinmyeditor.
classHeatingPlan…
withinRange(aNumberRange){
return(aNumberRange.low>=this._temperatureRange.low)&&
(aNumberRange.high<=this._temperatureRange.high);
}
caller…
if(!aPlan.withinRange(aRoom.daysTempRange))
alerts.push("roomtemperaturewentoutsiderange");
Example:AVariationtoCreatetheNewFunction
Intheaboveexample,Iwrotethecodeforthenewfunctiondirectly.Mostofthetime,that’sprettysimpleandtheeasiestwaytogo.Butthereisavariationonthisthat’soccasionallyuseful—whichcanallowmetocomposethenewfunctionentirelyfromrefactorings.
Istartwithacalleroftheexistingfunction.
caller…
constlow=aRoom.daysTempRange.low;
consthigh=aRoom.daysTempRange.high;
if(!aPlan.withinRange(low,high))
alerts.push("roomtemperaturewentoutsiderange");
IwanttorearrangethecodesoIcancreatethenewfunctionbyusingExtractFunction(106)onsomeexistingcode.Thecallercodeisn’tquitethereyet,butIcangettherebyusingExtractVariable(119)afewtimes.First,Idisentanglethecalltotheoldfunctionfromtheconditional.
caller…
constlow=aRoom.daysTempRange.low;
consthigh=aRoom.daysTempRange.high;
constisWithinRange=aPlan.withinRange(low,high);
if(!isWithinRange)
alerts.push("roomtemperaturewentoutsiderange");
Ithenextracttheinputparameter.
caller…
consttempRange=aRoom.daysTempRange;
constlow=tempRange.low;
consthigh=tempRange.high;
constisWithinRange=aPlan.withinRange(low,high);
if(!isWithinRange)
alerts.push("roomtemperaturewentoutsiderange");
Withthatdone,IcannowuseExtractFunction(106)tocreatethenewfunction.
caller…
consttempRange=aRoom.daysTempRange;
constisWithinRange=xxNEWwithinRange(aPlan,tempRange);
if(!isWithinRange)
alerts.push("roomtemperaturewentoutsiderange");
toplevel…
functionxxNEWwithinRange(aPlan,tempRange){
constlow=tempRange.low;
consthigh=tempRange.high;
constisWithinRange=aPlan.withinRange(low,high);
returnisWithinRange;
}
Sincetheoriginalfunctionisinadifferentcontext(theHeatingPlanclass),IneedtouseMoveFunction(196).
caller…
consttempRange=aRoom.daysTempRange;
constisWithinRange=aPlan.xxNEWwithinRange(tempRange);
if(!isWithinRange)
alerts.push("roomtemperaturewentoutsiderange");
classHeatingPlan…
xxNEWwithinRange(tempRange){
constlow=tempRange.low;
consthigh=tempRange.high;
constisWithinRange=this.withinRange(low,high);
returnisWithinRange;
}
Ithencontinueasbefore,replacingothercallersandinliningtheoldfunctionintothenewone.IwouldalsoinlinethevariablesIextractedtoprovidethecleanseparationforextractingthenewfunction.
Becausethisvariationisentirelycomposedofrefactorings,it’sparticularlyhandywhenIhavearefactoringtoolwithrobustextractandinlineoperations.
ReplaceParameterwithQuery
formerly:ReplaceParameterwithMethod
inverseof:ReplaceQuerywithParameter(325)
Motivation
Theparameterlisttoafunctionshouldsummarizethepointsofvariabilityofthatfunction,indicatingtheprimarywaysinwhichthatfunctionmaybehavedifferently.Aswithanystatementincode,it’sgoodtoavoidanyduplication,anditseasiertounderstandiftheparameterlistisshort.
Ifacallpassesinavaluethatthefunctioncanjustaseasilydetermineforitself,
that’saformofduplication—onethatunnecessarilycomplicatesthecallerwhichhastodeterminethevalueofaparameterwhenitcouldbefreedfromthatwork.
Thelimitonthisissuggestedbythephrase“justaseasily.”Byremovingtheparameter,I’mshiftingtheresponsibilityfordeterminingtheparametervalue.Whentheparameterispresent,determiningitsvalueisthecaller’sresponsibility;otherwise,thatresponsibilityshiftstothefunctionbody.Myusualhabitistosimplifylifeforcallers,whichimpliesmovingresponsibilitytothefunctionbody—butonlyifthatresponsibilityisappropriatethere.
ThemostcommonreasontoavoidReplaceParameterwithQueryisifremovingtheparameteraddsanunwanteddependencytothefunctionbody—forcingittoaccessaprogramelementthatI’dratheritremainedignorantof.Thismaybeanewdependency,oranexistingonethatI’dliketoremove.UsuallythiscomesupwhereI’dneedtoaddaproblematicfunctioncalltothefunctionbody,oraccesssomethingwithinareceiverobjectthatI’dprefertomoveoutlater.
ThesafestcaseforReplaceParameterwithQueryiswhenthevalueoftheparameterIwanttoremoveisdeterminedmerelybyqueryinganotherparameterinthelist.There’srarelyanypointinpassingtwoparametersifonecanbedeterminedfromtheother.
OnethingtowatchoutforisifthefunctionI’mlookingathasreferentialtransparency—thatis,ifIcanbesurethatitwillbehavethesamewaywheneverit’scalledwiththesameparametervalues.Suchfunctionsaremucheasiertoreasonaboutandtest,andIdon’twanttoalterthemtolosethatproperty.SoIwouldn’treplaceaparameterwithanaccesstoamutableglobalvariable.
Mechanics
Ifnecessary,useExtractFunction(106)onthecalculationoftheparameter.
Replacereferencestotheparameterinthefunctionbodywithreferencestotheexpressionthatyieldstheparameter.Testaftereachchange.
UseChangeFunctionDeclaration(124)toremovetheparameter.
Example
ImostoftenuseReplaceParameterwithQuerywhenI’vedonesomeotherrefactoringsthatmakeaparameternolongerneeded.Considerthiscode.
classOrder…
getfinalPrice(){
constbasePrice=this.quantity*this.itemPrice;
letdiscountLevel;
if(this.quantity>100)discountLevel=2;
elsediscountLevel=1;
returnthis.discountedPrice(basePrice,discountLevel);
}
discountedPrice(basePrice,discountLevel){
switch(discountLevel){
case1:returnbasePrice*0.95;
case2:returnbasePrice*0.9;
}
}
WhenI’msimplifyingafunction,I’mkeentoapplyReplaceTempwithQuery(176),whichwouldleadmeto
classOrder…
getfinalPrice(){
constbasePrice=this.quantity*this.itemPrice;
returnthis.discountedPrice(basePrice,this.discountLevel);
}
getdiscountLevel(){
return(this.quantity>100)?2:1;
}
OnceI’vedonethis,there’snoneedtopasstheresultofdiscountLeveltodiscountedPrice—itcanjustaseasilymakethecallitself.
Ireplaceanyreferencetotheparameterwithacalltothemethodinstead.
classOrder…
discountedPrice(basePrice,discountLevel){
switch(this.discountLevel){
case1:returnbasePrice*0.95;
case2:returnbasePrice*0.9;
}
}
IcanthenuseChangeFunctionDeclaration(124)toremovetheparameter.
classOrder…
getfinalPrice(){
constbasePrice=this.quantity*this.itemPrice;
returnthis.discountedPrice(basePrice,this.discountLevel);
}
discountedPrice(basePrice,discountLevel){
switch(this.discountLevel){
case1:returnbasePrice*0.95;
case2:returnbasePrice*0.9;
}
}
ReplaceQuerywithParameter
inverseof:ReplaceParameterwithQuery(322)
Motivation
Whenlookingthroughafunction’sbody,Isometimesseereferencestosomethinginthefunction’sscopethatI’mnothappywith.Thismightbeareferencetoaglobalvariable,ortoanelementinthesamemodulethatIintendtomoveaway.Toresolvethis,Ineedtoreplacetheinternalreferencewithaparameter,shiftingtheresponsibilityofresolvingthereferencetothecallerofthefunction.
Mostofthesecasesareduetomywishtoalterthedependencyrelationshipsinthecode—tomakethetargetfunctionnolongerdependentontheelementIwanttoparameterize.There’satensionherebetweenconvertingeverythingtoparameters,whichresultsinlongrepetitiveparameterlists,andsharingalotofscopewhichcanleadtoalotofcouplingbetweenfunctions.Likemosttrickydecisions,it’snotsomethingIcanreliablygetright,soit’simportantthatIcanreliablychangethingssotheprogramcantakeadvantageofmyincreasingunderstanding.
It’seasiertoreasonaboutafunctionthatwillalwaysgivethesameresultwhencalledwithsameparametervalues—thisiscalledreferentialtransparency.Ifafunctionaccessessomeelementinitsscopethatisn’treferentiallytransparent,thenthecontainingfunctionalsolacksreferentialtransparency.Icanfixthatbymovingthatelementtoaparameter.Althoughsuchamovewillshiftresponsibilitytothecaller,thereisoftenalottobegainedbycreatingclearmoduleswithreferentialtransparency.AcommonpatternistohavemodulesconsistingofpurefunctionswhicharewrappedbylogicthathandlestheI/Oandothervariableelementsofaprogram.IcanuseReplaceQuerywithParametertopurifypartsofaprogram,makingthosepartseasiertotestandreasonabout.
ButReplaceQuerywithParameterisn’tjustabagofbenefits.Bymovingaquerytoaparameter,Iforcemycallertofigureouthowtoprovidethisvalue.Thiscomplicateslifeforcallersofthefunctions,andmyusualbiasistodesigninterfacesthatmakelifeeasierfortheirconsumers.Intheend,itboilsdowntoallocationofresponsibilityaroundtheprogram,andthat’sadecisionthat’sneithereasynorimmutable—whichiswhythisrefactoring(anditsinverse)isonethatIneedtobeveryfamiliarwith.
Mechanics
UseExtractVariable(119)onthequerycodetoseparateitfromtherestofthefunctionbody.
ApplyExtractFunction(106)tothebodycodethatisn’tthecalltothequery.
Givethenewfunctionaneasilysearchablename,forlaterrenaming.
UseInlineVariable(123)togetridofthevariableyoujustcreated.
ApplyInlineFunction(115)totheoriginalfunction.
Renamethenewfunctiontothatoftheoriginal.
Example
Considerasimple,yetannoying,controlsystemfortemperature.Itallowstheusertoselectatemperatureonathermostat—butonlysetsthetargettemperaturewithinarangedeterminedbyaheatingplan.
classHeatingPlan…
gettargetTemperature(){
if(thermostat.selectedTemperature>this._max)returnthis._max;
elseif(thermostat.selectedTemperature<this._min)returnthis._min;
elsereturnthermostat.selectedTemperature;
}
caller…
if(thePlan.targetTemperature>thermostat.currentTemperature)setToHeat();
elseif(thePlan.targetTemperature<thermostat.currentTemperature)setToCool();
elsesetOff();
Asauserofsuchasystem,Imightbeannoyedtohavemydesiresoverriddenbytheheatingplanrules,butasaprogrammerImightbemoreconcernedabouthowthetargetTemperaturefunctionhasadependencyonaglobalthermostatobject.Icanbreakthisdependencybymovingittoaparameter.
MyfirststepistouseExtractVariable(119)ontheparameterthatIwanttohaveinmyfunction.
classHeatingPlan…
gettargetTemperature(){
constselectedTemperature=thermostat.selectedTemperature;
if(selectedTemperature>this._max)returnthis._max;
elseif(selectedTemperature<this._min)returnthis._min;
elsereturnselectedTemperature;
}
ThatmakesiteasytoapplyExtractFunction(106)ontheentirebodyofthefunctionexceptforthebitthatfiguresouttheparameter.
classHeatingPlan…
gettargetTemperature(){
constselectedTemperature=thermostat.selectedTemperature;
returnthis.xxNEWtargetTemperature(selectedTemperature);
}
xxNEWtargetTemperature(selectedTemperature){
if(selectedTemperature>this._max)returnthis._max;
elseif(selectedTemperature<this._min)returnthis._min;
elsereturnselectedTemperature;
}
ItheninlinethevariableIjustextracted,whichleavesthefunctionasasimplecall.
classHeatingPlan…
gettargetTemperature(){
returnthis.xxNEWtargetTemperature(thermostat.selectedTemperature);
}
IcannowuseInlineFunction(115)onthismethod.
caller…
if(thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature)
thermostat.currentTemperature)
setToHeat();
elseif(thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature)
thermostat.currentTemperature)
setToCool();
else
setOff();
Itakeadvantageoftheeasilysearchablenameofthenewfunctiontorenameit
byremovingtheprefix.
caller…
if(thePlan.targetTemperature(thermostat.selectedTemperature)>
thermostat.currentTemperature)
setToHeat();
elseif(thePlan.targetTemperature(thermostat.selectedTemperature)<
thermostat.currentTemperature)
setToCool();
else
setOff();
classHeatingPlan…
targetTemperature(selectedTemperature){
if(selectedTemperature>this._max)returnthis._max;
elseif(selectedTemperature<this._min)returnthis._min;
elsereturnselectedTemperature;
}
Asisoftenthecasewiththisrefactoring,thecallingcodelooksmoreunwieldythanbefore.Movingadependencyoutofamodulepushestheresponsibilityofdealingwiththatdependencybacktothecaller.That’sthetrade-offforthereducedcoupling.
Butremovingthecouplingtothethermostatobjectisn’ttheonlygainI’vemadewiththisrefactoring.TheHeatingPlanclassisimmutable—itsfieldsaresetintheconstructorwithnomethodstoalterthem.(I’llsaveyoutheeffortoflookingatthewholeclass;justtrustmeonthis.)Givenanimmutableheatingplan,bymovingthethermostatreferenceoutofthefunctionbodyI’vealsomadetargetTemperaturereferentiallytransparent.EverytimeIcalltargetTemperatureonthesameobject,withthesameargument,Iwillgetthesameresult.Ifallthemethodsoftheheatingplanhavereferentialtransparency,thatmakesthisclassmucheasiertotestandreasonabout.
AproblemwithJavaScript’sclassmodelisthatit’simpossibletoenforceanimmutableclass—there’salwaysawaytogetatanobject’sdata.Butwritingaclasstosignalandencourageimmutabilityisoftengoodenough.CreatingclassesthathavethischaracteristicisoftenasoundstrategyandReplaceQuerywithParameterisahandytoolfordoingthis.
RemoveSettingMethod
Motivation
Providingasettingmethodindicatesthatafieldmaybechanged.IfIdon’twantthatfieldtochangeoncetheobjectiscreated,Idon’tprovideasettingmethod(andmakethefieldimmutable).Thatway,thefieldissetonlyintheconstructor,myintentiontohaveitnotchangeisclear,andIusuallyremovetheverypossibilitythatthefieldwillchange.
There’sacoupleofcommoncaseswherethiscomesup.Oneiswherepeoplealwaysuseaccessormethodstomanipulateafield,evenwithinconstructors.Thisleadstotheonlycalltoasettingmethodbeingfromtheconstructor.Iprefertoremovethesettingmethodtomakeitclearthatupdatesmakenosenseafterconstruction.
Anothercaseiswheretheobjectiscreatedbyclientsusingcreationscriptratherthanbyasimpleconstructorcall.Suchacreationscriptstartswiththeconstructorcallfollowedbyasequenceofsettermethodcallstocreatethenewobject.Oncethescriptisfinished,wedon’texpectthenewobjecttochangesome(orevenall)ofitsfields.Thesettersareonlyexpectedtobecalledduringthisinitialcreation.Inthiscase,I’dgetridofthemtomakemyintentions
clearer.
Mechanics
Ifthevaluethat’sbeingsetisn’tprovidedtotheconstructor,useChangeFunctionDeclaration(124)toaddit.Addacalltothesettingmethodwithintheconstructor.
Ifyouwishtoremoveseveralsettingmethods,addalltheirvaluestotheconstructoratonce.Thissimplifiesthelatersteps.
Removeeachcallofasettingmethodoutsideoftheconstructor,usingthenewconstructorvalueinstead.Testaftereachone.
Ifyoucan’treplacethecalltothesetterbycreatinganewobject(becauseyouareupdatingasharedreferenceobject),abandontherefactoring.
UseInlineFunction(115)onthesettingmethod.Makethefieldimmutableifpossible.
Test.
Example
IhaveasimplePersonclass.
classPerson…
getname(){returnthis._name;}
setname(arg){this._name=arg;}
getid(){returnthis._id;}
setid(arg){this._id=arg;}
Atthemoment,Icreateanewobjectwithcodelikethis:
constmartin=newPerson();
martin.name="martin";
martin.id="1234";
Thenameofapersonmaychangeafterit’screated,buttheIDdoesnot.Tomakethisclear,IwanttoremovethesettingmethodforID.
IstillneedtosettheIDinitially,soI’lluseChangeFunctionDeclaration(124)toaddittotheconstructor.
classPerson…
constructor(id){
this.id=id;
}
IthenadjustthecreationscripttosettheIDviatheconstructor.
constmartin=newPerson("1234");
martin.name="martin";
martin.id="1234";
IdothisineachplaceIcreateaperson,testingaftereachchange.
Whentheyarealldone,IcanapplyInlineFunction(115)tothesettingmethod.
classPerson…
constructor(id){
this._id=id;
}
getname(){returnthis._name;}
setname(arg){this._name=arg;}
getid(){returnthis._id;}
setid(arg){this._id=arg;}
ReplaceConstructorwithFactoryFunction
formerly:ReplaceConstructorwithFactoryMethod
Motivation
Manyobject-orientedlanguageshaveaspecialconstructorfunctionthat’scalledtoinitializeanobject.Clientstypicallycallthisconstructorwhentheywanttocreateanewobject.Buttheseconstructorsoftencomewithawkwardlimitationsthataren’tthereformoregeneralfunctions.AJavaconstructormustreturnaninstanceoftheclassitwascalledwith,whichmeansIcan’treplaceitwithasubclassorproxydependingontheenvironmentorparameters.Constructornamingisfixed,whichmakesitimpossibleformetouseanamethatisclearerthanthedefault.Constructorsoftenrequireaspecialoperatortoinvoke(“new”inmanylanguages)whichmakesthemdifficulttouseincontextsthatexpectnormalfunctions.
Afactoryfunctionsuffersfromnosuchlimitations.Itwilllikelycalltheconstructoraspartofitsimplementation,butIcanfreelysubstitutesomethingelse.
Mechanics
Createafactoryfunction,itsbodybeingacalltotheconstructor.
Replaceeachcalltotheconstructorwithacalltothefactoryfunction.
Testaftereachchange.
Limittheconstructor’svisibilityasmuchaspossible.
Example
Aquickbutwearisomeexampleuseskindsofemployees.Consideranemployeeclass:
classEmployee…
constructor(name,typeCode){
this._name=name;
this._typeCode=typeCode;
}
getname(){returnthis._name;}
gettype(){
returnEmployee.legalTypeCodes[this._typeCode];
}
staticgetlegalTypeCodes(){
return{"E":"Engineer","M":"Manager","S":"Salesman"};
}
Thisisusedfrom
caller…
candidate=newEmployee(document.name,document.empType);
and
caller…
constleadEngineer=newEmployee(document.leadEngineer,'E');
Myfirststepistocreatethefactoryfunction.Itsbodyisasimpledelegationtotheconstructor.
toplevel…
functioncreateEmployee(name,typeCode){
returnnewEmployee(name,typeCode);
}
Ithenfindthecallersoftheconstructorandchangethem,oneatatime,tousethefactoryfunctioninstead.
Thefirstoneisobvious:
caller…
candidate=createEmployee(document.name,document.empType);
Withthesecondcase,Icouldusethenewfactoryfunctionlikethis:
caller…
constleadEngineer=createEmployee(document.leadEngineer,'E');
ButIdon’tlikeusingthetypecodehere—it’sgenerallyabadsmelltopassacodeasaliteralstring.SoIprefertocreateanewfactoryfunctionthatembedsthekindofemployeeIwantintoitsname.
caller…
constleadEngineer=createEngineer(document.leadEngineer);
toplevel…
functioncreateEngineer(name){
returnnewEmployee(name,'E');
}
ReplaceFunctionwithCommand
formerly:ReplaceMethodwithMethodObject
inverseof:ReplaceCommandwithFunction(342)
Motivation
Functions—eitherfreestandingorattachedtoobjectsasmethods—areoneofthefundamentalbuildingblocksofprogramming.Buttherearetimeswhenit’susefultoencapsulateafunctionintoitsownobject,whichIrefertoasa“commandobject”orsimplyacommand.Suchanobjectismostlybuiltaroundasinglemethod,whoserequestandexecutionisthepurposeoftheobject.
Acommandoffersagreaterflexibilityforthecontrolandexpressionofafunctionthantheplainfunctionmechanism.Commandscanhavecomplimentaryoperations,suchasundo.Icanprovidemethodstobuilduptheirparameterstosupportaricherlifecycle.Icanbuildincustomizationsusinginheritanceandhooks.IfI’mworkinginalanguagewithobjectsbutwithoutfirst-classfunctions,Icanprovidemuchofthatcapabilitybyusingcommandsinstead.Similarly,Icanusemethodsandfieldstohelpbreakdownacomplexfunction,eveninalanguagethatlacksnestedfunctions,andIcancallthosemethodsdirectlywhiletestinganddebugging.
Allthesearegoodreasonstousecommands,andIneedtobereadytorefactorfunctionsintocommandswhenIneedto.Butwemustnotforgetthatthisflexibility,asever,comesatapricepaidincomplexity.So,giventhechoicebetweenafirst-classfunctionandacommand,I’llpickthefunction95%ofthetime.IonlyuseacommandwhenIspecificallyneedafacilitythatsimplerapproachescan’tprovide.
Likemanywordsinsoftwaredevelopment,“command”isratheroverloaded.InthecontextI’musingithere,itisanobjectthatencapsulatesarequest,followingthecommandpatterninDesignPatterns[bib-gof].WhenIuse“command”inthissense,Iuse“commandobject”tosetthecontext,and“command”afterwards.Theword“command”isalsousedinthecommand-queryseparationprinciple[bib-cqs],whereacommandisanobjectmethodthatchangesobservablestate.I’vealwaystriedtoavoidusingcommandinthatsense,preferring“modifier”or“mutator”.
Mechanics
Createanemptyclassforthefunction.Nameitbasedonthefunction.
UseMoveFunction(196)tomovethefunctiontotheemptyclass.
Keeptheoriginalfunctionasaforwardingfunctionuntilatleasttheendoftherefactoring.
Followanyconventionthelanguagehasfornamingcommands.Ifthereisnoconvention,chooseagenericnameforthecommand’sexecutefunction,suchas“execute”or“call”.
Considermakingafieldforeachargument,andmovetheseargumentstotheconstructor.
Example
TheJavaScriptlanguagehasmanyfaults,butoneofitsgreatdecisionswastomakefunctionsfirst-classentities.Ithusdon’thavetogothroughallthehoopsofcreatingcommandsforcommontasksthatIneedtodoinlanguageswithoutthisfacility.Buttherearestilltimeswhenacommandistherighttoolforthejob.
OneofthesecasesisbreakingupacomplexfunctionsoIcanbetterunderstandandmodifyit.Toreallyshowthevalueofthisrefactoring,Ineedalongandcomplicatedfunction—butthatwouldtaketoolongtowrite,letaloneforyoutoread.Instead,I’llgowithafunctionthat’sshortenoughnottoneedit.Thisonescorespointsforaninsuranceapplication.
functionscore(candidate,medicalExam,scoringGuide){
letresult=0;
lethealthLevel=0;
lethighMedicalRiskFlag=false;
if(medicalExam.isSmoker){
healthLevel+=10;
highMedicalRiskFlag=true;
}
letcertificationGrade="regular";
if(scoringGuide.stateWithLowCertification(candidate.originState)){
certificationGrade="low";
result-=5;
}
//lotsmorecodelikethis
result-=Math.max(healthLevel-5,0);
returnresult;
}
IbeginbycreatinganemptyclassandthenMoveFunction(196)tomovethefunctionintoit.
functionscore(candidate,medicalExam,scoringGuide){
returnnewScorer().execute(candidate,medicalExam,scoringGuide);
}
classScorer{
execute(candidate,medicalExam,scoringGuide){
letresult=0;
lethealthLevel=0;
lethighMedicalRiskFlag=false;
if(medicalExam.isSmoker){
healthLevel+=10;
highMedicalRiskFlag=true;
}
letcertificationGrade="regular";
if(scoringGuide.stateWithLowCertification(candidate.originState)){
certificationGrade="low";
result-=5;
}
//lotsmorecodelikethis
result-=Math.max(healthLevel-5,0);
returnresult;
}
}
Mostofthetime,Iprefertopassargumentstoacommandontheconstructorandhavetheexecutemethodtakenoparameters.Whilethismatterslessforasimpledecompositionscenariolikethis,it’sveryhandywhenIwanttomanipulatethecommandwithamorecomplicatedparametersettinglifecycleorcustomizations.Differentcommandclassescanhavedifferentparametersbutbemixedtogetherwhenqueuedforexecution.
Icandotheseparametersoneatatime.
functionscore(candidate,medicalExam,scoringGuide){
returnnewScorer(candidate).execute(candidate,medicalExam,scoringGuide);
}
classScorer…
constructor(candidate){
this._candidate=candidate;
}
execute(candidate,medicalExam,scoringGuide){
letresult=0;
lethealthLevel=0;
lethighMedicalRiskFlag=false;
if(medicalExam.isSmoker){
healthLevel+=10;
highMedicalRiskFlag=true;
}
letcertificationGrade="regular";
if(scoringGuide.stateWithLowCertification(this._candidate.originState)){
certificationGrade="low";
result-=5;
}
//lotsmorecodelikethis
result-=Math.max(healthLevel-5,0);
returnresult;
}
Icontinuewiththeotherparameters
functionscore(candidate,medicalExam,scoringGuide){
returnnewScorer(candidate,medicalExam,scoringGuide).execute();
}
classScorer…
constructor(candidate,medicalExam,scoringGuide){
this._candidate=candidate;
this._medicalExam=medicalExam;
this._scoringGuide=scoringGuide;
}
execute(){
letresult=0;
lethealthLevel=0;
lethighMedicalRiskFlag=false;
if(this._medicalExam.isSmoker){
healthLevel+=10;
highMedicalRiskFlag=true;
}
letcertificationGrade="regular";
if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
certificationGrade="low";
result-=5;
}
//lotsmorecodelikethis
result-=Math.max(healthLevel-5,0);
returnresult;
}
ThatcompletesReplaceFunctionwithCommand,butthewholepointofdoingthisrefactoringistoallowmetobreakdownthecomplicatedfunctions—soletmeoutlinesomestepstoachievethat.Mynextmovehereistochangeallthe
localvariablesintofields.Again,Idotheseoneatatime.
classScorer…
constructor(candidate,medicalExam,scoringGuide){
this._candidate=candidate;
this._medicalExam=medicalExam;
this._scoringGuide=scoringGuide;
}
execute(){
this._result=0;
lethealthLevel=0;
lethighMedicalRiskFlag=false;
if(this._medicalExam.isSmoker){
healthLevel+=10;
highMedicalRiskFlag=true;
}
letcertificationGrade="regular";
if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
certificationGrade="low";
this._result-=5;
}
//lotsmorecodelikethis
this._result-=Math.max(healthLevel-5,0);
returnthis._result;
}
Irepeatthisforallthelocalvariables.(ThisisoneofthoserefactoringsthatIfeltwassufficientlysimplethatIhaven’tgivenitanentryinthecatalog.Ifeelslightlyguiltyaboutthis.)
classScorer…
constructor(candidate,medicalExam,scoringGuide){
this._candidate=candidate;
this._medicalExam=medicalExam;
this._scoringGuide=scoringGuide;
}
execute(){
this._result=0;
this._healthLevel=0;
this._highMedicalRiskFlag=false;
if(this._medicalExam.isSmoker){
this._healthLevel+=10;
this._highMedicalRiskFlag=true;
}
this._certificationGrade="regular";
if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
this._certificationGrade="low";
this._result-=5;
}
//lotsmorecodelikethis
this._result-=Math.max(this._healthLevel-5,0);
returnthis._result;
}
NowI’vemovedallthefunction’sstatetothecommandobject,IcanuserefactoringslikeExtractFunction(106)withoutgettingtangledupinallthevariablesandtheirscopes.
classScorer…
constructor(candidate,medicalExam,scoringGuide){
this._candidate=candidate;
this._medicalExam=medicalExam;
this._scoringGuide=scoringGuide;
}
execute(){
this._result=0;
this._healthLevel=0;
this._highMedicalRiskFlag=false;
this.scoreSmoking();
this._certificationGrade="regular";
if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){
this._certificationGrade="low";
this._result-=5;
}
//lotsmorecodelikethis
this._result-=Math.max(this._healthLevel-5,0);
returnthis._result;
}
scoreSmoking(){
if(this._medicalExam.isSmoker){
this._healthLevel+=10;
this._highMedicalRiskFlag=true;
}
}
ThisallowsmetotreatthecommandsimilarlytohowI’ddealwithanested
function.Indeed,whendoingthisrefactoringinJavaScript,usingnestedfunctionswouldbeareasonablealternativetousingacommand.I’dstilluseacommandforthis,partlybecauseI’mmorefamiliarwithcommandsandpartlybecausewithacommandIcanwritetestsanddebuggingcallsagainstthesubfunctions.
ReplaceCommandwithFunction
inverseof:ReplaceFunctionwithCommand(335)
Motivation
Commandobjectsprovideapowerfulmechanismforhandlingcomplexcomputations.Theycaneasilybebrokendownintoseparatemethodssharingcommonstatethroughthefields;theycanbeinvokedviadifferentmethodsfordifferenteffects;theycanhavetheirdatabuiltupinstages.Butthatpowercomesatacost.Mostofthetime,Ijustwanttoinvokeafunctionandhaveitdo
itsthing.Ifthat’sthecase,andthefunctionisn’ttoocomplex,thenacommandobjectismoretroublethanitsworthandshouldbeturnedintoaregularfunction.
Mechanics
ApplyExtractFunction(106)tothecreationofthecommandandthecalltothecommand’sexecutionmethod.
Thiscreatesthenewfunctionthatwillreplacethecommandinduecourse.
Foreachmethodcalledbythecommand’sexecutionmethod,applyInlineFunction(115).
Ifthesupportingfunctionreturnsavalue,useExtractVariable(119)onthecallfirstandthenInlineFunction(115).
UseChangeFunctionDeclaration(124)toputalltheparametersoftheconstructorintothecommand’sexecutionmethodinstead.
Foreachfield,alterthereferencesinthecommand’sexecutionmethodtousetheparameterinstead.Testaftereachchange.
Inlinetheconstructorcallandcommand’sexecutionmethodcallintothecaller(whichisthereplacementfunction).
Test.
ApplyRemoveDeadCode(236)tothecommandclass.
Example
I’llbeginwiththissmallcommandobject.
classChargeCalculator{
constructor(customer,usage,provider){
this._customer=customer;
this._usage=usage;
this._provider=provider;
}
getbaseCharge(){
returnthis._customer.baseRate*this._usage;
}
getcharge(){
returnthis.baseCharge+this._provider.connectionCharge;
}
}
Itisusedbycodelikethis:
caller…
monthCharge=newChargeCalculator(customer,usage,provider).charge;
Thecommandclassissmallandsimpleenoughtobebetteroffasafunction.
IbeginbyusingExtractFunction(106)towraptheclasscreationandinvocation.
caller…
monthCharge=charge(customer,usage,provider);
toplevel…
functioncharge(customer,usage,provider){
returnnewChargeCalculator(customer,usage,provider).charge;
}
Ihavetodecidehowtodealwithanysupportingfunctions,inthiscasebaseCharge.MyusualapproachforafunctionthatreturnsavalueistofirstExtractVariable(119)onthatvalue.
classChargeCalculator…
getbaseCharge(){
returnthis._customer.baseRate*this._usage;
}
getcharge(){
constbaseCharge=this.baseCharge;
returnbaseCharge+this._provider.connectionCharge;
}
Then,IuseInlineFunction(115)onthesupportingfunction.
classChargeCalculator…
getcharge(){
constbaseCharge=this._customer.baseRate*this._usage;
returnbaseCharge+this._provider.connectionCharge;
}
Inowhavealltheprocessinginasinglefunction,somynextmoveistomovethedatapassedtotheconstructortothemainmethod.IfirstuseChangeFunctionDeclaration(124)toaddalltheconstructorparameterstothechargemethod.
classChargeCalculator…
constructor(customer,usage,provider){
this._customer=customer;
this._usage=usage;
this._provider=provider;
}
charge(customer,usage,provider){
constbaseCharge=this._customer.baseRate*this._usage;
returnbaseCharge+this._provider.connectionCharge;
}
toplevel…
functioncharge(customer,usage,provider){
returnnewChargeCalculator(customer,usage,provider)
.charge(customer,usage,provider);
}
NowIcanalterthebodyofchargetousethepassedparametersinstead.Icandothisoneatatime.
classChargeCalculator…
constructor(customer,usage,provider){
this._customer=customer;
this._usage=usage;
this._provider=provider;
}
charge(customer,usage,provider){
constbaseCharge=customer.baseRate*this._usage;
returnbaseCharge+this._provider.connectionCharge;
}
Idon’thavetoremovetheassignmenttothis._customerintheconstructor,asitwilljustbeignored.ButIprefertodoitsincethatwillmakeatestfailifImisschangingauseoffieldtotheparameter.(Andifatestdoesn’tfail,Ishouldconsideraddinganewtest.)
Irepeatthisfortheotherparameters,endingupwith
classChargeCalculator…
charge(customer,usage,provider){
constbaseCharge=customer.baseRate*usage;
returnbaseCharge+provider.connectionCharge;
}
OnceI’vedoneallofthese,Icaninlineintothetop-levelchargefunction.ThisisaspecialkindofInlineFunction(115),asit’sinliningboththeconstructorandmethodcalltogether.
toplevel…
functioncharge(customer,usage,provider){
constbaseCharge=customer.baseRate*usage;
returnbaseCharge+provider.connectionCharge;
}
Thecommandclassisnowdeadcode,soI’lluseRemoveDeadCode(236)togiveitanhonorableburial.
Chapter12DealingwithInheritanceInthisfinalchapter,I’llturntooneofthebestknownfeaturesofobject-orientedprogramming:inheritance.Likeanypowerfulmechanism,itisbothveryusefulandeasytomisuse,andit’softenhardtoseethemisuseuntilit’sintherear-viewmirror.
Often,featuresneedtomoveupordowntheinheritancehierarchy.Severalrefactoringsdealwiththat:PullUpMethod(348),PullUpField(351),PullUpConstructorBody(353),PushDownMethod(357),andPushDownField(359).IcanaddandremoveclassesfromthehierachywithExtractSuperclass(373),RemoveSubclass(368),andCollapseHierarchy(378).ImaywanttoaddasubclasstoreplaceafieldthatI’musingtotriggerdifferentbehaviorbasedonitsvalue;IdothiswithReplaceTypeCodewithSubclasses(361).
Inheritanceisapowerfultool,butsometimesitgetsusedinthewrongplace—ortheplaceit’susedinbecomeswrong.Inthatcase,IuseReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397)toturninheritanceintodelegation.
PullUpMethod
inverseof:PushDownMethod(357)
Motivation
Eliminatingduplicatecodeisimportant.Twoduplicatemethodsmayworkfineastheyare,buttheyarenothingbutabreedinggroundforbugsinthefuture.Wheneverthereisduplication,thereisriskthatanalterationtoonecopywillnotbemadetotheother.Usually,itisdifficulttofindtheduplicates.
TheeasiestcaseofusingPullUpMethodiswhenthemethodshavethesamebody,implyingthere’sbeenacopyandpaste.Ofcourseit’snotalwaysasobviousasthat.Icouldjustdotherefactoringandseeifthetestscroak—butthat
putsalotofrelianceonmytests.Iusuallyfinditvaluabletolookforthedifferences—often,theyshowupbehaviorthatIforgottotestfor.
Often,PullUpMethodcomesafterothersteps.Iseetwomethodsindifferentclassesthatcanbeparameterizedinsuchawaythattheyendupasessentiallythesamemethod.Inthatcase,thesmalleststepisformetoapplyParameterizeFunction(308)separatelyandthenPullUpMethod.
ThemostawkwardcomplicationwithPullUpMethodisifthebodyofthemethodsreferstofeaturesthatareonthesubclassbutnotonthesuperclass.Whenthathappens,IneedtousePullUpField(351)andPullUpMethodonthoseelementsfirst.
IfIhavetwomethodswithasimilaroverallflow,butdifferingindetails,I’llconsidertheFormTemplateMethod[bib-form-template].
Mechanics
Inspectmethodstoensuretheyareidentical.
Iftheydothesamething,butarenotidentical,refactorthemuntiltheyhaveidenticalbodies.
Checkthatallmethodcallsandfieldreferencesinsidethemethodbodyrefertofeaturesthatcanbecalledfromthesuperclass.
Ifthemethodshavedifferentsignatures,useChangeFunctionDeclaration(124)togetthemtotheoneyouwanttouseonthesuperclass.
Createanewmethodinthesuperclass.Copythebodyofoneofthemethodsovertoit.
Runstaticchecks.
Deleteonesubclassmethod.
Test.
Keepdeletingsubclassmethodsuntiltheyareallgone.
Example
Ihavetwosubclassmethodsthatdothesamething.
classEmployeeextendsParty…
getannualCost(){
returnthis.monthlyCost*12;
}
classDepartmentextendsParty…
gettotalAnnualCost(){
returnthis.monthlyCost*12;
}
IlookatbothclassesandseethattheyrefertothemonthlyCostpropertywhichisn’tdefinedonthesuperclass,butispresentinbothsubclasses.SinceI’minadynamiclanguage,I’mOK;ifIwereinastaticlanguage,I’dneedtodefineanabstractmethodonParty.
Themethodshavedifferentnames,soIChangeFunctionDeclaration(124)tomakethemthesame.
classDepartment…
getannualCost(){
returnthis.monthlyCost*12;
}
Icopythemethodfromonesubclassandpasteitintothesuperclass.
classParty…
getannualCost(){
returnthis.monthlyCost*12;
}
Inastaticlanguage,I’dcompiletoensurethatallthereferenceswereOK.Thatwon’thelpmehere,soIfirstremoveannualCostfromEmployee,test,andthenremoveitfromDepartment.
Thatcompletestherefactoring,butdoesleaveaquestion.annualCostcalls
monthlyCost,butmonthlyCostdoesn’tappearinthePartyclass.Itallworks,becauseJavaScriptisadynamiclanguage—butthereisvalueinsignalingthatsubclassesofPartyshouldprovideanimplementationformonthlyCost,particularlyifmoresubclassesgetaddedlateron.Agoodwaytoprovidethissignalisatrapmethodlikethis:
classParty…
getmonthlyCost(){
thrownewSubclassResponsibilityError();
}
IcallsuchanerrorasubclassresponsibilityerrorasthatwasthenameusedinSmalltalk.
PullUpField
inverseof:PushDownField(359)
Motivation
Ifsubclassesaredevelopedindependently,orcombinedthroughrefactoring,Ioftenfindthattheyduplicatefeatures.Inparticular,certainfieldscanbeduplicates.Suchfieldssometimeshavesimilarnames—butnotalways.TheonlywayIcantellwhatisgoingonisbylookingatthefieldsandexamininghowtheyareused.Iftheyarebeingusedinasimilarway,Icanpullthemupintothesuperclass.
Bydoingthis,Ireduceduplicationintwoways.Iremovetheduplicatedata
declarationandIcanthenmovebehaviorthatusesthefieldfromthesubclassestothesuperclass.
Manydynamiclanguagesdonotdefinefieldsaspartoftheirclassdefinition—instead,fieldsappearwhentheyarefirstassignedto.Inthiscase,pullingupafieldisessentiallyaconsequenceofPullUpConstructorBody(353).
Mechanics
Inspectallusersofthecandidatefieldtoensuretheyareusedinthesameway.
Ifthefieldshavedifferentnames,useRenameField(244)togivethemthesamename.
Createanewfieldinthesuperclass.
Thenewfieldwillneedtobeaccessibletosubclasses(protectedincommonlanguages).
Deletethesubclassfields.
Test.
PullUpConstructorBody
Motivation
Constructorsaretrickythings.Theyaren’tquitenormalmethods—soI’mmorerestrictedinwhatIcandowiththem.
IfIseesubclassmethodswithcommonbehavior,myfirstthoughtistouseExtractFunction(106)followedbyPullUpMethod(348),whichwillmoveitnicelyintothesuperclass.Constructorstanglethat—becausetheyhavespecialrulesaboutwhatcanbedoneinwhatorder,soIneedaslightlydifferentapproach.
Ifthisrefactoringstartsgettingmessy,IreachforReplaceConstructorwithFactoryFunction(332).
Mechanics
Defineasuperclassconstructor,ifonedoesn’talreadyexist.Ensureit’scalledbysubclassconstructors.
UseSlideStatements(221)tomoveanycommonstatementstojustafterthesupercall.
Removethecommoncodefromeachsubclassandputitinthesuperclass.Addtothesupercallanyconstructorparametersreferencedinthecommoncode.
Test.
Ifthereisanycommoncodethatcannotmovetothestartoftheconstructor,useExtractFunction(106)followedbyPullUpMethod(348).
Example
Istartwiththefollowingcode:
classParty{}
classEmployeeextendsParty{
constructor(name,id,monthlyCost){
super();
this._id=id;
this._name=name;
this._monthlyCost=monthlyCost;
}
//restofclass…
classDepartmentextendsParty{
constructor(name,staff){
super();
this._name=name;
this._staff=staff;
}
//restofclass…
Thecommoncodehereistheassignmentofthename.IuseSlideStatements(221)tomovetheassignmentinEmployeenexttothecalltosuper():
classEmployeeextendsParty{
constructor(name,id,monthlyCost){
super();
this._name=name;
this._id=id;
this._monthlyCost=monthlyCost;
}
//restofclass…
Withthattested,Imovethecommoncodetothesuperclass.Sincethatcodecontainsareferencetoaconstructorargument,Ipassthatinasaparameter.
classParty…
constructor(name){
this._name=name;
}
classEmployee…
constructor(name,id,monthlyCost){
super(name);
this._id=id;
this._monthlyCost=monthlyCost;
}
classDepartment…
constructor(name,staff){
super(name);
this._staff=staff;
}
Runthetests,andI’mdone.
Mostofthetime,constructorbehaviorwillworklikethis:Dothecommonelementsfirst(withasupercall),thendoextraworkthatthesubclassneeds.Occasionally,however,thereissomecommonbehaviorlater.
Considerthisexample:
classEmployee…
constructor(name){…}
getisPrivileged(){…}
assignCar(){…}
classManagerextendsEmployee…
constructor(name,grade){
super(name);
this._grade=grade;
if(this.isPrivileged)this.assignCar();//everysubclassdoesthis
}
getisPrivileged(){
returnthis._grade>4;
}
ThewrinkleherecomesfromthefactthatthecalltoisPrivilegedcan’tbemadeuntilafterthegradefieldisassigned,andthatcanonlybedoneinthesubclass.
Inthiscase,IdoExtractFunction(106)onthecommoncode:
classManager…
constructor(name,grade){
super(name);
this._grade=grade;
this.finishConstruction();
}
finishConstruction(){
if(this.isPrivileged)this.assignCar();
}
Then,IusePullUpMethod(348)tomoveittothesuperclass.
classEmployee…
finishConstruction(){
if(this.isPrivileged)this.assignCar();
}
PushDownMethod
inverseof:PullUpMethod(348)
Motivation
Ifamethodisonlyrelevanttoonesubclass(orasmallproportionofsubclasses),removingitfromthesuperclassandputtingitonlyonthesubclass(es)makes
thatclearer.Icanonlydothisrefactoringifthecallerknowsit’sworkingwithaparticularsubclass—otherwise,IshoulduseReplaceConditionalwithPolymorphism(271)withsomeplacebobehavioronthesuperclass.
Mechanics
Copythemethodintoeverysubclassthatneedsit.
Removethemethodfromthesuperclass.
Test.
Removethemethodfromeachsuperclassthatdoesn’tneedit.
Test.
PushDownField
inverseof:PullUpField(351)
Motivation
Ifafieldisonlyusedbyonesubclass(orasmallproportionofsubclasses),Imoveittothosesubclasses.
Mechanics
Declarefieldinallsubclassesthatneedit.
Removethefieldfromthesuperclass.
Test.
Removethefieldfromallsubclassesthatdon’tneedit.
Test.
ReplaceTypeCodewithSubclasses
subsumes:ReplaceTypeCodewithState/Strategy
subsumes:ExtractSubclass
inverseof:RemoveSubclass(368)
Motivation
Softwaresystemsoftenneedtorepresentdifferentkindsofasimilarthing.Imayclassifyemployeesbytheirjobtype(engineer,manager,salesman),orordersbytheirpriority(rush,regular).Myfirsttoolforhandlingthisissomekindoftype
codefield—dependingonthelanguage,thatmightbeanenum,symbol,string,ornumber.Often,thistypecodewillcomefromanexternalservicethatprovidesmewiththedataI’mworkingon.
Mostofthetime,suchatypecodeisallIneed.ButthereareacoupleofsituationswhereIcoulddowithsomethingmore,andthatsomethingmorearesubclasses.Therearetwothingsthatareparticularlyenticingaboutsubclasses.First,theyallowmetousepolymorphismtohandleconditionallogic.IfindthismosthelpfulwhenIhaveseveralfunctionsthatinvokedifferentbehaviordependingonthevalueofthetypecode.Withsubclasses,IcanapplyReplaceConditionalwithPolymorphism(271)tothesefunctions.
ThesecondcaseiswhereIhavefieldsormethodsthatareonlyvalidforparticularvaluesofatypecode,suchasasalesquotathat’sonlyapplicabletothe“salesman”typecode.IcanthencreatethesubclassandapplyPushDownField(359).WhileIcanincludevalidationlogictoensureafieldisonlyusedwhenthetypecodehasthecorrectvalue,usingasubclassmakestherelationshipmoreexplicit.
WhenusingReplaceTypeCodewithSubclasses,IneedtoconsiderwhethertoapplyitdirectlytotheclassI’mlookingat,ortothetypecodeitself.DoImakeengineerasubtypeofemployee,orshouldIgivetheemployeeanemployeetypepropertywhichcanhavesubtypesforengineerandmanager?Usingdirectsubclassingissimpler,butIcan’tuseitforthejobtypeifIneeditforsomethingelse.Ialsocan’tusedirectsubclassesifthetypeismutable.IfIneedtomovethesubclassestoanemployeetypeproperty,IcandothatbyusingReplacePrimitivewithObject(172)onthetypecodetocreateanemployeetypeclassandthenusingReplaceTypeCodewithSubclassesonthatnewclass.
Mechanics
Self-encapsulatethetypecodefield.
Pickonetypecodevalue.Createasubclassforthattypecode.Overridethetypecodegettertoreturntheliteraltypecodevalue.
Createselectorlogictomapfromthetypecodeparametertothenewsubclass.
Withdirectinheritance,useReplaceConstructorwithFactoryFunction(332)
andputtheselectorlogicinthefactory.Withindirectinheritance,theselectorlogicmaystayintheconstructor.
Test.
Repeatcreatingthesubclassandaddingtotheselectorlogicforeachtypecodevalue.Testaftereachchange.
Removethetypecodefield.
Test.
UsePushDownMethod(357)andReplaceConditionalwithPolymorphism(271)onanymethodsthatusethetypecodeaccessors.Onceallarereplaced,youcanremovethetypecodeaccessors.
Example
I’llstartwiththisoverusedemployeeexample.
classEmployee…
constructor(name,type){
this.validateType(type);
this._name=name;
this._type=type;
}
validateType(arg){
if(!["engineer","manager","salesman"].includes(arg))
thrownewError(`Employeecannotbeoftype${arg}`);
}
toString(){return`${this._name}(${this._type})`;}
MyfirststepistouseEncapsulateVariable(132)toself-encapsulatethetypecode.
classEmployee…
gettype(){returnthis._type;}
toString(){return`${this._name}(${this.type})`;}
NotethattoStringusesthenewgetterbyremovingtheunderscore.
Ipickonetypecode,theengineer,tostartwith.Iusedirectinheritance,subclassingtheemployeeclassitself.Theemployeesubclassissimple—justoverridingthetypecodegetterwiththeappropriateliteralvalue.
classEngineerextendsEmployee{
gettype(){return"engineer";}
}
AlthoughJavaScriptconstructorscanreturnotherobjects,thingswillgetmessyifItrytoputselectorlogicinthere,sincethatlogicgetsintertwinedwithfieldinitialization.SoIuseReplaceConstructorwithFactoryFunction(332)tocreateanewspaceforit.
functioncreateEmployee(name,type){
returnnewEmployee(name,type);
}
Tousethenewsubclass,Iaddselectorlogicintothefactory.
functioncreateEmployee(name,type){
switch(type){
case"engineer":returnnewEngineer(name,type);
}
returnnewEmployee(name,type);
}
Itesttoensurethatworkedoutcorrectly.But,becauseI’mparanoid,Ithenalterthereturnvalueoftheengineer’soverrideandtestagaintoensurethetestfails.ThatwayIknowthesubclassisbeingused.Icorrectthereturnvalueandcontinuewiththeothercases.Icandothemoneatatime,testingaftereachchange.
classSalesmanextendsEmployee{
gettype(){return"salesman";}
}
classManagerextendsEmployee{
gettype(){return"manager";}
}
functioncreateEmployee(name,type){
switch(type){
case"engineer":returnnewEngineer(name,type);
case"salesman":returnnewSalesman(name,type);
case"manager":returnnewManager(name,type);
}
returnnewEmployee(name,type);
}
OnceI’mdonewiththemall,Icanremovethetypecodefieldandthesuper-classgettingmethod(theonesinthesubclassesremain).
classEmployee…
constructor(name,type){
this.validateType(type);
this._name=name;
this._type=type;
}
gettype(){returnthis._type;}
toString(){return`${this._name}(${this.type})`;}
Aftertestingtoensureallisstillwell,Icanremovethevalidationlogic,sincetheswitchiseffectivelydoingthesamething.
classEmployee…
constructor(name,type){
this.validateType(type);
this._name=name;
}
functioncreateEmployee(name,type){
switch(type){
case"engineer":returnnewEngineer(name,type);
case"salesman":returnnewSalesman(name,type);
case"manager":returnnewManager(name,type);
default:thrownewError(`Employeecannotbeoftype${type}`);
}
returnnewEmployee(name,type);
}
Thetypeargumenttotheconstructorisnowuseless,soitfallsvictimtoChangeFunctionDeclaration(124).
classEmployee…
constructor(name,type){
this._name=name;
}
functioncreateEmployee(name,type){
switch(type){
case"engineer":returnnewEngineer(name,type);
case"salesman":returnnewSalesman(name,type);
case"manager":returnnewManager(name,type);
default:thrownewError(`Employeecannotbeoftype${type}`);
}
}
Istillhavethetypecodeaccessorsonthesubclasses—gettype.I’llusuallywanttoremovethesetoo,butthatmaytakeabitoftimeduetoothermethodsthatdependonthem.I’lluseReplaceConditionalwithPolymorphism(271)andPushDownMethod(357)todealwiththese.Atsomepoint,I’llhavenocodethatusesthetypegetters,soIwillsubjectthemtothetendermerciesofRemoveDeadCode(236).
Example:UsingIndirectInheritance
Let’sgobacktothestartingcase—butthistime,Ialreadyhaveexistingsubclassesforpart-timeandfull-timeemployees,soIcan’tsubclassfromEmployeeforthetypecodes.Anotherreasontonotusedirectinheritanceiskeepingtheabilitytochangethetypeofemployee.
classEmployee…
constructor(name,type){
this.validateType(type);
this._name=name;
this._type=type;
}
validateType(arg){
if(!["engineer","manager","salesman"].includes(arg))
thrownewError(`Employeecannotbeoftype${arg}`);
}
gettype(){returnthis._type;}
settype(arg){this._type=arg;}
getcapitalizedType(){
returnthis._type.charAt(0).toUpperCase()+this._type.substr(1).toLowerCase();
}
toString(){
return`${this._name}(${this.capitalizedType})`;
}
ThistimetoStringisabitmorecomplicated,toallowmetoillustratesomethingshortly.
MyfirststepistouseReplacePrimitivewithObject(172)onthetypecode.
classEmployeeType{
constructor(aString){
this._value=aString;
}
toString(){returnthis._value;}
}
classEmployee…
constructor(name,type){
this.validateType(type);
this._name=name;
this.type=type;
}
validateType(arg){
if(!["engineer","manager","salesman"].includes(arg))
thrownewError(`Employeecannotbeoftype${arg}`);
}
gettypeString(){returnthis._type.toString();}
gettype(){returnthis._type;}
settype(arg){this._type=newEmployeeType(arg);}
getcapitalizedType(){
returnthis.typeString.charAt(0).toUpperCase()
+this.typeString.substr(1).toLowerCase();
}
toString(){
return`${this._name}(${this.capitalizedType})`;
}
IthenapplytheusualmechanicsofReplaceTypeCodewithSubclassestotheemployeetype.
classEmployee…
settype(arg){this._type=Employee.createEmployeeType(arg);}
staticcreateEmployeeType(aString){
switch(aString){
case"engineer":returnnewEngineer();
case"manager":returnnewManager();
case"salesman":returnnewSalesman();
default:thrownewError(`Employeecannotbeoftype${aString}`);
}
}
classEmployeeType{
}
classEngineerextendsEmployeeType{
toString(){return"engineer";}
}
classManagerextendsEmployeeType{
toString(){return"manager";}
}
classSalesmanextendsEmployeeType{
toString(){return"salesman";}
}
IfIwereleavingitatthat,IcouldremovetheemptyEmployeeType.ButIprefertoleaveitthereasitmakesexplicittherelationshipbetweenthevarioussubclasses.It’salsoahandyspotformovingotherbehaviorthere,suchasthecapitalizationlogicItossedintotheexamplespecificallytoillustratethispoint.
classEmployee…
toString(){
return`${this._name}(${this.type.capitalizedName})`;
}
classEmployeeType…
getcapitalizedName(){
returnthis.toString().charAt(0).toUpperCase()
+this.toString().substr(1).toLowerCase();
}
Forthosefamiliarwiththefirsteditionofthebook,thisexampleessentiallysupersedestheReplaceTypeCodewithState/Strategy.InowthinkofthatrefactoringasReplaceTypeCodewithSubclassesusingindirectinheritance,sodidn’tconsideritworthitsownentryinthecatalog.(Ineverlikedthenameanyway.)
RemoveSubclass
formerly:ReplaceSubclasswithFields
inverseof:ReplaceTypeCodewithSubclasses(361)
Motivation
Subclassesareuseful.Theysupportvariationsindatastructureandpolymorphicbehavior.Theyareagoodwaytoprogrambydifference.Butasasoftwaresystemevolves,subclassescanlosetheirvalueasthevariationstheysupportaremovedtootherplacesorremovedaltogether.Sometimes,subclassesareaddedinanticipationoffeaturesthatneverendupbeingbuilt,orendupbeingbuiltinawaythatdoesn’tneedthesubclasses.
Asubclassthatdoestoolittleincursacostinunderstandingthatisnolongerworthwhile.Whenthattimecomes,it’sbesttoremovethesubclass,replacingit
withafieldonitssuperclass.
Mechanics
UseReplaceConstructorwithFactoryFunction(332)onthesubclassconstructor.
Iftheclientsoftheconstructorsuseadatafieldtodecidewhichsubclasstocreate,putthatdecisionlogicintoasuperclassfactorymethod.
Ifanycodetestsagainstthesubclass’stypes,useExtractFunction(106)onthetypetestandMoveFunction(196)tomoveittothesuperclass.Testaftereachchange.
Createafieldtorepresentthesubclasstype.
Changethemethodsthatrefertothesubclasstousethenewtypefield.
Deletethesubclass.
Test.
Often,thisrefactoringisusedonagroupofsubclassesatonce—inwhichcasecarryoutthestepstoencapsulatethem(addfactoryfunction,movetypetests)first,thenindividuallyfoldthemintothesuperclass.
Example
I’llstartwiththisstumpofsubclasses.
classPerson…
constructor(name){
this._name=name;
}
getname(){returnthis._name;}
getgenderCode(){return"X";}
//snip
classMaleextendsPerson{
getgenderCode(){return"M";}
}
classFemaleextendsPerson{
getgenderCode(){return"F";}
}
Ifthat’sallthatasubclassdoes,it’snotreallyworthhaving.ButbeforeIremovethesesubclasses,it’susuallyworthcheckingtoseeifthere’sanysubclass-dependentbehaviorintheclientsthatshouldbemovedinthere.Inthiscase,Idon’tfindanythingworthkeepingthesubclassesfor.
client…
constnumberOfMales=people.filter(p=>pinstanceofMale).length;
WheneverIwanttochangehowIrepresentsomething,Itrytofirstencapsulatethecurrentrepresentationtominimizetheimpactonanyclientcode.Whenitcomestocreatingsubclasses,thewaytoencapsulateistouseReplaceConstructorwithFactoryFunction(332).Inthiscase,there’sacoupleofwaysIcouldmakethefactory.
Themostdirectwayistocreateafactorymethodforeachconstructor.
functioncreatePerson(name){
returnnewPerson(name);
}
functioncreateMale(name){
returnnewMale(name);
}
functioncreateFemale(name){
returnnewFemale(name);
}
Butalthoughthat’sthedirectchoice,objectslikethisareoftenloadedfromasourcethatusesthegendercodesdirectly.
functionloadFromInput(data){
constresult=[];
data.forEach(aRecord=>{
letp;
switch(aRecord.gender){
case'M':p=newMale(aRecord.name);break;
case'F':p=newFemale(aRecord.name);break;
default:p=newPerson(aRecord.name);
}
result.push(p);
});
returnresult;
}
Inthatcase,IfinditbettertouseExtractFunction(106)ontheselectionlogicforwhichclasstocreate,andmakethatthefactoryfunction.
functioncreatePerson(aRecord){
letp;
switch(aRecord.gender){
case'M':p=newMale(aRecord.name);break;
case'F':p=newFemale(aRecord.name);break;
default:p=newPerson(aRecord.name);
}
returnp;
}
functionloadFromInput(data){
constresult=[];
data.forEach(aRecord=>{
result.push(createPerson(aRecord));
});
returnresult;
}
WhileI’mthere,I’llcleanupthosetwofunctions.I’lluseInlineVariable(123)oncreatePerson:
functioncreatePerson(aRecord){
switch(aRecord.gender){
case'M':returnnewMale(aRecord.name);
case'F':returnnewFemale(aRecord.name);
default:returnnewPerson(aRecord.name);
}
}
AndReplaceLoopwithPipeline(230)onloadFromInput:
functionloadFromInput(data){
returndata.map(aRecord=>createPerson(aRecord));
}
Thefactoryencapsulatesthecreationofthesubclasses,butthereisalsotheuseofinstanceof—whichneversmellsgood.IuseExtractFunction(106)onthetypecheck.
client…
constnumberOfMales=people.filter(p=>isMale(p)).length;
functionisMale(aPerson){returnaPersoninstanceofMale;}
ThenIuseMoveFunction(196)tomoveitintoPerson.
classPerson…
getisMale(){returnthisinstanceofMale;}
client…
constnumberOfMales=people.filter(p=>p.isMale).length;
Withthatrefactoringdone,allknowledgeofthesubclassesisnowsafelyencasedwithinthesuperclassandthefactoryfunction.(UsuallyI’mwaryofasuperclassreferringtoasubclass,butthiscodeisn’tgoingtolastuntilmynextcupoftea,soI’mnotgoingworryaboutit.)
Inowaddafieldtorepresentthedifferencebetweenthesubclasses;sinceI’musingacodeloadedfromelsewhere,Imightaswelljustusethat.
classPerson…
constructor(name,genderCode){
this._name=name;
this._genderCode=genderCode||"X";
}
getgenderCode(){returnthis._genderCode;}
Wheninitializingit,Isetittothedefaultcase.(Asasidenote,althoughmostpeoplecanbeclassifiedasmaleorfemale,therearepeoplewhocan’t.It’sacommonmodelingmistaketoforgetthat.)
Ithentakethemalecaseandfolditslogicintothesuperclass.ThisinvolvesmodifyingthefactorytoreturnaPersonandmodifyinganyinstanceofteststousethegendercodefield.
functioncreatePerson(aRecord){
switch(aRecord.gender){
case'M':returnnewPerson(aRecord.name,"M");
case'F':returnnewFemale(aRecord.name);
default:returnnewPerson(aRecord.name);
}
}
classPerson…
getisMale(){return"M"===this._genderCode;}
Itest,removethemalesubclass,testagain,andrepeatforthefemalesubclass.
functioncreatePerson(aRecord){
switch(aRecord.gender){
case'M':returnnewPerson(aRecord.name,"M");
case'F':returnnewPerson(aRecord.name,"F");
default:returnnewPerson(aRecord.name);
}
}
Ifindthelackofsymmetrywiththegendercodetobeannoying.Afuturereaderofthecodewillalwayswonderaboutthislackofsymmetry.SoIprefertochangethecodetomakeitsymmetrical—ifIcandoitwithoutintroducinganyothercomplexity,whichisthecasehere.
functioncreatePerson(aRecord){
switch(aRecord.gender){
case'M':returnnewPerson(aRecord.name,"M");
case'F':returnnewPerson(aRecord.name,"F");
default:returnnewPerson(aRecord.name,"X");
}
}
classPerson…
constructor(name,genderCode){
this._name=name;
this._genderCode=genderCode||"X";
}
ExtractSuperclass
Motivation
IfIseetwoclassesdoingsimilarthings,Icantakeadvantageofthebasic
mechanismofinheritancetopulltheirsimilaritiestogetherintoasuperclass.IcanusePullUpField(351)tomovecommondataintothesuperclass,andPullUpMethod(348)tomovethecommonbehavior.
Manywritersonobjectorientationtreatinheritanceassomethingthatshouldbecarefullyplannedinadvance,basedonsomekindofclassificationstructureinthe“realworld.”Suchclassificationstructurescanbeahinttowardsusinginheritance—butjustasofteninheritanceissomethingIrealizeduringtheevolutionofaprogram,asIfindcommonelementsthatIwanttopulltogether.
AnalternativetoExtractSuperclassisExtractClass(180).Hereyouhave,essentially,achoicebetweenusinginheritanceordelegationasawaytounifyduplicatebehavior.OftenExtractSuperclassisthesimplerapproach,soI’lldothisfirstknowingIcanuseReplaceSuperclasswithDelegate(397)shouldIneedtolater.
Mechanics
Createanemptysuperclass.Maketheoriginalclassesitssubclasses.
Ifneeded,useChangeFunctionDeclaration(124)ontheconstructors.
Test.
Onebyone,usePullUpConstructorBody(353),PullUpMethod(348),andPullUpField(351)tomovecommonelementstothesuperclass.
Examineremainingmethodsonthesubclasses.Seeiftherearecommonparts.Ifso,useExtractFunction(106)followedbyPullUpMethod(348).
Checkclientsoftheoriginalclasses.Consideradjustingthemtousethesuperclassinterface.
Example
I’mponderingthesetwoclasses:
classEmployee{
constructor(name,id,monthlyCost){
this._id=id;
this._name=name;
this._monthlyCost=monthlyCost;
}
getmonthlyCost(){returnthis._monthlyCost;}
getname(){returnthis._name;}
getid(){returnthis._id;}
getannualCost(){
returnthis.monthlyCost*12;
}
}
classDepartment{
constructor(name,staff){
this._name=name;
this._staff=staff;
}
getstaff(){returnthis._staff.slice();}
getname(){returnthis._name;}
gettotalMonthlyCost(){
returnthis.staff
.map(e=>e.monthlyCost)
.reduce((sum,cost)=>sum+cost);
}
getheadCount(){
returnthis.staff.length;
}
gettotalAnnualCost(){
returnthis.totalMonthlyCost*12;
}
}
Theysharesomecommonfunctionality—theirnameandthenotionsofannualandmonthlycosts.Icanmakethiscommonalitymoreexplicitbyextractingacommonsuperclassfromthem.
Ibeginbycreatinganemptysuperclassandlettingthembothextendfromit.
classParty{}
classEmployeeextendsParty{
constructor(name,id,monthlyCost){
super();
this._id=id;
this._name=name;
this._monthlyCost=monthlyCost;
}
//restofclass…
classDepartmentextendsParty{
constructor(name,staff){
super();
this._name=name;
this._staff=staff;
}
//restofclass…
WhendoingExtractSuperclass,Iliketostartwiththedata,whichinJavaScriptinvolvesmanipulatingtheconstructor.SoIstartwithPullUpField(351)topullupthename.
classParty…
constructor(name){
this._name=name;
}
classEmployee…
constructor(name,id,monthlyCost){
super(name);
this._id=id;
this._monthlyCost=monthlyCost;
}
classDepartment…
constructor(name,staff){
super(name);
this._staff=staff;
}
AsIgetdatauptothesuperclass,IcanalsoapplyPullUpMethod(348)onassociatedmethods.First,thename:
classParty…
getname(){returnthis._name;}
classEmployee…
getname(){returnthis._name;}
classDepartment…
getname(){returnthis._name;}
Ihavetwomethodswithsimilarbodies.
classEmployee…
getannualCost(){
returnthis.monthlyCost*12;
}
classDepartment…
gettotalAnnualCost(){
returnthis.totalMonthlyCost*12;
}
Themethodstheyuse,monthlyCostandtotalMonthlyCost,havedifferentnamesanddifferentbodies—butdotheyrepresentthesameintent?Ifso,IshoulduseChangeFunctionDeclaration(124)tounifytheirnames.
classDepartment…
gettotalAnnualCost(){
returnthis.monthlyCost*12;
}
getmonthlyCost(){…}
Ithendoasimilarrenamingtotheannualcosts:
classDepartment…
getannualCost(){
returnthis.monthlyCost*12;
}
IcannowapplyPullUpMethod(348)totheannualcostmethods.
classParty…
getannualCost(){
returnthis.monthlyCost*12;
}
classEmployee…
getannualCost(){
returnthis.monthlyCost*12;
}
classDepartment…
getannualCost(){
returnthis.monthlyCost*12;
}
CollapseHierarchy
Motivation
WhenI’mrefactoringaclasshierarchy,I’moftenpullingandpushingfeaturesaround.Asthehierarchyevolves,Isometimesfindthataclassanditsparentarenolongerdifferentenoughtobeworthkeepingseparate.Atthispoint,I’llmergethemtogether.
Mechanics
Choosewhichonetoremove.
Ichoosebasedonwhichnamemakesmostsenseinthefuture.Ifneithernameisbest,I’llpickonearbitrarily.
UsePullUpField(351),PushDownField(359),PullUpMethod(348),andPullUpField(351)tomovealltheelementsintoasingleclass.
Adjustanyreferencestothevictimtochangethemtotheclassthatwillstay.
Removetheemptyclass.
Test.
ReplaceSubclasswithDelegate
Motivation
IfIhavesomeobjectswhosebehaviorvariesfromcategorytocategory,thenaturalmechanismtoexpressthisisinheritance.Iputallthecommondataandbehaviorinthesuperclass,andleteachsubclassaddandoverridefeaturesasneeded.Object-orientedlanguagesmakethissimpletoimplementandthusa
familiarmechanism.
Butinheritancehasitsdownsides.Mostobviously,it’sacardthatcanonlybeplayedonce.IfIhavemorethanonereasontovarysomething,Icanonlyuseinheritanceforasingleaxisofvariation.So,ifIwanttovarybehaviorofpeoplebytheiragecategoryandbytheirincomelevel,Icaneitherhavesubclassesforyoungandsenior,orforwell-offandpoor—Ican’thaveboth.
Afurtherproblemisthatinheritanceintroducesaverycloserelationshipbetweenclasses.AnychangeIwanttomaketotheparentcaneasilybreakchildren,soIhavetobecarefulandunderstandhowchildrenderivefromthesuperclass.Thisproblemismadeworsewhenthelogicofthetwoclassesresidesindifferentmodulesandislookedafterbydifferentteams.
Delegationhandlesbothoftheseproblems.Icandelegatetomanydifferentclassesfordifferentreasons.Delegationisaregularrelationshipbetweenobjects—soIcanhaveaclearinterfacetoworkwith,whichismuchlesscouplingthansubclassing.It’sthereforecommontorunintotheproblemswithsubclassingandapplyReplaceSubclasswithDelegate.
Thereisapopularprinciple:“Favorobjectcompositionoverclassinheritance”(wherecompositioniseffectivelythesameasdelegation).Manypeopletakethistomean“inheritanceconsideredharmful”andclaimthatweshouldneveruseinheritance.Iuseinheritancefrequently,partlybecauseIalwaysknowIcanuseReplaceSubclasswithDelegateshouldIneedtochangeitlater.Inheritanceisavaluablemechanismthatdoesthejobmostofthetimewithoutproblems.SoIreachforitfirst,andmoveontodelegationwhenitstartstorubbadly.Thisusageisactuallyconsistentwiththeprinciple—whichcomesfromtheGangofFourbook[bib-gof]thatexplainshowinheritanceandcompositionworktogether.Theprinciplewasareactiontotheoveruseofinheritance.
ThosewhoarefamiliarwiththeGangofFourbookmayfindithelpfultothinkofthisrefactoringasreplacingsubclasseswiththeStateorStrategypatterns.Bothofthesepatternsarestructurallythesame,relyingonthehostdelegatingtoaseparatehierarchy.NotallcasesofReplaceSubclasswithDelegateinvolveaninheritancehierarchyforthedelegate(asthefirstexamplebelowillustrates),butsettingupahierarchyforstatesorstrategiesisoftenuseful.
Mechanics
Iftherearemanycallersfortheconstructors,applyReplaceConstructorwithFactoryFunction(332).
Createanemptyclassforthedelegate.Itsconstructorshouldtakeanysubclass-specificdataaswellas,usually,aback-referencetothesuperclass.
Addafieldtothesuperclasstoholdthedelegate.
Modifythecreationofthesubclasssothatitinitializesthedelegatefieldwithaninstanceofthedelegate.
Thiscanbedoneinthefactoryfunction,orintheconstructoriftheconstructorcanreliablytellwhethertocreatethecorrectdelegate.
Chooseasubclassmethodtomovetothedelegateclass.
UseMoveFunction(196)tomoveittothedelegateclass.Don’tremovethesource’sdelegatingcode.
Ifthemethodneedselementsthatshouldmovetothedelegate,movethem.Ifitneedselementsthatshouldstayinthesuperclass,addafieldtothedelegatethatreferstothesuperclass.
Ifthesourcemethodhascallersoutsidetheclass,movethesource’sdelegatingcodefromthesubclasstothesuperclass,guardingitwithacheckforthepresenceofthedelegate.Ifnot,applyRemoveDeadCode(236).
Ifthere’smorethanonesubclass,andyoustartduplicatingcodewithinthem,useExtractSuperclass(373).Inthiscase,anydelegatingmethodsonthesourcesuper-classnolongerneedaguardifthedefaultbehaviorismovedtothedelegatesuperclass.
Test.
Repeatuntilallthemethodsofthesubclassaremoved.
Findallcallersofthesubclasses’sconstructorandchangethemtousethesuperclassconstructor.
Test.
UseRemoveDeadCode(236)onthesubclass.
Example
Ihaveaclassthatmakesabookingforashow.
classBooking…
constructor(show,date){
this._show=show;
this._date=date;
}
Thereisasubclassforpremiumbookingthattakesintoaccountsvariousextrasthatareavailable.
classPremiumBookingextendsBooking…
constructor(show,date,extras){
super(show,date);
this._extras=extras;
}
Therearequiteafewchangesthatthepremiumbookingmakestowhatitinheritsfromthesuperclass.Asistypicalwiththiskindofprogramming-by-difference,insomecasesthesubclassoverridesmethodsonthesuperclass,inothersitaddsnewmethodsthatareonlyrelevantforthesubclass.Iwon’tgointoallofthem,butIwillpickoutafewinterestingcases.
First,thereisasimpleoverride.Regularbookingsofferatalkbackaftertheshow,butonlyonnon-peakdays.
classBooking…
gethasTalkback(){
returnthis._show.hasOwnProperty('talkback')&&!this.isPeakDay;
}
Premiumbookingsoverridethistooffertalkbacksonalldays.
classPremiumBooking…
gethasTalkback(){
returnthis._show.hasOwnProperty('talkback');
}
Determiningthepriceisasimilaroverride,withatwistthatthepremiummethodcallsthesuperclassmethod.
classBooking…
getbasePrice(){
letresult=this._show.price;
if(this.isPeakDay)result+=Math.round(result*0.15);
returnresult;
}
classPremiumBooking…
getbasePrice(){
returnMath.round(super.basePrice+this._extras.premiumFee);
}
Thelastexampleiswherethepremiumbookingoffersabehaviorthatisn’tpresentonthesuperclass.
classPremiumBooking…
gethasDinner(){
returnthis._extras.hasOwnProperty('dinner')&&!this.isPeakDay;
}
Inheritanceworkswellforthisexample.Icanunderstandthebaseclasswithouthavingtounderstandthesubclass.Thesubclassisdefinedjustbysayinghowitdiffersfromthebasecase—bothreducingduplicationandclearlycommunicatingwhatarethedifferencesit’sintroducing.
Actually,itisn’tquiteasperfectasthepreviousparagraphimplies.Therearethingsinthesuperclassstructurethatonlymakesenseduetothesubclass—suchasmethodsthathavebeenfactoredinsuchawayastomakeiteasiertooverridejusttherightkindsofbehavior.SoalthoughmostofthetimeIcanmodifythebaseclasswithouthavingtounderstandsubclasses,thereareoccasionswheresuchmindfulignoranceofthesubclasseswillleadmetobreakingasubclassbymodifyingthesuperclass.However,iftheseoccasionsarenottoocommon,theinheritancepaysoff—providedIhavegoodteststodetectasubclassbreakage.
SowhywouldIwanttochangesuchahappysituationbyusingReplaceSubclasswithDelegate?Inheritanceisatoolthatcanonlybeusedonce—soifIhaveanotherreasontouseinheritance,andIthinkitwillbenefitmemorethanthepremiumbookingsubclass,I’llneedtohandlepremiumbookingsadifferentway.Also,Imayneedtochangefromthedefaultbookingtothepremiumbookingdynamically—i.e.supportamethodlikeaBooking.bePremium().Insomecases,Icanavoidthisbycreatingawholenewobject(acommonexampleiswhereanHTTPrequestloadsnewdatafromtheserver).Butsometimes,Ineedtomodifyadatastructureandnotrebuilditfromscratch,anditisdifficulttojustreplaceasinglebookingthat’sreferredtofrommanydifferentplaces.Insuchsituations,itcanbeusefultoallowabookingtoswitchfromdefaulttopremiumandbackagain.
Whentheseneedscropup,IneedtoapplyReplaceSubclasswithDelegate.Ihaveclientscalltheconstructorsofthetwoclassestomakethebookings:
bookingclient
aBooking=newBooking(show,date);
premiumclient
aBooking=newPremiumBooking(show,date,extras);
Removingsubclasseswillalterallofthis,soIliketoencapsulatetheconstructorcallswithReplaceConstructorwithFactoryFunction(332)
toplevel…
functioncreateBooking(show,date){
returnnewBooking(show,date);
}
functioncreatePremiumBooking(show,date,extras){
returnnewPremiumBooking(show,date,extras);
}
bookingclient
aBooking=createBooking(show,date);
premiumclient
aBooking=createPremiumBooking(show,date,extras);
Inowmakethenewdelegateclass.Itsconstructorparametersarethoseparametersthatareonlyusedinthesubclass,togetherwithaback-referencetothebookingobject.I’llneedthisbecauseseveralsubclassmethodsrequireaccesstodatastoredinthesuperclass.Inheritancemakesthiseasytodo,butwithadelegateIneedaback-reference.
classPremiumBookingDelegate…
constructor(hostBooking,extras){
this._host=hostBooking;
this._extras=extras;
}
Inowconnectthenewdelegatetothebookingobject.Idothisbymodifyingthefactoryfunctionforpremiumbookings.
toplevel…
functioncreatePremiumBooking(show,date,extras){
constresult=newPremiumBooking(show,date,extras);
result._bePremium(extras);
returnresult;
}
classBooking…
_bePremium(extras){
this._premiumDelegate=newPremiumBookingDelegate(this,extras);
}
Iusealeadingunderscoreon_bePremiumtoindicatethatitshouldn’tbepartofthepublicinterfaceforBooking.Ofcourse,ifthepointofdoingthisrefactoringistoallowabookingtomutatetopremium,itcanbeapublicmethod.
Alternatively,IcandoalltheconnectionsintheconstructorforBooking.Inordertodothat,Ineedsomewaytosignaltotheconstructorthatwehaveapremiumbooking.Thatcouldbeanextraparameter,orjusttheuseofextrasifIcanbesurethatitisalwayspresentwhenusedwithapremiumbooking.Here,Iprefertheexplicitnessofdoingthisthroughthefactoryfunction.
Withthestructuressetup,it’stimetostartmovingthebehavior.Thefirstcase
I’llconsideristhesimpleoverrideofhasTalkback.Here’stheexistingcode:
classBooking…
gethasTalkback(){
returnthis._show.hasOwnProperty('talkback')&&!this.isPeakDay;
}
classPremiumBooking…
gethasTalkback(){
returnthis._show.hasOwnProperty('talkback');
}
IuseMoveFunction(196)tomovethesubclassmethodtothedelegate.Tomakeitfititshome,Irouteanyaccesstosuperclassdatawithacallto_host.
classPremiumBookingDelegate…
gethasTalkback(){
returnthis._host._show.hasOwnProperty('talkback');
}
classPremiumBooking…
gethasTalkback(){
returnthis._premiumDelegate.hasTalkback;
}
Itesttoensureeverythingisworking,thendeletethesubclassmethod:
classPremiumBooking…
gethasTalkback(){
returnthis._premiumDelegate.hasTalkback;
}
Irunthetestsatthispoint,expectingsometofail.
NowIfinishthemovebyaddingdispatchlogictothesuperclassmethodtousethedelegateifitispresent.
classBooking…
gethasTalkback(){
return(this._premiumDelegate)
?this._premiumDelegate.hasTalkback
:this._show.hasOwnProperty('talkback')&&!this.isPeakDay;
}
ThenextcaseI’lllookatisthebaseprice.
classBooking…
getbasePrice(){
letresult=this._show.price;
if(this.isPeakDay)result+=Math.round(result*0.15);
returnresult;
}
classPremiumBooking…
getbasePrice(){
returnMath.round(super.basePrice+this._extras.premiumFee);
}
Thisisalmostthesame,butthereisawrinkleintheformofthepeskycallonsuper(whichisprettycommoninthesekindsofsubclassextensioncases).WhenImovethesubclasscodetothedelegate,I’llneedtocalltheparentcase—butIcan’tjustcallthis._host._basePricewithoutgettingintoanendlessrecursion.
Ihaveacoupleofoptionshere.OneistoapplyExtractFunction(106)onthebasecalculationtoallowmetoseparatethedispatchlogicfrompricecalculation.(Therestofthemoveisasbefore.)
classBooking…
getbasePrice(){
return(this._premiumDelegate)
?this._premiumDelegate.basePrice
:this._privateBasePrice;
}
get_privateBasePrice(){
letresult=this._show.price;
if(this.isPeakDay)result+=Math.round(result*0.15);
returnresult;
}
classPremiumBookingDelegate…
getbasePrice(){
returnMath.round(this._host._privateBasePrice+this._extras.premiumFee);
}
Alternatively,Icanrecastthedelegate’smethodasanextensionofthebasemethod.
classBooking…
getbasePrice(){
letresult=this._show.price;
if(this.isPeakDay)result+=Math.round(result*0.15);
return(this._premiumDelegate)
?this._premiumDelegate.extendBasePrice(result)
:result;
}
classPremiumBookingDelegate…
extendBasePrice(base){
returnMath.round(base+this._extras.premiumFee);
}
Bothworkreasonablyhere;Ihaveaslightpreferenceforthelatterasit’sabitsmaller.
Thelastcaseisamethodthatonlyexistsonthesubclass.
classPremiumBooking…
gethasDinner(){
returnthis._extras.hasOwnProperty('dinner')&&!this.isPeakDay;
}
Imoveitfromthesubclasstothedelegate:
classPremiumBookingDelegate…
gethasDinner(){
returnthis._extras.hasOwnProperty('dinner')&&!this._host.isPeakDay;
}
IthenadddispatchlogictoBooking:
classBooking…
gethasDinner(){
return(this._premiumDelegate)
?this._premiumDelegate.hasDinner
:undefined;
}
InJavaScript,accessingapropertyonanobjectwhereitisn’tdefinedreturnsundefined,soIdothathere.(Althoughmyeveryinstinctistohaveitraiseanerror,whichwouldbethecaseinotherobject-orienteddynamiclanguagesI’musedto.)
OnceI’vemovedallthebehavioroutofthesubclass,Icanchangethefactorymethodtoreturnthesuperclass—and,onceI’verunteststoensurealliswell,deletethesubclass.
toplevel…
functioncreatePremiumBooking(show,date,extras){
constresult=newPremiumBooking(show,date,extras);
result._bePremium(extras);
returnresult;
}
classPremiumBookingextendsBooking…
ThisisoneofthoserefactoringswhereIdon’tfeelthatrefactoringaloneimprovesthecode.Inheritancehandlesthissituationverywell,whereasusingdelegationinvolvesaddingdispatchlogic,two-wayreferences,andthusextracomplexity.Therefactoringmaystillbeworthwhile,sincetheadvantageofamutablepremiumstatus,oraneedtouseinheritanceforotherpurposes,mayoutweighthedisadvantageoflosinginheritance.
Example:ReplacingaHierarchy
ThepreviousexampleshowedusingReplaceSubclasswithDelegateonasinglesubclass,butIcandothesamethingwithanentirehierarchy.
functioncreateBird(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallow(data);
case'AfricanSwallow':
returnnewAfricanSwallow(data);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrot(data);
default:
returnnewBird(data);
}
}
classBird{
constructor(data){
this._name=data.name;
this._plumage=data.plumage;
}
getname(){returnthis._name;}
getplumage(){
returnthis._plumage||"average";
}
getairSpeedVelocity(){returnnull;}
}
classEuropeanSwallowextendsBird{
getairSpeedVelocity(){return35;}
}
classAfricanSwallowextendsBird{
constructor(data){
super(data);
this._numberOfCoconuts=data.numberOfCoconuts;
}
getairSpeedVelocity(){
return40-2*this._numberOfCoconuts;
}
}
classNorwegianBlueParrotextendsBird{
constructor(data){
super(data);
this._voltage=data.voltage;
this._isNailed=data.isNailed;
}
getplumage(){
if(this._voltage>100)return"scorched";
elsereturnthis._plumage||"beautiful";
}
getairSpeedVelocity(){
return(this._isNailed)?0:10+this._voltage/10;
}
}
Thesystemwillshortlybemakingabigdifferencebetweenbirdstaggedinthewildandthosetaggedincaptivity.ThatdifferencecouldbemodeledastwosubclassesforBird:WildBirdandCaptiveBird.However,Icanonlyuseinheritanceonce,soifIwanttousesubclassesforwildversuscaptive,I’llhavetoremovethemforthespecies.
Whenseveralsubclassesareinvolved,I’lltacklethemoneatatime,startingwithasimpleone—inthiscase,EuropeanSwallow.Icreateanemptydelegateclassforthedelegate.
classEuropeanSwallowDelegate{
}
Idon’tputinanydataorback-referenceparametersyet.Forthisexample,I’llintroducethemasIneedthem.
Ineedtodecidewheretohandletheinitializationofthedelegatefield.Here,sinceIhavealltheinformationinthesingledataargumenttotheconstructor,Idecidetodoitintheconstructor.SincethereareseveraldelegatesIcouldadd,Imakeafunctiontoselectthecorrectonebasedonthetypecodeinthedocument.
classBird…
constructor(data){
this._name=data.name;
this._plumage=data.plumage;
this._speciesDelegate=this.selectSpeciesDelegate(data);
}
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate();
default:returnnull;
}
}
NowIhavethestructuresetup,IcanapplyMoveFunction(196)totheEuropeanswallow’sairspeedvelocity.
classEuropeanSwallowDelegate…
getairSpeedVelocity(){return35;}
classEuropeanSwallow…
getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity
IchangeairSpeedVelocityonthesuperclasstocalladelegate,ifpresent.
classBird…
getairSpeedVelocity(){
returnthis._speciesDelegate?this._speciesDelegate.airSpeedVelocity:
}
Iremovethesubclass.
classEuropeanSwallowextendsBird{
getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity;}
}
toplevel…
functioncreateBird(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallow(data);
case'AfricanSwallow':
returnnewAfricanSwallow(data);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrot(data);
default:
returnnewBird(data);
}
}
NextI’lltackletheAfricanswallow.Icreateaclass;thistime,theconstructorneedsthedatadocument.
classAfricanSwallowDelegate…
constructor(data){
this._numberOfCoconuts=data.numberOfCoconuts;
}
classBird…
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate();
case'AfricanSwallow':
returnnewAfricanSwallowDelegate(data);
default:returnnull;
}
}
IuseMoveFunction(196)onairSpeedVelocity.
classAfricanSwallowDelegate…
getairSpeedVelocity(){
return40-2*this._numberOfCoconuts;
}
classAfricanSwallow…
getairSpeedVelocity(){
returnthis._speciesDelegate.airSpeedVelocity;
}
IcannowremovetheAfricanswallowsubclass.
classAfricanSwallowextendsBird{
//allofthebody…
}
functioncreateBird(data){
switch(data.type){
case'AfricanSwallow':
returnnewAfricanSwallow(data);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrot(data);
default:
returnnewBird(data);
}
}
NowfortheNorwegianblue.Creatingtheclassandmovingtheairspeedvelocityusesthesamestepsasbefore,soI’lljustshowtheresult.
classBird…
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate();
case'AfricanSwallow':
returnnewAfricanSwallowDelegate(data);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrotDelegate(data);
default:returnnull;
}
}
classNorwegianBlueParrotDelegate…
constructor(data){
this._voltage=data.voltage;
this._isNailed=data.isNailed;
}
getairSpeedVelocity(){
return(this._isNailed)?0:10+this._voltage/10;
}
Allwellandgood,buttheNorwegianblueoverridestheplumageproperty,whichIdidn’thavetodealwithfortheothercases.TheinitialMoveFunction(196)issimpleenough,albeitwiththeneedtomodifytheconstructortoputinaback-referencetothebird.
classNorwegianBlueParrot…
getplumage(){
returnthis._speciesDelegate.plumage;
}
classNorwegianBlueParrotDelegate…
getplumage(){
if(this._voltage>100)return"scorched";
elsereturnthis._bird._plumage||"beautiful";
}
constructor(data,bird){
this._bird=bird;
this._voltage=data.voltage;
this._isNailed=data.isNailed;
}
classBird…
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate();
case'AfricanSwallow':
returnnewAfricanSwallowDelegate(data);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrotDelegate(data,this);
default:returnnull;
}
}
Thetrickystepishowtoremovethesubclassmethodforplumage.IfIdo
classBird…
getplumage(){
if(this._speciesDelegate)
returnthis._speciesDelegate.plumage;
else
returnthis._plumage||"average";
}
ThenI’llgetabunchoferrorsbecausethereisnoplumagepropertyontheotherspecies’delegateclasses.
Icoulduseamorepreciseconditional:
classBird…
getplumage(){
if(this._speciesDelegateinstanceofNorwegianBlueParrotDelegate)
returnthis._speciesDelegate.plumage;
else
returnthis._plumage||"average";
}
ButIhopethatsmellsasmuchofdecomposingparrottoyouasitdoestome.It’salmostneveragoodideatouseanexplicitclasschecklikethis.
Anotheroptionistoimplementthedefaultcaseontheotherdelegates.
classBird…
getplumage(){
if(this._speciesDelegate)
returnthis._speciesDelegate.plumage;
else
returnthis._plumage||"average";
}
classEuropeanSwallowDelegate…
getplumage(){
returnthis._bird._plumage||"average";
}
classAfricanSwallowDelegate…
getplumage(){
returnthis._bird._plumage||"average";
}
Butthisduplicatesthedefaultmethodforplumage.Andifthat’snotbadenough,Ialsogetsomebonusduplicationintheconstructorstoassigntheback-reference.
Thesolutiontotheduplicationis,naturally,inheritance—IapplyExtractSuperclass(373)tothespeciesdelegates:
classSpeciesDelegate{
constructor(data,bird){
this._bird=bird;
}
getplumage(){
returnthis._bird._plumage||"average";
}
classEuropeanSwallowDelegateextendsSpeciesDelegate{
classAfricanSwallowDelegateextendsSpeciesDelegate{
constructor(data,bird){
super(data,bird);
this._numberOfCoconuts=data.numberOfCoconuts;
}
classNorwegianBlueParrotDelegateextendsSpeciesDelegate{
constructor(data,bird){
super(data,bird);
this._voltage=data.voltage;
this._isNailed=data.isNailed;
}
Indeed,nowIhaveasuperclass,IcanmoveanydefaultbehaviorfromBirdtoSpeciesDelegatebyensuringthere’salwayssomethinginthespeciesDelegatefield.
classBird…
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate(data,this);
case'AfricanSwallow':
returnnewAfricanSwallowDelegate(data,this);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrotDelegate(data,this);
default:returnnewSpeciesDelegate(data,this);
}
}
//restofbird'scode…
getplumage(){returnthis._speciesDelegate.plumage;}
getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity
classSpeciesDelegate…
getairSpeedVelocity(){returnnull;}
Ilikethis,asitsimplifiesthedelegatingmethodsonBird.Icaneasilyseewhichbehaviorisdelegatedtothespeciesdelegateandwhichstaysbehind.
Here’sthefinalstateoftheseclasses:
functioncreateBird(data){
returnnewBird(data);
}
classBird{
constructor(data){
this._name=data.name;
this._plumage=data.plumage;
this._speciesDelegate=this.selectSpeciesDelegate(data);
}
getname(){returnthis._name;}
getplumage(){returnthis._speciesDelegate.plumage;}
getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity;}
selectSpeciesDelegate(data){
switch(data.type){
case'EuropeanSwallow':
returnnewEuropeanSwallowDelegate(data,this);
case'AfricanSwallow':
returnnewAfricanSwallowDelegate(data,this);
case'NorweigianBlueParrot':
returnnewNorwegianBlueParrotDelegate(data,this);
default:returnnewSpeciesDelegate(data,this);
}
}
//restofbird'scode…
}
classSpeciesDelegate{
constructor(data,bird){
this._bird=bird;
}
getplumage(){
returnthis._bird._plumage||"average";
}
getairSpeedVelocity(){returnnull;}
}
classEuropeanSwallowDelegateextendsSpeciesDelegate{
getairSpeedVelocity(){return35;}
}
classAfricanSwallowDelegateextendsSpeciesDelegate{
constructor(data,bird){
super(data,bird);
this._numberOfCoconuts=data.numberOfCoconuts;
}
getairSpeedVelocity(){
return40-2*this._numberOfCoconuts;
}
}
classNorwegianBlueParrotDelegateextendsSpeciesDelegate{
constructor(data,bird){
super(data,bird);
this._voltage=data.voltage;
this._isNailed=data.isNailed;
}
getairSpeedVelocity(){
return(this._isNailed)?0:10+this._voltage/10;
}
getplumage(){
if(this._voltage>100)return"scorched";
elsereturnthis._bird._plumage||"beautiful";
}
}
Thisexamplereplacestheoriginalsubclasseswithadelegate,butthereisstillaverysimilarinheritancestructureinSpeciesDelegate.HaveIgainedanythingfromthisrefactoring,otherthanfreeingupinheritanceonBird?Thespeciesinheritanceisnowmoretightlyscoped,coveringjustthedataandfunctionsthatvaryduetothespecies.Anycodethat’sthesameforallspeciesremainsonBirdanditsfuturesubclasses.
Icouldapplythesameideaofcreatingasuperclassdelegatetothebookingexampleearlier.ThiswouldallowmetoreplacethosemethodsonBookingthathavedispatchlogicwithsimplecallstothedelegateandlettingitsinheritancesortoutthedispatch.However,it’snearlydinner-time,soI’llleavethatasanexerciseforthereader.
Theseexamplesillustratethatthephrase“Favorobjectcompositionoverclassinheritance”mightbetterbesaidas“Favorajudiciousmixtureofcompositionandinheritanceovereitheralone”—butIfearthatisnotascatchy.
ReplaceSuperclasswithDelegate
formerly:ReplaceInheritancewithDelegation
Motivation
Inobject-orientedprograms,inheritanceisapowerfulandeasilyavailablewaytoreuseexistingfunctionality.Iinheritfromsomeexistingclass,thenoverrideandaddadditionalfeatures.Butsubclassingcanbedoneinawaythatleadstoconfusionandcomplication.
Oneoftheclassicexamplesofmis-inheritancefromtheearlydaysofobjectswasmakingastackbeasubclassoflist.Theideathatledtothiswasreusingoflist’sdatastorageandoperationstomanipulateit.Whileit’sgoodtoreuse,thisinheritancehadaproblem:Alltheoperationsofthelistwerepresentontheinterfaceofthestack,althoughmostofthemwerenotapplicabletoastack.Abetterapproachistomakethelistintoafieldofthestackanddelegatethenecessaryoperationstoit.
ThisisanexampleofonereasontouseReplaceSuperclasswithDelegate—if
functionsofthesuperclassdon’tmakesenseonthesubclass,that’sasignthatIshouldn’tbeusinginheritancetousethesuperclass’sfunctionality.
Aswellasusingallthefunctionsofthesuperclass,itshouldalsobetruethateveryinstanceofthesubclassisaninstanceofthesuperclassandavalidobjectinallcaseswherewe’reusingthesuperclass.IfIhaveacarmodelclass,withthingslikenameandenginesize,ImightthinkIcouldreusethesefeaturestorepresentaphysicalcar,addingfunctionsforVINnumberandmanufacturingdate.Thisisacommon,andoftensubtle,modelingmistakewhichI’vecalledthetype-instancehomonym(https://martinfowler.com/bliki/TypeInstanceHomonym.html).
Thesearebothexamplesofproblemsleadingtoconfusionanderrors—whichcanbeeasilyavoidedbyreplacinginheritancewithdelegationtoaseparateobject.Usingdelegationmakesitclearthatitisaseparatething—onewhereonlysomeofthefunctionscarryover.
Evenincaseswherethesubclassisreasonablemodeling,IuseReplaceSuper-classwithDelegatebecausetherelationshipbetweenasub-andsuperclassishighlycoupled,withthesubclasseasilybrokenbychangesinthesuperclass.ThedownsideisthatIneedtowriteaforwardingfunctionforanyfunctionthatisthesameinthehostandinthedelegate—but,fortunately,eventhoughsuchforwardingfunctionsareboringtowrite,theyaretoosimpletogetwrong.
Asaconsequenceofallthis,somepeopleadviseavoidinginheritanceentirely—butIdon’tagreewiththat.Providedtheappropriatesemanticconditionsapply(everymethodonthesupertypeappliestothesubtype,everyinstanceofthesubtypeisaninstanceofthesupertype),inheritanceisasimpleandeffectivemechanism.IcaneasilyapplyReplaceSuperclasswithDelegateshouldthesituationchangeandinheritanceisnolongerthebestoption.Somyadviceisto(mostly)useinheritancefirst,andapplyReplaceSuperclasswithDelegatewhen(andif)itbecomesaproblem.
Mechanics
Createafieldinthesubclassthatreferstothesuperclassobject.Initializethisdelegatereferencetoanewinstance.
Foreachelementofthesuperclass,createaforwardingfunctioninthesubclass
thatforwardstothedelegatereference.Testafterforwardingeachconsistentgroup.
Mostofthetimeyoucantestaftereachfunctionthat’sforwarded,but,forexample,get/setpairscanonlybetestedoncebothhavebeenmoved.
Whenallsuperclasselementshavebeenoverriddenwithforwarders,removetheinheritancelink.
Example
Irecentlywasconsultingforanoldtown’slibraryofancientscrolls.Theykeepdetailsoftheirscrollsinacatalog.EachscrollhasanIDnumberandrecordsitstitleandlistoftags.
classCatalogItem…
constructor(id,title,tags){
this._id=id;
this._title=title;
this._tags=tags;
}
getid(){returnthis._id;}
gettitle(){returnthis._title;}
hasTag(arg){returnthis._tags.includes(arg);}
Oneofthethingsthatscrollsneedisregularcleaning.Thecodeforthatusesthecatalogitemandextendsitwiththedataitneedsforcleaning.
classScrollextendsCatalogItem…
constructor(id,title,tags,dateLastCleaned){
super(id,title,tags);
this._lastCleaned=dateLastCleaned;
}
needsCleaning(targetDate){
constthreshold=this.hasTag("revered")?700:1500;
returnthis.daysSinceLastCleaning(targetDate)>threshold;
}
daysSinceLastCleaning(targetDate){
returnthis._lastCleaned.until(targetDate,ChronoUnit.DAYS);
}
Thisisanexampleofacommonmodelingerror.Thereisadifferencebetweenthephysicalscrollandthecatalogitem.Thescrolldescribingthetreatmentforthegreyscalediseasemayhaveseveralcopies,butbejustoneiteminthecatalog.
Itmanysituations,Icangetawaywithanerrorlikethis.Icanthinkofthetitleandtagsascopiesofdatainthecatalog.Shouldthisdataneverchange,Icangetawaywiththisrepresentation.ButifIneedtoupdateeither,Imustbecarefultoensurethatallcopiesofthesamecatalogitemareupdatedcorrectly.
Evenwithoutthisissue,I’dstillwanttochangetherelationship.Usingcatalogitemasasuperclasstoscrollislikelytoconfuseprogrammersinthefuture,andisthusapoormodeltoworkwith.
Ibeginbycreatingapropertyinscrollthatreferstothecatalogitem,initializingitwithanewinstance.
classScrollextendsCatalogItem…
constructor(id,title,tags,dateLastCleaned){
super(id,title,tags);
this._catalogItem=newCatalogItem(id,title,tags);
this._lastCleaned=dateLastCleaned;
}
IcreateforwardingmethodsforeachelementofthesuperclassthatIuseonthesubclass.
classScroll…
getid(){returnthis._catalogItem.id;}
gettitle(){returnthis._catalogItem.title;}
hasTag(aString){returnthis._catalogItem.hasTag(aString);}
Iremovetheinheritancelinktothecatalogitem.
classScrollextendsCatalogItem{
constructor(id,title,tags,dateLastCleaned){
super(id,title,tags);
this._catalogItem=newCatalogItem(id,title,tags);
this._lastCleaned=dateLastCleaned;
}
BreakingtheinheritancelinkfinishesthebasicReplaceSuperclasswithDelegaterefactoring,butthereissomethingmoreIneedtodointhiscase.
Therefactoringshiftstheroleofthecatalogitemtothatofacomponentofscroll;eachscrollcontainsauniqueinstanceofacatalogitem.InmanycaseswhereIdothisrefactoring,thisisenough.However,inthissituationabettermodelistolinkthegreyscalecatalogitemtothesixscrollsinthelibrarythatarecopiesofthatwriting.Doingthisis,essentially,ChangeValuetoReference(256).
There’saproblemthatIhavetofix,however,beforeIuseChangeValuetoReference(256).Intheoriginalinheritancestructure,thescrollusedthecatalogitem’sIDfieldtostoreitsID.ButifItreatthecatalogitemasareference,itneedstousethatIDforthecatalogitemIDratherthanthescrollID.ThismeansIneedtocreateanIDfieldonscrollandusethatinsteadofoneincatalogitem.It’sasort-ofmove,sort-ofsplit.
classScroll…
constructor(id,title,tags,dateLastCleaned){
this._id=id;
this._catalogItem=newCatalogItem(null,title,tags);
this._lastCleaned=dateLastCleaned;
}
getid(){returnthis._id;}
CreatingacatalogitemwithanullIDwouldusuallyraiseredflagsandcausealarmstosound.Butthat’sjusttemporarywhileIgetthingsintoshape.OnceI’vedonethat,thescrollswillrefertoasharedcatalogitemwithitsproperID.
Currentlythescrollsareloadedaspartofaloadroutine.
loadroutine…
constscrolls=aDocument
.map(record=>newScroll(record.id,
record.catalogData.title,
record.catalogData.tags,
LocalDate.parse(record.lastCleaned)));
ThefirststepinChangeValuetoReference(256)isfindingorcreatinga
repository.IfindthereisarepositorythatIcaneasilyimportintotheloadroutine.TherepositorysuppliescatalogitemsindexedbyanID.MynexttaskistoseehowtogetthatIDintotheconstructorofthescroll.Fortunately,it’spresentintheinputdataandwasbeingignoredasitwasn’tusefulwhenusinginheritance.Withthatsortedout,IcannowuseChangeFunctionDeclaration(124)toaddboththecatalogandthecatalogitem’sIDtotheconstructorparameters.
loadroutine…
constscrolls=aDocument
.map(record=>newScroll(record.id,
record.catalogData.title,
record.catalogData.tags,
LocalDate.parse(record.lastCleaned),
record.catalogData.id,
catalog));
classScroll…
constructor(id,title,tags,dateLastCleaned,catalogID,catalog){
this._id=id;
this._catalogItem=newCatalogItem(null,title,tags);
this._lastCleaned=dateLastCleaned;
}
InowmodifytheconstructortousethecatalogIDtolookupthecatalogitemanduseitinsteadofcreatinganewone.
classScroll…
constructor(id,title,tags,dateLastCleaned,catalogID,catalog){
this._id=id;
this._catalogItem=catalog.get(catalogID);
this._lastCleaned=dateLastCleaned;
}
Inolongerneedthetitleandtagspassedintotheconstructor,soIuseChangeFunctionDeclaration(124)toremovethem.
loadroutine…
constscrolls=aDocument
.map(record=>newScroll(record.id,
record.catalogData.title,
record.catalogData.tags,
LocalDate.parse(record.lastCleaned),
record.catalogData.id,
catalog));
classScroll…
constructor(id,title,tags,dateLastCleaned,catalogID,catalog){
this._id=id;
this._catalogItem=catalog.get(catalogID);
this._lastCleaned=dateLastCleaned;
}
Bibliography[bib-beck-xpe]ISBN0321278658.
[bib-gof]ISBN0201634988.
[bib-refactoring-book]https://martinfowler.com/books/refactoring.html.
[bib-opdyke]TODO:fix.
[bib-beck-sbpp]ISBN013476904X.
[bib-xp]ISBN0321278658.
[bib-agile]https://martinfowler.com/articles/newMethodology.html.
[bib-tdd]https://martinfowler.com/bliki/TestDrivenDevelopment.html.
[bib-refact-doc]https://martinfowler.com/articles/refactoring-document-load.html.
[bib-forsgren]ISBN1942788339.
[bib-feathers-welc]ISBN0131177052.
[bib-refact-db]https://martinfowler.com/books/refactoringDatabases.html.
[bib-evo-db]https://martinfowler.com/articles/evodb.html.
[bib-evo-arch]ISBN1491986360.
[bib-yagni]https://martinfowler.com/bliki/Yagni.html.
[bib-wake-workbook]ISBN0321109295.
[bib-r2p]https://martinfowler.com/books/r2p.html.
[bib-loop-pipe-article]https://martinfowler.com/articles/refactoring-
pipelines.html.
[bib-refact-html]https://martinfowler.com/books/refactoringHtml.html.
[bib-refact-ruby]https://martinfowler.com/books/refactoringRubyEd.html.
[bib-ref.com]https://refactoring.com.
[bib-bazuzi-safe]http://jay.bazuzi.com/Safely-extract-a-method-in-any-C++-code/.
[bib-parnas]https://dl.acm.org/citation.cfm?id=361623.
[bib-cqs]http://martinfowler.com/bliki/CommandQuerySeparation.html.
[bib-coll-pipe]https://martinfowler.com/articles/collection-pipeline/.
[bib-humble-object]http://xunitpatterns.com/Humble%20Object.html.
[bib-list-and-hash]https://www.martinfowler.com/bliki/ListAndHash.html.
[bib-observer]ISBN0201634988.
[bib-overloaded-getter-setter]http://martinfowler.com/bliki/OverloadedGetterSetter.html.
[bib-pres-domain-sep]https://martinfowler.com/bliki/PresentationDomainSeparation.html.
[bib-range-pattern]http://martinfowler.com/eaaDev/Range.html.
[bib-repository]https://martinfowler.com/eaaCatalog/repository.html.
[bib-test-coverage]https://martinfowler.com/bliki/TestCoverage.html.
[bib-value-object]http://martinfowler.com/bliki/ValueObject.html.
[bib-null-object]https://en.wikipedia.org/wiki/Null_Object_pattern.
[bib-uniform-access]
https://martinfowler.com/bliki/UniformAccessPrinciple.html.
[bib-2hard]https://martinfowler.com/bliki/TwoHardThings.html.
[bib-form-template]https://martinfowler.com/books/r2p.html.
[bib-xunit]https://www.martinfowler.com/bliki/Xunit.html.
[bib-jackson]https://github.com/FasterXML/jackson.
[bib-lang-server]https://langserver.org.
[bib-intellij]https://www.jetbrains.com/idea/.
[bib-eclipse]http://www.eclipse.org.
[bib-mocha]https://mochajs.org.
[bib-chai]http://chaijs.com.
[bib-babel]https://babeljs.io.
[bib-maudite]https://en.wikipedia.org/wiki/Unibroue.