463

Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

  • Upload
    others

  • View
    5

  • Download
    1

Embed Size (px)

Citation preview

Page 1: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and
Page 2: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and
Page 3: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

RefactoringImprovingtheDesignofExistingCode

SecondEdition

MartinFowlerwithcontributionsbyKentBeck

Page 4: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 5: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 6: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

DefiningRefactoring

TheTwoHats

WhyShouldWeRefactor?

WhenShouldWeRefactor?

ProblemswithRefactoring

Refactoring,Architecture,andYagni

RefactoringandtheWiderSoftwareDevelopmentProcess

RefactoringandPerformance

WhereDidRefactoringComeFrom?

AutomatedRefactorings

GoingFurther

Chapter3:BadSmellsinCode

MysteriousName

DuplicatedCode

LongFunction

LongParameterList

GlobalData

MutableData

DivergentChange

ShotgunSurgery

FeatureEnvy

Page 7: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

DataClumps

PrimitiveObsession

RepeatedSwitches

Loops

LazyElement

SpeculativeGenerality

TemporaryField

MessageChains

MiddleMan

InsiderTrading

LargeClass

AlternativeClasseswithDifferentInterfaces

DataClass

RefusedBequest

Comments

Chapter4:BuildingTests

TheValueofSelf-TestingCode

SampleCodetoTest

AFirstTest

AddAnotherTest

ModifyingtheFixture

Page 8: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

ProbingtheBoundaries

MuchMoreThanThis

Chapter5:IntroducingtheCatalog

FormatoftheRefactorings

TheChoiceofRefactorings

Chapter6:AFirstSetofRefactorings

ExtractFunction

InlineFunction

ExtractVariable

InlineVariable

ChangeFunctionDeclaration

EncapsulateVariable

RenameVariable

IntroduceParameterObject

CombineFunctionsintoClass

CombineFunctionsintoTransform

SplitPhase

Chapter7:Encapsulation

EncapsulateRecord

EncapsulateCollection

ReplacePrimitivewithObject

Page 9: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

ReplaceTempwithQuery

ExtractClass

InlineClass

HideDelegate

RemoveMiddleMan

SubstituteAlgorithm

Chapter8:MovingFeatures

MoveFunction

MoveField

MoveStatementsintoFunction

MoveStatementstoCallers

ReplaceInlineCodewithFunctionCall

SlideStatements

SplitLoop

ReplaceLoopwithPipeline

RemoveDeadCode

Chapter9:OrganizingData

SplitVariable

RenameField

ReplaceDerivedVariablewithQuery

ChangeReferencetoValue

Page 10: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

ChangeValuetoReference

Chapter10:SimplifyingConditionalLogic

DecomposeConditional

ConsolidateConditionalExpression

ReplaceNestedConditionalwithGuardClauses

ReplaceConditionalwithPolymorphism

IntroduceSpecialCase

IntroduceAssertion

Chapter11:RefactoringAPIs

SeparateQueryfromModifier

ParameterizeFunction

RemoveFlagArgument

PreserveWholeObject

ReplaceParameterwithQuery

ReplaceQuerywithParameter

RemoveSettingMethod

ReplaceConstructorwithFactoryFunction

ReplaceFunctionwithCommand

ReplaceCommandwithFunction

Chapter12:DealingwithInheritance

PullUpMethod

Page 11: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

PullUpField

PullUpConstructorBody

PushDownMethod

PushDownField

ReplaceTypeCodewithSubclasses

RemoveSubclass

ExtractSuperclass

CollapseHierarchy

ReplaceSubclasswithDelegate

ReplaceSuperclasswithDelegate

Bibliography

Page 12: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 13: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

developmentvocabulary.

Myfirstexperiencewithdisciplined,“onestepatatime”refactoringwaswhenIwaspair-programmingat30,000feetwithKentBeck.Hemadesurethatweappliedrefactoringsfromthisbook’scatalogonestepatatime.Iwasamazedathowwellthispracticeworked.Notonlydidmyconfidenceintheresultingcodeincrease,Ialsofeltlessstressed.Ihighlyrecommendyoutrytheserefactorings:Youandyourcodewillfeelmuchbetterforit.

—ErichGamma,ObjectTechnologyInternational,Inc.

Page 14: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 15: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 16: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 17: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

(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

Page 18: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 19: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 20: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 21: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 22: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 23: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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)

Page 24: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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-

Page 25: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies 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

Page 26: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 27: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 28: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 29: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 30: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 31: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 32: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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).

Page 33: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 34: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 35: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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);

Page 36: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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){

Page 37: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 38: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 39: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//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

}

Page 40: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 41: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 42: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 43: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 44: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 45: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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];

Page 46: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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.

Page 47: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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`;

Page 48: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 49: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 50: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 51: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 52: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 53: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){…}

Page 54: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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`;

Page 55: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 56: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 57: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 58: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 59: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 60: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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":

Page 61: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 62: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 63: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 64: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 65: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 66: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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

Page 67: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 68: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 69: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 70: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 71: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 72: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 73: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 74: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 75: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 76: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 77: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 78: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 79: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 80: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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!

Page 81: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 82: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 83: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 84: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 85: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 86: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 87: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 88: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 89: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 90: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 91: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 92: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 93: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 94: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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-

Page 95: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 96: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 97: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 98: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 99: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 100: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

poortestcoverage.

Althoughthisbook(anditspredecessor)areintendedforprogrammerswithanylanguage,thereisaplaceforlanguage-specificrefactoringbooks.Twoofmyformercolleagues,JayFieldsandShaneHarvey,didthisfortheRubyprogramminglanguage[bib-refact-ruby].

Formoreup-to-datematerial,lookupthewebrepresentationofthisbook,aswellasthemainrefactoringwebsite:refactoring.com[bib-ref.com].

Page 101: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 102: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 103: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 104: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 105: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

(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.

Page 106: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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).

Page 107: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 108: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 109: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 110: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 111: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 112: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 113: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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)..

Page 114: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 115: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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—

Page 116: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 117: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 118: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 119: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 120: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 121: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);}

Page 122: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 123: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 124: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 125: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 126: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 127: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 128: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 129: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 130: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 131: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 132: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 133: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 134: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

untestedareasofthecode,notforassessingthequalityofatestsuite.

Thebestmeasureforagoodenoughtestsuiteissubjective:Howconfidentareyouthatifsomeoneintroducesadefectintothecode,sometestwillfail?Thisisn’tsomethingthatcanbeobjectivelyanalyzed,anditdoesn’taccountforfalseconfidence,buttheaimofself-testingcodeistogetthatconfidence.IfIcanrefactormycodeandbeprettysurethatI’venotintroducedabugbecausemytestscomebackgreen—thenIcanbehappythatIhavegoodenoughtests.

Itispossibletowritetoomanytests.OnesignofthatiswhenIspendmoretimechangingtheteststhanthecodeundertest—andIfeelthetestsareslowingmedown.Butwhileover-testingdoeshappen,it’svanishinglyrarecomparedtounder-testing.

Page 135: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 136: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 137: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 138: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 139: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

formerly:ExtractMethod

inverseof:InlineFunction(115)

Motivation

ExtractFunction(106)isoneofthemostcommonrefactoringsIdo.(Here,Iusetheterm“function”butthesameistrueforamethodinanobject-orientedlanguage,oranykindofprocedureorsubroutine.)Ilookatafragmentofcode,understandwhatitisdoing,thenextractitintoitsownfunctionnamedafteritspurpose.

Page 140: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 141: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 142: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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()}`);

}

Page 143: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 144: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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}`);

Page 145: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 146: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 147: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 148: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 149: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

formerly:InlineMethod

inverseof:ExtractFunction(106)

Motivation

Oneofthethemesofthisbookisusingshortfunctionsnamedtoshowtheirintent,becausethesefunctionsleadtoclearerandeasiertoreadcode.Butsometimes,Idocomeacrossafunctioninwhichthebodyisasclearasthename.Or,Irefactorthebodyofthecodeintosomethingthatisjustasclearasthename.Whenthishappens,Igetridofthefunction.Indirectioncanbehelpful,butneedlessindirectionisirritating.

IalsouseInlineFunctioniswhenIhaveagroupoffunctionsthatseembadlyfactored.Icaninlinethemallintoonebigfunctionandthenre-extractthefunctionsthewayIprefer.

IcommonlyuseInlineFunctionwhenIseecodethat’susingtoomuch

Page 150: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 151: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 152: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 153: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 154: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 155: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 156: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 157: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 158: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Thischecksthatit’sonlyassignedtoonce.

Findthefirstreferencetothevariableandreplaceitwiththeright-handsideoftheassignment.

Test.

Repeatreplacingreferencestothevariableuntilyou’vereplacedallofthem.

Removethedeclarationandassignmentofthevariable.

Test.

ChangeFunctionDeclaration

aka:RenameFunction

formerly:RenameMethod

formerly:AddParameter

formerly:RemoveParameter

Page 159: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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,

Page 160: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 161: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 162: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 163: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 164: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 165: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 166: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 167: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 168: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 169: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 170: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 171: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 172: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 173: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 174: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 175: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

constcpyNm="AcmeGooseberries";

Icanbegintherenamingbymakingacopy:

constcompanyName="AcmeGooseberries";

constcpyNm=companyName;

Withthecopy,Icangraduallychangereferencesfromtheoldnametothenewname.WhenI’mdone,Iremovethecopy.Iprefertodeclarethenewnameandcopytotheoldnameifitmakesitatadeasiertoremovetheoldnameandputitbackagainshouldatestfail.

Thisworksforconstantsaswellasforvariablesthatareread-onlytoclients(suchasanexportedvariableinJavaScript).

IntroduceParameterObject

Motivation

Ioftenseegroupsofdataitemsthatregularlytraveltogether,appearinginfunctionafterfunction.Suchagroupisadataclump,andIliketoreplaceitwithasingledatastructure.

Page 176: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 177: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 178: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 179: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 180: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

truevalueobject.

CombineFunctionsintoClass

Motivation

Classesareafundamentalconstructinmostmodernprogramminglanguages.Theybindtogetherdataandfunctionsintoasharedenvironment,exposingsomeofthatdataandfunctiontootherprogramelementsforcollaboration.Theyaretheprimaryconstructinobject-orientedlanguages,butarealsousefulwithotherapproachestoo.

WhenIseeagroupoffunctionsthatoperatecloselytogetheronacommonbodyofdata(usuallypassedasargumentstothefunctioncall),Iseeanopportunitytoformaclass.Usingaclassmakesthecommonenvironmentthatthesefunctionssharemoreexplicit,allowsmetosimplifyfunctioncallsinsidetheobjectbyremovingmanyofthearguments,andprovidesareferencetopasssuchanobjecttootherpartsofthesystem.

Inadditiontoorganizingalreadyformedfunctions,thisrefactoringalsoprovides

Page 181: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 182: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 183: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 184: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 185: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 186: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Softwareofteninvolvesfeedingdataintoprogramsthatcalculatevariousderivedinformationfromit.Thesederivedvaluesmaybeneededinseveralplaces,andthosecalculationsareoftenrepeatedwhereverthederiveddataisused.Iprefertobringallofthesederivationstogether,soIhaveaconsistentplacetofindandupdatethemandavoidanyduplicatelogic.

Onewaytodothisistouseadatatransformationfunctionthattakesthesourcedataasinputandcalculatesallthederivations,puttingeachderivedvalueasafieldintheoutputdata.Then,toexaminethederivations,allIneeddoislookatthetransformfunction.

AnalternativetoCombineFunctionsintoTransformisCombineFunctionsintoClass(144)thatmovesthelogicintomethodsonaclassformedfromthesourcedata.Eitheroftheserefactoringsarehelpful,andmychoicewilloftendependonthestyleofprogrammingalreadyinthesoftware.Butthereisoneimportantdifference:Usingaclassismuchbetterifthesourcedatagetsupdatedwithinthe

Page 187: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 188: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 189: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 190: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 191: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 192: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

WhenIrunintocodethat’sdealingwithtwodifferentthings,Ilookforawaytosplititintoseparatemodules.Iendeavortomakethissplitbecause,ifIneedtomakeachange,Icandealwitheachtopicseparatelyandnothavetoholdbothinmyheadtogether.IfI’mlucky,Imayonlyhavetochangeonemodulewithouthavingtorememberthedetailsoftheotheroneatall.

Oneoftheneatestwaystodoasplitlikethisistodividethebehaviorintotwosequentialphases.Agoodexampleofthisiswhenyouhavesomeprocessingwhoseinputsdon’treflectthemodelyouneedtocarryoutthelogic.Beforeyou

Page 193: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 194: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 195: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 196: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 197: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 198: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 199: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 200: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 201: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 202: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 203: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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":{

Page 204: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

"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){

Page 205: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 206: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 207: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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,

Page 208: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 209: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 210: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 211: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 212: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 213: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 214: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 215: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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).

Page 216: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 217: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 218: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Oneuseoftemporaryvariablesistocapturethevalueofsomecodeinordertorefertoitlaterinafunction.Usingatempallowsmetorefertothevaluewhileexplainingitsmeaningandavoidingrepeatingthecodethatcalculatesit.Butwhileusingavariableishandy,itcanoftenbeworthwhiletogoastepfurtheranduseafunctioninstead.

IfI’mworkingonbreakingupalargefunction,turningvariablesintotheirownfunctionsmakesiteasiertoextractpartsofthefunction,sinceInolongerneedtopassinvariablesintotheextractedfunctions.Puttingthislogicintofunctionsoftenalsosetsupastrongerboundarybetweentheextractedlogicandtheoriginalfunction,whichhelpsmespotandavoidawkwarddependenciesandsideeffects.

Page 219: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 220: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 221: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 222: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

ExtractClass

Motivation

You’veprobablyreadguidelinesthataclassshouldbeacrispabstraction,onlyhandleafewclearresponsibilities,andsoon.Inpractice,classesgrow.Youaddsomeoperationshere,abitofdatathere.Youaddaresponsibilitytoaclassfeelingthatit’snotworthaseparateclass—butasthatresponsibilitygrowsandbreeds,theclassbecomestoocomplicated.Soon,yourclassisascrispasamicrowavedduck.

Imagineaclasswithmanymethodsandquitealotofdata.Aclassthatistoobigtounderstandeasily.Youneedtoconsiderwhereitcanbesplit—andsplitit.Agoodsigniswhenasubsetofthedataandasubsetofthemethodsseemtogotogether.Othergoodsignsaresubsetsofdatathatusuallychangetogetherorareparticularlydependentoneachother.Ausefultestistoaskyourselfwhatwouldhappenifyouremoveapieceofdataoramethod.Whatotherfieldsandmethodswouldbecomenonsense?

Page 223: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;}

Page 224: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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}`;}

Page 225: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 226: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

InlineClassistheinverseofExtractClass(180).IuseInlineClassifaclassisnolongerpullingitsweightandshouldn’tbearoundanymore.Often,thisistheresultofrefactoringthatmovesotherresponsibilitiesoutoftheclasssothereislittleleft.Atthatpoint,Ifoldtheclassintoanother—onethatmakesmostuseoftheruntclass.

AnotherreasontouseInlineClassisifIhavetwoclassesthatIwanttorefactorintoapairofclasseswithadifferentallocationoffeatures.ImayfinditeasiertofirstuseInlineClasstocombinethemintoasingleclass,thenExtractClass(180)tomakethenewseparation.Thisisageneralapproachwhenreorganizingthings:Sometimes,it’seasiertomoveelementsoneatatimefromonecontexttoanother,butsometimesit’sbettertouseaninlinerefactoringtocollapsethecontextstogether,thenuseanextractrefactoringtoseparatethemintodifferentelements.

Mechanics

Page 227: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 228: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 229: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 230: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;}

Page 231: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 232: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 233: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 234: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 235: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

I’venevertriedtoskinacat.I’mtoldthereareseveralwaystodoit.I’msuresomeareeasierthanothers.Soitiswithalgorithms.IfIfindaclearerwaytodosomething,Ireplacethecomplicatedwaywiththeclearerway.Refactoringcanbreakdownsomethingcomplexintosimplerpieces,butsometimesIjustreachthepointatwhichIhavetoremovethewholealgorithmandreplaceitwithsomethingsimpler.ThisoccursasIlearnmoreabouttheproblemandrealizethatthere’saneasierwaytodoit.ItalsohappensifIstartusingalibrarythatsuppliesfeaturesthatduplicatemycode.

Page 236: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Sometimes,whenIwanttochangethealgorithmtoworkslightlydifferently,it’seasiertostartbyreplacingitwithsomethingthatwouldmakemychangemorestraightforwardtomake.

WhenIhavetotakethisstep,IhavetobesureI’vedecomposedthemethodasmuchasIcan.Replacingalarge,complexalgorithmisverydifficult;onlybymakingitsimplecanImakethesubstitutiontractable.

Mechanics

Arrangethecodetobereplacedsothatitfillsacompletefunction.

Preparetestsusingthisfunctiononly,tocaptureitsbehavior.

Prepareyouralternativealgorithm.

Runstaticchecks.

Runteststocomparetheoutputoftheoldalgorithmtothenewone.Iftheyarethesame,you’redone.Otherwise,usetheoldalgorithmforcomparisonintestinganddebugging.

Page 237: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 238: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 239: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 240: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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]);

Page 241: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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)

Page 242: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

+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;

Page 243: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

};

Page 244: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 245: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 246: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 247: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 248: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 249: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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());

Page 250: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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…

Page 251: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 252: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 253: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 254: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

inverseof:MoveStatementstoCallers(215)

Motivation

Removingduplicationisoneofthebestrulesofthumbofhealthycode.IfIseethesamecodeexecutedeverytimeIcallaparticularfunction,Ilooktocombinethatrepeatingcodeintothefunctionitself.Thatway,anyfuturemodificationstotherepeatingcodecanbedoneinoneplaceandusedbyallthecallers.Shouldthecodevaryinthefuture,Icaneasilymoveit(orsomeofit)outagainwith

Page 255: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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>`);

Page 256: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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");

}

Page 257: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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");

}

Page 258: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

IalsomaketheparameternamesfitmyconventionwhileI’matit.

MoveStatementstoCallers

inverseof:MoveStatementsintoFunction(211)

Motivation

Functionsarethebasicbuildingblockoftheabstractionswebuildasprogrammers.And,aswithanyabstraction,wedon’talwaysgettheboundariesright.Asacodebasechangesitscapabilities—asmostusefulsoftwaredoes—weoftenfindourabstractionboundariesshift.Forfunctions,thatmeansthatwhatmightoncehavebeenacohesive,atomicunitofbehaviorbecomesamixoftwoormoredifferentthings.

Page 259: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 260: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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);

Page 261: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 262: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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`);

}

Page 263: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 264: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 265: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Mechanics

Replacetheinlinecodewithacalltotheexistingfunction.

Test.

SlideStatements

formerly:ConsolidateDuplicateConditionalFragments

Motivation

Codeiseasiertounderstandwhenthingsthatarerelatedtoeachotherappeartogether.Ifseverallinesofcodeaccessthesamedatastructure,it’sbestforthemtobetogetherratherthanintermingledwithcodeaccessingotherdatastructures.Atitssimplest,IuseSlideStatementstokeepsuchcodetogether.Averycommoncaseofthisisdeclaringandusingvariables.Somepeopleliketodeclarealltheirvariablesatthetopofafunction.IprefertodeclarethevariablejustbeforeIfirstuseit.

Usually,Imoverelatedcodetogetherasapreparatorystepforanother

Page 266: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 267: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 268: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

(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

Page 269: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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-

Page 270: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

statementfragments.YoucanthinkofitasSlideStatementswhereboththeslidingfragmentandtheslid-overfragmentaresinglestatements.Thisrefactoringappealstome;afterall,I’malwaysgoingonabouttakingsmallsteps—stepsthatmayseemridiculouslysmalltothosenewtorefactoring.

ButIendedupwritingthisrefactoringwithlargerfragmentsbecausethatiswhatIdo.IonlymoveonestatementatatimeifI’mhavingdifficultywithalargerslide,andIrarelyrunintoproblemswithlargerslides.Withmoremessycode,however,smallerslidesendupbeingeasier.

SplitLoop

Page 271: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Youoftenseeloopsthataredoingtwodifferentthingsatoncejustbecausetheycandothatwithonepassthroughaloop.Butifyou’redoingtwodifferentthingsinthesameloop,thenwheneveryouneedtomodifytheloopyouhavetounderstandboththings.Bysplittingtheloop,youensureyouonlyneedtounderstandthebehavioryouneedtomodify.

Page 272: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 273: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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}`;

Page 274: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 275: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Likemostprogrammers,Iwastaughttouseloopstoiterateoveracollectionofobjects.Increasingly,however,languageenvironmentsprovideabetterconstruct:thecollectionpipeline.Collectionpipelines[bib-coll-pipe]allowmetodescribemyprocessingasaseriesofoperations,eachconsumingandemittingacollection.Themostcommonoftheseoperationsaremap,whichusesafunctiontotransformeachelementoftheinputcollection,andfilterwhichusesafunctiontoselectasubsetoftheinputcollectionforlaterstepsinthepipeline.Ifindlogicmucheasiertofollowifitisexpressedasapipeline—Icanthenreadfromtoptobottomtoseehowobjectsflowthroughthepipeline.

Mechanics

Createanewvariablefortheloop’scollection.

Thismaybeasimplecopyofanexistingvariable.

Startingatthetop,takeeachbitofbehaviorintheloopandreplaceitwitha

Page 276: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 277: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 278: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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");

Page 279: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 280: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 281: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 282: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Ifthedeadcodecanbereferencedfromoutside,e.g.whenit’safullfunction,doasearchtocheckforcallers.

Removethedeadcode.

Test.

Page 283: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 284: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 285: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 286: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 287: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 288: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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"};

Page 289: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 290: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;}

Page 291: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 292: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 293: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 294: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 295: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 296: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 297: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 298: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 299: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 300: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 301: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 302: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 303: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//loadotherdata

}

getcustomer(){returnthis._customer;}

Now,anychangesImaketothecustomerofoneorderwillbesynchronizedacrossalltheorderssharingthesamecustomer.

Forthisexample,Icreatedanewcustomerobjectwiththefirstorderthatreferencedit.Anothercommonapproachistogetalistofcustomers,populatetherepositorywiththem,andthenlinktothemasIreadtheorders.Inthatcase,anorderthatcontainsacustomerIDnotintherepositorywouldindicateanerror.

Oneproblemwiththiscodeisthattheconstructorbodyiscoupledtotheglobalrepository.Globalsshouldbetreatedwithcare—likeapowerfuldrug,theycanbebeneficialinsmalldosesbutapoisonifusedtoomuch.IfI’mconcernedaboutit,Icanpasstherepositoryasaparametertotheconstructor.

Page 304: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 305: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Oneofthemostcommonsourcesofcomplexityinaprogramiscomplexconditionallogic.AsIwritecodetodovariousthingsdependingonvariousconditions,Icanquicklyendupwithaprettylongfunction.Lengthofafunctionisinitselfafactorthatmakesithardertoread,butconditionsincreasethedifficulty.Theproblemusuallyliesinthefactthatthecode,bothintheconditionchecksandintheactions,tellsmewhathappensbutcaneasilyobscurewhyithappens.

Aswithanylargeblockofcode,Icanmakemyintentionclearerbydecomposingitandreplacingeachchunkofcodewithafunctioncallnamedaftertheintentionofthatchunk.Withconditions,Iparticularlylikedoingthisfortheconditionalpartandeachofthealternatives.Thisway,IhighlighttheconditionandmakeitclearwhatI’mbranchingon.Ialsohighlightthereasonforthebranching.

Page 306: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 307: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 308: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Sometimes,Irunintoaseriesofconditionalcheckswhereeachcheckisdifferentyettheresultingactionisthesame.WhenIseethis,Iuseandandoroperatorstoconsolidatethemintoasingleconditionalcheckwithasingleresult.

Consolidatingtheconditionalcodeisimportantfortworeasons.First,itmakesitclearerbyshowingthatI’mreallymakingasinglecheckthatcombinesotherchecks.Thesequencehasthesameeffect,butitlookslikeI’mcarryingoutasequenceofseparatechecksthatjusthappentobeclosetogether.ThesecondreasonIliketodothisisthatitoftensetsmeupforExtractFunction(106).ExtractingaconditionisoneofthemostusefulthingsIcandotoclarifymycode.ItreplacesastatementofwhatI’mdoingwithwhyI’mdoingit.

Thereasonsinfavorofconsolidatingconditionalsalsopointtothereasonsagainstdoingit.IfIconsiderittobetrulyindependentchecksthatshouldn’tbethoughtofasasinglecheck,Idon’tdotherefactoring.

Page 309: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 310: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//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

Page 311: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Ioftenfindthatconditionalexpressionscomeintwostyles.Inthefirststyle,bothlegsoftheconditionalarepartofnormalbehavior,whileinthesecondstyle,onelegisnormalandtheotherindicatesanunusualcondition.

Page 312: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 313: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 314: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 315: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 316: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 317: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Complexconditionallogicisoneofthehardestthingstoreasonaboutinprogramming,soIalwayslookforwaystoaddstructuretoconditionallogic.Often,IfindIcanseparatethelogicintodifferentcircumstances—high-levelcases—todividetheconditions.Sometimesit’senoughtorepresentthisdivisionwithinthestructureofaconditionalitself,butusingclassesandpolymorphismcanmaketheseparationmoreexplicit.

Page 318: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 319: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 320: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 321: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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";

Page 322: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

}

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){

Page 323: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 324: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 325: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 326: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 327: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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{

Page 328: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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;

Page 329: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 330: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 331: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

(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;

Page 332: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 333: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 334: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 335: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 336: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;}

Page 337: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 338: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 339: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 340: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 341: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 342: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 343: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 344: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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,

};

Page 345: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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

},

Page 346: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//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…

Page 347: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 348: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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;

Page 349: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 350: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 351: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 352: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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.

Page 353: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 354: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 355: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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";

Page 356: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

}

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);

Page 357: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 358: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 359: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 360: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 361: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

+withinBand(usage,200,Infinity)*0.07;

returnusd(amount);

}

functiontopBand(usage){

returnusage>200?usage-200:0;

}

Withthelogicworkingthewayitdoesnow,Icouldremovetheinitialguardclause.Butalthoughit’slogicallyunnecessarynow,Iliketokeepitasitdocumentshowtohandlethatcase.

RemoveFlagArgument

formerly:ReplaceParameterwithExplicitMethods

Page 362: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 363: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 364: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 365: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 366: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 367: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 368: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 369: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 370: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 371: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 372: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Ithencontinueasbefore,replacingothercallersandinliningtheoldfunctionintothenewone.IwouldalsoinlinethevariablesIextractedtoprovidethecleanseparationforextractingthenewfunction.

Becausethisvariationisentirelycomposedofrefactorings,it’sparticularlyhandywhenIhavearefactoringtoolwithrobustextractandinlineoperations.

ReplaceParameterwithQuery

formerly:ReplaceParameterwithMethod

inverseof:ReplaceQuerywithParameter(325)

Motivation

Theparameterlisttoafunctionshouldsummarizethepointsofvariabilityofthatfunction,indicatingtheprimarywaysinwhichthatfunctionmaybehavedifferently.Aswithanystatementincode,it’sgoodtoavoidanyduplication,anditseasiertounderstandiftheparameterlistisshort.

Ifacallpassesinavaluethatthefunctioncanjustaseasilydetermineforitself,

Page 373: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 374: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 375: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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)

Page 376: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 377: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 378: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 379: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 380: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 381: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 382: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 383: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 384: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 385: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 386: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

formerly:ReplaceMethodwithMethodObject

inverseof:ReplaceCommandwithFunction(342)

Motivation

Functions—eitherfreestandingorattachedtoobjectsasmethods—areoneofthefundamentalbuildingblocksofprogramming.Buttherearetimeswhenit’susefultoencapsulateafunctionintoitsownobject,whichIrefertoasa“commandobject”orsimplyacommand.Suchanobjectismostlybuiltaroundasinglemethod,whoserequestandexecutionisthepurposeoftheobject.

Page 387: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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”.

Page 388: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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{

Page 389: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 390: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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

Page 391: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 392: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 393: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

function.Indeed,whendoingthisrefactoringinJavaScript,usingnestedfunctionswouldbeareasonablealternativetousingacommand.I’dstilluseacommandforthis,partlybecauseI’mmorefamiliarwithcommandsandpartlybecausewithacommandIcanwritetestsanddebuggingcallsagainstthesubfunctions.

ReplaceCommandwithFunction

inverseof:ReplaceFunctionwithCommand(335)

Motivation

Commandobjectsprovideapowerfulmechanismforhandlingcomplexcomputations.Theycaneasilybebrokendownintoseparatemethodssharingcommonstatethroughthefields;theycanbeinvokedviadifferentmethodsfordifferenteffects;theycanhavetheirdatabuiltupinstages.Butthatpowercomesatacost.Mostofthetime,Ijustwanttoinvokeafunctionandhaveitdo

Page 394: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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(){

Page 395: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 396: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 397: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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.

Page 398: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 399: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

inverseof:PushDownMethod(357)

Motivation

Eliminatingduplicatecodeisimportant.Twoduplicatemethodsmayworkfineastheyare,buttheyarenothingbutabreedinggroundforbugsinthefuture.Wheneverthereisduplication,thereisriskthatanalterationtoonecopywillnotbemadetotheother.Usually,itisdifficulttofindtheduplicates.

TheeasiestcaseofusingPullUpMethodiswhenthemethodshavethesamebody,implyingthere’sbeenacopyandpaste.Ofcourseit’snotalwaysasobviousasthat.Icouldjustdotherefactoringandseeifthetestscroak—butthat

Page 400: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 401: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 402: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

monthlyCost,butmonthlyCostdoesn’tappearinthePartyclass.Itallworks,becauseJavaScriptisadynamiclanguage—butthereisvalueinsignalingthatsubclassesofPartyshouldprovideanimplementationformonthlyCost,particularlyifmoresubclassesgetaddedlateron.Agoodwaytoprovidethissignalisatrapmethodlikethis:

classParty…

getmonthlyCost(){

thrownewSubclassResponsibilityError();

}

IcallsuchanerrorasubclassresponsibilityerrorasthatwasthenameusedinSmalltalk.

PullUpField

Page 403: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

inverseof:PushDownField(359)

Motivation

Ifsubclassesaredevelopedindependently,orcombinedthroughrefactoring,Ioftenfindthattheyduplicatefeatures.Inparticular,certainfieldscanbeduplicates.Suchfieldssometimeshavesimilarnames—butnotalways.TheonlywayIcantellwhatisgoingonisbylookingatthefieldsandexamininghowtheyareused.Iftheyarebeingusedinasimilarway,Icanpullthemupintothesuperclass.

Bydoingthis,Ireduceduplicationintwoways.Iremovetheduplicatedata

Page 404: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

declarationandIcanthenmovebehaviorthatusesthefieldfromthesubclassestothesuperclass.

Manydynamiclanguagesdonotdefinefieldsaspartoftheirclassdefinition—instead,fieldsappearwhentheyarefirstassignedto.Inthiscase,pullingupafieldisessentiallyaconsequenceofPullUpConstructorBody(353).

Mechanics

Inspectallusersofthecandidatefieldtoensuretheyareusedinthesameway.

Ifthefieldshavedifferentnames,useRenameField(244)togivethemthesamename.

Createanewfieldinthesuperclass.

Thenewfieldwillneedtobeaccessibletosubclasses(protectedincommonlanguages).

Deletethesubclassfields.

Test.

PullUpConstructorBody

Page 405: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

Page 406: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 407: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//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;

}

Page 408: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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();

}

Page 409: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Then,IusePullUpMethod(348)tomoveittothesuperclass.

classEmployee…

finishConstruction(){

if(this.isPrivileged)this.assignCar();

}

PushDownMethod

inverseof:PullUpMethod(348)

Motivation

Ifamethodisonlyrelevanttoonesubclass(orasmallproportionofsubclasses),removingitfromthesuperclassandputtingitonlyonthesubclass(es)makes

Page 410: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

thatclearer.Icanonlydothisrefactoringifthecallerknowsit’sworkingwithaparticularsubclass—otherwise,IshoulduseReplaceConditionalwithPolymorphism(271)withsomeplacebobehavioronthesuperclass.

Mechanics

Copythemethodintoeverysubclassthatneedsit.

Removethemethodfromthesuperclass.

Test.

Removethemethodfromeachsuperclassthatdoesn’tneedit.

Test.

PushDownField

Page 411: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

inverseof:PullUpField(351)

Motivation

Ifafieldisonlyusedbyonesubclass(orasmallproportionofsubclasses),Imoveittothosesubclasses.

Mechanics

Declarefieldinallsubclassesthatneedit.

Removethefieldfromthesuperclass.

Test.

Page 412: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Removethefieldfromallsubclassesthatdon’tneedit.

Test.

ReplaceTypeCodewithSubclasses

subsumes:ReplaceTypeCodewithState/Strategy

subsumes:ExtractSubclass

inverseof:RemoveSubclass(368)

Motivation

Softwaresystemsoftenneedtorepresentdifferentkindsofasimilarthing.Imayclassifyemployeesbytheirjobtype(engineer,manager,salesman),orordersbytheirpriority(rush,regular).Myfirsttoolforhandlingthisissomekindoftype

Page 413: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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)

Page 414: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 415: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 416: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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;

Page 417: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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})`;

}

Page 418: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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();

Page 419: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 420: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

formerly:ReplaceSubclasswithFields

inverseof:ReplaceTypeCodewithSubclasses(361)

Motivation

Subclassesareuseful.Theysupportvariationsindatastructureandpolymorphicbehavior.Theyareagoodwaytoprogrambydifference.Butasasoftwaresystemevolves,subclassescanlosetheirvalueasthevariationstheysupportaremovedtootherplacesorremovedaltogether.Sometimes,subclassesareaddedinanticipationoffeaturesthatneverendupbeingbuilt,orendupbeingbuiltinawaythatdoesn’tneedthesubclasses.

Asubclassthatdoestoolittleincursacostinunderstandingthatisnolongerworthwhile.Whenthattimecomes,it’sbesttoremovethesubclass,replacingit

Page 421: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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";}

Page 422: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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);

}

Page 423: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 424: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 425: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 426: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

IfIseetwoclassesdoingsimilarthings,Icantakeadvantageofthebasic

Page 427: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 428: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 429: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

//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;}

Page 430: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 431: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

classEmployee…

getannualCost(){

returnthis.monthlyCost*12;

}

classDepartment…

getannualCost(){

returnthis.monthlyCost*12;

}

CollapseHierarchy

Motivation

WhenI’mrefactoringaclasshierarchy,I’moftenpullingandpushingfeaturesaround.Asthehierarchyevolves,Isometimesfindthataclassanditsparentarenolongerdifferentenoughtobeworthkeepingseparate.Atthispoint,I’llmergethemtogether.

Mechanics

Page 432: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Choosewhichonetoremove.

Ichoosebasedonwhichnamemakesmostsenseinthefuture.Ifneithernameisbest,I’llpickonearbitrarily.

UsePullUpField(351),PushDownField(359),PullUpMethod(348),andPullUpField(351)tomovealltheelementsintoasingleclass.

Adjustanyreferencestothevictimtochangethemtotheclassthatwillstay.

Removetheemptyclass.

Test.

ReplaceSubclasswithDelegate

Page 433: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

Motivation

IfIhavesomeobjectswhosebehaviorvariesfromcategorytocategory,thenaturalmechanismtoexpressthisisinheritance.Iputallthecommondataandbehaviorinthesuperclass,andleteachsubclassaddandoverridefeaturesasneeded.Object-orientedlanguagesmakethissimpletoimplementandthusa

Page 434: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 435: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 436: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 437: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 438: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 439: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 440: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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…

Page 441: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 442: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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:

Page 443: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 444: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 445: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

}

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…

Page 446: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 447: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.

Page 448: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

Page 449: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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.

Page 450: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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){

Page 451: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

Page 452: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

}

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(){

Page 453: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 454: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

formerly:ReplaceInheritancewithDelegation

Motivation

Inobject-orientedprograms,inheritanceisapowerfulandeasilyavailablewaytoreuseexistingfunctionality.Iinheritfromsomeexistingclass,thenoverrideandaddadditionalfeatures.Butsubclassingcanbedoneinawaythatleadstoconfusionandcomplication.

Oneoftheclassicexamplesofmis-inheritancefromtheearlydaysofobjectswasmakingastackbeasubclassoflist.Theideathatledtothiswasreusingoflist’sdatastorageandoperationstomanipulateit.Whileit’sgoodtoreuse,thisinheritancehadaproblem:Alltheoperationsofthelistwerepresentontheinterfaceofthestack,althoughmostofthemwerenotapplicabletoastack.Abetterapproachistomakethelistintoafieldofthestackanddelegatethenecessaryoperationstoit.

ThisisanexampleofonereasontouseReplaceSuperclasswithDelegate—if

Page 455: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 456: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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);

}

Page 457: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 458: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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

Page 459: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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,

Page 460: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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;

}

Page 461: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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-

Page 462: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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]

Page 463: Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier Parameterize Function Remove Flag Argument Preserve Whole Object ... hierarchies and

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.