Refactoring: Improving the Design of Existing Code, Second ... · Separate Query from Modifier...

Preview:

Citation preview

RefactoringImprovingtheDesignofExistingCode

SecondEdition

MartinFowlerwithcontributionsbyKentBeck

ContentsataGlanceForewordtotheFirstEdition

Preface

Chapter1:Refactoring:AFirstExample

Chapter2:PrinciplesinRefactoring

Chapter3:BadSmellsinCode

Chapter4:BuildingTests

Chapter5:IntroducingtheCatalog

Chapter6:AFirstSetofRefactorings

Chapter7:Encapsulation

Chapter8:MovingFeatures

Chapter9:OrganizingData

Chapter10:SimplifyingConditionalLogic

Chapter11:RefactoringAPIs

Chapter12:DealingwithInheritance

Bibliography

ContentsForewordtotheFirstEdition

Preface

WhatIsRefactoring?

What’sinThisBook?

WhoShouldReadThisBook?

BuildingonaFoundationLaidbyOthers

Acknowledgments

Chapter1:Refactoring:AFirstExample

TheStartingPoint

CommentsontheStartingProgram

TheFirstStepinRefactoring

DecomposingthestatementFunction

Status:LotsofNestedFunctions

SplittingthePhasesofCalculationandFormatting

Status:SeparatedintoTwoFiles(andPhases)

ReorganizingtheCalculationsbyType

Status:CreatingtheDatawiththePolymorphicCalculator

FinalThoughts

Chapter2:PrinciplesinRefactoring

DefiningRefactoring

TheTwoHats

WhyShouldWeRefactor?

WhenShouldWeRefactor?

ProblemswithRefactoring

Refactoring,Architecture,andYagni

RefactoringandtheWiderSoftwareDevelopmentProcess

RefactoringandPerformance

WhereDidRefactoringComeFrom?

AutomatedRefactorings

GoingFurther

Chapter3:BadSmellsinCode

MysteriousName

DuplicatedCode

LongFunction

LongParameterList

GlobalData

MutableData

DivergentChange

ShotgunSurgery

FeatureEnvy

DataClumps

PrimitiveObsession

RepeatedSwitches

Loops

LazyElement

SpeculativeGenerality

TemporaryField

MessageChains

MiddleMan

InsiderTrading

LargeClass

AlternativeClasseswithDifferentInterfaces

DataClass

RefusedBequest

Comments

Chapter4:BuildingTests

TheValueofSelf-TestingCode

SampleCodetoTest

AFirstTest

AddAnotherTest

ModifyingtheFixture

ProbingtheBoundaries

MuchMoreThanThis

Chapter5:IntroducingtheCatalog

FormatoftheRefactorings

TheChoiceofRefactorings

Chapter6:AFirstSetofRefactorings

ExtractFunction

InlineFunction

ExtractVariable

InlineVariable

ChangeFunctionDeclaration

EncapsulateVariable

RenameVariable

IntroduceParameterObject

CombineFunctionsintoClass

CombineFunctionsintoTransform

SplitPhase

Chapter7:Encapsulation

EncapsulateRecord

EncapsulateCollection

ReplacePrimitivewithObject

ReplaceTempwithQuery

ExtractClass

InlineClass

HideDelegate

RemoveMiddleMan

SubstituteAlgorithm

Chapter8:MovingFeatures

MoveFunction

MoveField

MoveStatementsintoFunction

MoveStatementstoCallers

ReplaceInlineCodewithFunctionCall

SlideStatements

SplitLoop

ReplaceLoopwithPipeline

RemoveDeadCode

Chapter9:OrganizingData

SplitVariable

RenameField

ReplaceDerivedVariablewithQuery

ChangeReferencetoValue

ChangeValuetoReference

Chapter10:SimplifyingConditionalLogic

DecomposeConditional

ConsolidateConditionalExpression

ReplaceNestedConditionalwithGuardClauses

ReplaceConditionalwithPolymorphism

IntroduceSpecialCase

IntroduceAssertion

Chapter11:RefactoringAPIs

SeparateQueryfromModifier

ParameterizeFunction

RemoveFlagArgument

PreserveWholeObject

ReplaceParameterwithQuery

ReplaceQuerywithParameter

RemoveSettingMethod

ReplaceConstructorwithFactoryFunction

ReplaceFunctionwithCommand

ReplaceCommandwithFunction

Chapter12:DealingwithInheritance

PullUpMethod

PullUpField

PullUpConstructorBody

PushDownMethod

PushDownField

ReplaceTypeCodewithSubclasses

RemoveSubclass

ExtractSuperclass

CollapseHierarchy

ReplaceSubclasswithDelegate

ReplaceSuperclasswithDelegate

Bibliography

ForewordtotheFirstEdition“Refactoring”wasconceivedinSmalltalkcircles,butitwasn’tlongbeforeitfounditswayintootherprogramminglanguagecamps.Becauserefactoringisintegraltoframeworkdevelopment,thetermcomesupquicklywhen“frameworkers”talkabouttheircraft.Itcomesupwhentheyrefinetheirclasshierarchiesandwhentheyraveabouthowmanylinesofcodetheywereabletodelete.Frameworkersknowthataframeworkwon’tberightthefirsttimearound—itmustevolveastheygainexperience.Theyalsoknowthatthecodewillbereadandmodifiedmorefrequentlythanitwillbewritten.Thekeytokeepingcodereadableandmodifiableisrefactoring—forframeworks,inparticular,butalsoforsoftwareingeneral.

So,what’stheproblem?Simplythis:Refactoringisrisky.Itrequireschangestoworkingcodethatcanintroducesubtlebugs.Refactoring,ifnotdoneproperly,cansetyoubackdays,evenweeks.Andrefactoringbecomesriskierwhenpracticedinformallyoradhoc.Youstartdigginginthecode.Soonyoudiscovernewopportunitiesforchange,andyoudigdeeper.Themoreyoudig,themorestuffyouturnup...andthemorechangesyoumake.Eventuallyyoudigyourselfintoaholeyoucan’tgetoutof.Toavoiddiggingyourowngrave,refactoringmustbedonesystematically.WhenmycoauthorsandIwroteDesignPatterns,wementionedthatdesignpatternsprovidetargetsforrefactorings.However,identifyingthetargetisonlyonepartoftheproblem;transformingyourcodesothatyougetthereisanotherchallenge.

MartinFowlerandthecontributingauthorsmakeaninvaluablecontributiontoobject-orientedsoftwaredevelopmentbysheddinglightontherefactoringprocess.Thisbookexplainstheprinciplesandbestpracticesofrefactoring,andpointsoutwhenandwhereyoushouldstartdigginginyourcodetoimproveit.Atthebook’scoreisacomprehensivecatalogofrefactorings.Eachrefactoringdescribesthemotivationandmechanicsofaprovencodetransformation.Someoftherefactorings,suchasExtractMethodorMoveField,mayseemobvious.

Butdon’tbefooled.Understandingthemechanicsofsuchrefactoringsisthekeytorefactoringinadisciplinedway.Therefactoringsinthisbookwillhelpyouchangeyourcodeonesmallstepatatime,thusreducingtherisksofevolvingyourdesign.Youwillquicklyaddtheserefactoringsandtheirnamestoyour

developmentvocabulary.

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

—ErichGamma,ObjectTechnologyInternational,Inc.

PrefaceOnceuponatime,aconsultantmadeavisittoadevelopmentprojectinordertolookatsomeofthecodethathadbeenwritten.Ashewanderedthroughtheclasshierarchyatthecenterofthesystem,theconsultantfounditrathermessy.Thehigher-levelclassesmadecertainassumptionsabouthowtheclasseswouldwork—assumptionsthatwereembodiedininheritedcode.Thatcodedidn’tsuitallthesubclasses,however,andwasoverriddenquiteheavily.Slightmodificationstothesuperclasswouldhavegreatlyreducedtheneedtooverrideit.Inotherplaces,anintentionofthesuperclasshadnotbeenproperlyunderstood,andbehaviorpresentinthesuperclasswasduplicated.Inyetotherplaces,severalsubclassesdidthesamethingwithcodethatcouldclearlybemovedupthehierarchy.

Theconsultantrecommendedtotheprojectmanagementthatthecodebelookedatandcleanedup—buttheprojectmanagementwasn’tenthusiastic.Thecodeseemedtoworkandtherewereconsiderableschedulepressures.Themanagerssaidtheywouldgetaroundtoitatsomelaterpoint.

Theconsultanthadalsoshownwhatwasgoingontotheprogrammersworkingonthehierarchy.Theprogrammerswerekeenandsawtheproblem.Theyknewthatitwasn’treallytheirfault;sometimes,anewpairofeyesisneededtospottheproblem.Sotheprogrammersspentadayortwocleaningupthehierarchy.Whenfinished,theyhadremovedhalfthecodeinthehierarchywithoutreducingitsfunctionality.Theywerepleasedwiththeresultandfoundthatitbecamequickerandeasierbothtoaddnewclassesandtousetheclassesintherestofthesystem.

Theprojectmanagementwasnotpleased.Schedulesweretightandtherewasalotofworktodo.Thesetwoprogrammershadspenttwodaysdoingworkthataddednothingtothemanyfeaturesthesystemhadtodeliverinafewmonths’time.Theoldcodehadworkedjustfine.Yes,thedesignwasabitmore“pure”andabitmore“clean.”Buttheprojecthadtoshipcodethatworked,notcodethatwouldpleaseanacademic.Theconsultantsuggestedthatasimilarcleanupshouldbedoneonothercentralpartsofthesystem,whichmighthalttheprojectforaweekortwo.Allthiswastomakethecodelookbetter,nottomakeitdoanythingitdidn’talreadydo.

Howdoyoufeelaboutthisstory?Doyouthinktheconsultantwasrighttosuggestfurthercleanup?Ordoyoufollowthatoldengineeringadage,“ifitworks,don’tfixit”?

Imustadmittosomebiashere.Iwasthatconsultant.Sixmonthslater,theprojectfailed,inlargepartbecausethecodewastoocomplextodebugortunetoacceptableperformance.

TheconsultantKentBeckwasbroughtintorestarttheproject—anexercisethatinvolvedrewritingalmostthewholesystemfromscratch.Hedidseveralthingsdifferently,butoneofthemostimportantchangeswastoinsistoncontinuouscleaningupofthecodeusingrefactoring.Theimprovedeffectivenessoftheteam,andtherolerefactoringplayed,iswhatinspiredmetowritethefirsteditionofthisbook—soIcouldpassontheknowledgethatKentandothershaveacquiredbyusingrefactoringtoimprovethequalityofsoftware.

Sincethen,refactoringhasbecomeanacceptedpartofthevocabularyofprogramming.Andtheoriginalbookhasstoodupratherwell.However,eighteenyearsisanoldageforaprogrammingbook,soIfeltitwastimetogobackandreworkit.Doingthishadmerewriteprettymucheverypageinthebook.But,inasense,verylittlehaschanged.Theessenceofrefactoringisthesame;mostofthekeyrefactoringsremainessentiallythesame.ButIdohopethattherewritingwillhelpmorepeoplelearnhowtodorefactoringeffectively.

WhatIsRefactoring?

Refactoringistheprocessofchangingasoftwaresysteminawaythatdoesnotaltertheexternalbehaviorofthecodeyetimprovesitsinternalstructure.Itisadisciplinedwaytocleanupcodethatminimizesthechancesofintroducingbugs.Inessence,whenyourefactor,youareimprovingthedesignofthecodeafterithasbeenwritten.

“Improvingthedesignafterithasbeenwritten.”That’sanoddturnofphrase.Formuchofthehistoryofsoftwaredevelopment,mostpeoplebelievedthatwedesignfirst,andonlywhendonewithdesignshouldwecode.Overtime,thecodewillbemodified,andtheintegrityofthesystem—itsstructureaccordingtothatdesign—graduallyfades.Thecodeslowlysinksfromengineeringtohacking.

Refactoringistheoppositeofthispractice.Withrefactoring,wecantakeabad,evenchaotic,designandreworkitintowell-structuredcode.Eachstepissimple—evensimplistic.Imoveafieldfromoneclasstoanother,pullsomecodeoutofamethodtomakeitintoitsownmethod,orpushsomecodeupordownahierarchy.Yetthecumulativeeffectofthesesmallchangescanradicallyimprovethedesign.Itistheexactreverseofthenotionofsoftwaredecay.

Withrefactoring,thebalanceofworkchanges.Ifoundthatdesign,ratherthanoccurringallupfront,occurscontinuouslyduringdevelopment.AsIbuildthesystem,Ilearnhowtoimprovethedesign.Theresultofthisinteractionisaprogramwhosedesignstaysgoodasdevelopmentcontinues.

What’sinThisBook?

Thisbookisaguidetorefactoring;itiswrittenforaprofessionalprogrammer.Myaimistoshowyouhowtodorefactoringinacontrolledandefficientmanner.Youwilllearntorefactorinsuchawaythatyoudon’tintroducebugsintothecodebutmethodicallyimproveitsstructure.

Traditionally,abookstartswithanintroduction.Iagreewiththatinprinciple,butIfindithardtointroducerefactoringwithageneralizeddiscussionordefinitions—soIstartwithanexample.Chapter1takesasmallprogramwithsomecommondesignflawsandrefactorsitintoaprogramthat’seasiertounderstandandchange.Thiswillshowyouboththeprocessofrefactoringandanumberofusefulrefactorings.Thisisthekeychaptertoreadifyouwanttounderstandwhatrefactoringreallyisabout.

InChapter2,Icovermoreofthegeneralprinciplesofrefactoring,somedefinitions,andthereasonsfordoingrefactoring.Ioutlinesomeofthechallengeswithrefactoring.InChapter3,KentBeckhelpsmedescribehowtofindbadsmellsincodeandhowtocleanthemupwithrefactorings.Testingplaysaveryimportantroleinrefactoring,soChapter4describeshowtobuildtestsintocode.

Theheartofthebook—thecatalogofrefactorings—takesuptherestofitsvolume.Whilethisisbynomeansacomprehensivecatalog,itcoversthekeyrefactoringsthatmostdeveloperswilllikelyneed.ItgrewfromthenotesImadewhenlearningaboutrefactoringinthelate1990s,andIstillusethesenotesnowasIdon’trememberthemall.WhenIwanttodosomething,suchasSplitPhase

(154),thecatalogremindsmehowtodoitinasafe,step-by-stepmanner.Ihopethisisthepartofthebookthatyou’llcomebacktooften.

AWeb-FirstBook

TheWorld-WideWebhasmadeanenormousimpactonoursociety,particularlyaffectinghowwegatherinformation.WhenIwrotethisbook,mostoftheknowledgeaboutsoftwaredevelopmentwastransferredthroughprint.NowIgathermostofmyinformationonline.Thishaspresentedachallengeforauthorslikemyself:Istherestillaroleforbooks,andwhatshouldtheylooklike?

Ibelievetherestillisroleforbookslikethis—buttheyneedtochange.Thevalueofabookisalargebodyofknowledgeputtogetherinacohesivefashion.Inwritingthisbook,Itriedtocovermanydifferentrefactoringsandorganizetheminaconsistentandintegratedmanner.

Butthatintegratedwholeisanabstractliteraryworkthat,whiletraditionallyrepresentedbyapaperbook,neednotbeinthefuture.Mostofthebookindustrystillseesthepaperbookastheprimaryrepresentation,andwhilewe’veenthusiasticallyadoptedebooks,theyarejustelectronicrepresentationsofanoriginalworkbasedonthestructureofapaperbook.

Withthisbook,I’mexploringadifferentapproach.Thecanonicalformofthisbookisitswebsite.Thepaperbookisaselectionofmaterialfromthewebsite,arrangedinamannerthatmakessenseforprint.Itdoesn’tattempttoincludealltherefactoringsonthewebsite,particularlysinceImaywelladdmorerefactoringstothecanonicalwebbookinthefuture.Similarly,theebookisadifferentrepresentationofthewebbookthatmaynotincludethesamesetofrefactoringsastheprintedbook—afterall,ebooksdon’tgetheavyasIaddpagesandtheycanbeeasilyupdatedaftertheyarebought.

Idon’tknowwhetheryou’rereadingthisonthewebsite,inanebook,onpaper,orinsomeotherformIcan’timagineasIwritethis.Idomybesttomakethisausefulwork,whateverwayyouwishtoabsorbit.

JavaScriptExamples

Asinmosttechnicalareasofsoftwaredevelopment,codeexamplesareveryimportanttoillustratetheconcepts.However,therefactoringslookmostlythe

sameindifferentlanguages.Therewillsometimesbeparticularthingsthatalanguageforcesmetopayattentionto,butthecoreelementsoftherefactoringsremainthesame.

IchoseJavaScripttoillustratetheserefactorings,asIfeltthatthislanguagewouldbereadablebythemostamountofpeople.Youshouldn’tfinditdifficult,however,toadapttherefactoringstowhateverlanguagetheyarecurrentlyusing.Itrynottouseanyofthemorecomplicatedbitsofthelanguage,soyoushouldbeabletofollowtherefactoringswithonlyacursoryknowledgeofJavaScript.MyuseofJavaScriptiscertainlynotanendorsementofthelanguage.

AlthoughIuseJavaScriptformyexamples,thatdoesn’tmeanthetechniquesinthisbookareconfinedtoJavaScript.ThefirsteditionofthisbookusedJava,andmanyprogrammersfounditusefuleventhoughtheyneverwroteasingleJavaclass.Ididtoywithillustratingthisgeneralitybyusingadozendifferentlanguagesfortheexamples,butIfeltthatwouldbetooconfusingforthereader.Still,thisbookiswrittenforprogrammersinanylanguage.Outsideoftheexamplesections,I’mnotmakinganyassumptionsaboutthelanguage.Iexpectthereadertoabsorbmygeneralcommentsandapplythemtothelanguagetheyareusing.Indeed,IexpectreaderstotaketheJavaScriptexamplesandadaptthemtotheirlanguage.

Thismeansthat,apartfromdiscussingspecificexamples,whenItalkabout“class”,“module”,“function,”etc.,Iusethosetermsinthegeneralprogrammingmeaning,notasspecifictermsoftheJavaScriptlanguagemodel.

ThefactthatI’musingJavaScriptastheexamplelanguagealsomeansthatItrytoavoidJavaScriptstylesthatwillbelessfamiliartothosewhoaren’tregularJavaScriptprogrammers.Thisisnota“refactoringinJavaScript”book—rather,it’sageneralrefactoringbookthathappenstouseJavaScript.TherearemanyinterestingrefactoringsthatarespecifictoJavaScript(suchasrefactoringfromcallbacks,topromises,toasync/await)buttheyareoutofscopeforthisbook.

WhoShouldReadThisBook?

I’veaimedthisbookataprofessionalprogrammer—someonewhowritessoftwareforaliving.Theexamplesanddiscussionincludealotofcodetoreadandunderstand.TheexamplesareinJavaScript,butshouldbeapplicabletomostlanguages.Iwouldexpectaprogrammertohavesomeexperienceto

appreciatewhat’sgoingonwiththisbook,butIdon’tassumemuchknowledge.

Althoughtheprimarytargetofthisbookisadeveloperseekingtolearnaboutrefactoring,thisbookisalsovaluableforsomeonewhoalreadyunderstandsrefactoring—itcanbeusedasateachingaid.Inthisbook,I’veputalotofeffortintoexplaininghowvariousrefactoringswork,soanexperienceddevelopercanusethismaterialinmentoringtheircolleagues.

Althoughitisfocusedonthecode,refactoringhasalargeimpactonthedesignofsystem.Itisvitalforseniordesignersandarchitectstounderstandtheprinciplesofrefactoringandtousethemintheirprojects.Refactoringisbestintroducedbyarespectedandexperienceddeveloper.Suchadevelopercanbestunderstandtheprinciplesbehindrefactoringandadaptthoseprinciplestothespecificworkplace.ThisisparticularlytruewhenyouareusingalanguageotherthanJavaScript,becauseyou’llhavetoadapttheexamplesI’vegiventootherlanguages.

Here’showtogetthemostfromthisbookwithoutreadingallofit.

Ifyouwanttounderstandwhatrefactoringis,readChapter1—theexampleshouldmaketheprocessclear.

Ifyouwanttounderstandwhyyoushouldrefactor,readthefirsttwochapters.Theywilltellyouwhatrefactoringisandwhyyoushoulddoit.

Ifyouwanttofindwhereyoushouldrefactor,readChapter3.Ittellsyouthesignsthatsuggesttheneedforrefactoring.

Ifyouwanttoactuallydorefactoring,readthefirstfourchapterscompletely,thenskip-readthecatalog.Readenoughofthecatalogtoknow,roughly,whatisinthere.Youdon’thavetounderstandallthedetails.Whenyouactuallyneedtocarryoutarefactoring,readtherefactoringindetailanduseittohelpyou.Thecatalogisareferencesection,soyouprobablywon’twanttoreaditinonego.

Animportantpartofwritingthisbookwasnamingthevariousrefactorings.Terminologyhelpsuscommunicate,sothatwhenonedeveloperadvisesanothertoextractsomecodeintoafunction,ortosplitsomecomputationintoseparatephases,bothunderstandthereferencestoExtractFunction(106)andSplitPhase(154).Thisvocabularyalsohelpsinselectingautomatedrefactorings.

BuildingonaFoundationLaidbyOthers

IneedtosayrightatthebeginningthatIoweabigdebtwiththisbook—adebttothosewhoseworkinthe1990sdevelopedthefieldofrefactoring.Itwaslearningfromtheirexperiencethatinspiredandinformedmetowritethefirsteditionofthisbook,andalthoughmanyyearshavepassed,it’simportantthatIcontinuetoacknowledgethefoundationthattheylaid.Ideally,oneofthemshouldhavewrittenthatfirstedition,butIendedupbeingtheonewiththetimeandenergy.

TwooftheleadingearlyproponentsofrefactoringwereWardCunninghamandKentBeck.Theyuseditasafoundationofdevelopmentintheearlydaysandadaptedtheirdevelopmentprocessestotakeadvantageofit.Inparticular,itwasmycollaborationwithKentthatshowedmetheimportanceofrefactoring—aninspirationthatleddirectlytothisbook.

RalphJohnsonleadsagroupattheUniversityofIllinoisatUrbana-Champaignthatisnotableforitspracticalcontributionstoobjecttechnology.Ralphhaslongbeenachampionofrefactoring,andseveralofhisstudentsdidvitalearlyworkinthisfield.BillOpdykedevelopedthefirstdetailedwrittenworkonrefactoringinhisdoctoralthesis.JohnBrantandDonRobertswentbeyondwritingwords—theycreatedthefirstautomatedrefactoringtool,theRefactoringBrowser,forrefactoringSmalltalkprograms.

Manypeoplehaveadvancedthefieldofrefactoringsincethefirsteditionofthisbook.Inparticular,theworkofthosewhohaveaddedautomatedrefactoringstodevelopmenttoolshavecontributedenormouslytomakingprogrammers’liveseasier.It’seasyformetotakeitforgrantedthatIcanrenameawidelyusedfunctionwithasimplekeysequence—butthateasereliesontheeffortsofIDEteamswhoseworkhelpsusall.

Acknowledgments

Chapter1Refactoring:AFirstExampleHowdoIbegintotalkaboutrefactoring?Thetraditionalwayisbyintroducingthehistoryofthesubject,broadprinciples,andthelike.Whensomebodydoesthatataconference,Igetslightlysleepy.Mymindstartswandering,withalow-prioritybackgroundprocesspollingthespeakeruntiltheygiveanexample.

TheexampleswakemeupbecauseIcanseewhatisgoingon.Withprinciples,itistooeasytomakebroadgeneralizations—andtoohardtofigureouthowtoapplythings.Anexamplehelpsmakethingsclear.

SoI’mgoingtostartthisbookwithanexampleofrefactoring.I’lltalkabouthowrefactoringworksandwillgiveyouasenseoftherefactoringprocess.Icanthendotheusualprinciples-styleintroductioninthenextchapter.

Withanyintroductoryexample,however,Irunintoaproblem.IfIpickalargeprogram,describingitandhowitisrefactoredistoocomplicatedforamortalreadertoworkthrough.(Itriedthiswiththeoriginalbook—andendedupthrowingawaytwoexamples,whichwerestillprettysmallbuttookoverahundredpageseachtodescribe.)However,ifIpickaprogramthatissmallenoughtobecomprehensible,refactoringdoesnotlooklikeitisworthwhile.

I’mthusintheclassicbindofanyonewhowantstodescribetechniquesthatareusefulforreal-worldprograms.Frankly,itisnotworththeefforttodoalltherefactoringthatI’mgoingtoshowyouonthesmallprogramIwillbeusing.ButifthecodeI’mshowingyouispartofalargersystem,thentherefactoringbecomesimportant.Justlookatmyexampleandimagineitinthecontextofamuchlargersystem.

TheStartingPoint

Inthefirsteditionofthisbook,mystartingprogramprintedabillfromavideorentalstore,whichmaynowleadmanyofyoutoask:“What’savideorentalstore?”Ratherthananswerthatquestion,I’vere-skinnedtheexampletosomethingthatisbotholderandstillcurrent.

Imageacompanyoftheatricalplayerswhogoouttovariouseventsperformingplays.Typically,acustomerwillrequestafewplaysandthecompanychargesthembasedonthesizeoftheaudienceandthekindofplaytheyperform.Therearecurrentlytwokindsofplaysthatcompanyperforms:tragediesandcomedies.Aswellasprovidingabillfortheperformance,thecompanygivesitscustomers“volumecredits”whichtheycanusefordiscountsonfutureperformances—thinkofitasacustomerloyaltymechanism.

TheperformersstoredataabouttheirplaysinasimpleJSONfilethatlookssomethinglikethis:

plays.json…

{

"hamlet":{"name":"Hamlet","type":"tragedy"},

"as-like":{"name":"AsYouLikeIt","type":"comedy"},

"othello":{"name":"Othello","type":"tragedy"}

}

ThedatafortheirbillsalsocomesinaJSONfile:

invoices.json…

[

{

"customer":"BigCo",

"performances":[

{

"playID":"hamlet",

"audience":55

},

{

"playID":"as-like",

"audience":35

},

{

"playID":"othello",

"audience":40

}

]

}

]

Thecodethatprintsthebillisthissimplefunction.

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

constplay=plays[perf.playID];

letthisAmount=0;

switch(play.type){

case"tragedy":

thisAmount=40000;

if(perf.audience>30){

thisAmount+=1000*(perf.audience-30);

}

break;

case"comedy":

thisAmount=30000;

if(perf.audience>20){

thisAmount+=10000+500*(perf.audience-20);

}

thisAmount+=300*perf.audience;

break;

default:

thrownewError(`unknowntype:${play.type}`);

}

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

}

Runningthatcodeonthetestdatafilesaboveresultsinthefollowingoutput.

StatementforBigCo

Hamlet:$650.00(55seats)

AsYouLikeIt:$580.00(35seats)

Othello:$500.00(40seats)

Amountowedis$1,730.00

Youearned47credits

CommentsontheStartingProgram

Whatareyourthoughtsonthedesignofthisprogram?ThefirstthingI’dsayisthatit’stolerableasitis—aprogramsoshortdoesn’trequireanydeepstructuretobecomprehensible.ButremembermyearlierpointthatIhavetokeepexamplessmall.Imaginethisprogramonalargerscale—perhapshundredsoflineslong.Atthatsize,asingleinlinefunctionishardtounderstand.

Giventhattheprogramworks,isn’tanystatementaboutitsstructuremerelyanaestheticjudgment,adislikeof“ugly”code?Afterall,thecompilerdoesn’tcarewhetherthecodeisuglyorclean.ButwhenIchangethesystem,thereisahumaninvolved,andhumansdocare.Apoorlydesignedsystemishardtochange—becauseitisdifficulttofigureoutwhattochangeandhowthesechangeswillinteractwiththeexistingcodetogetthebehaviorIwant.Andifitishardtofigureoutwhattochange,thereisagoodchancethatIwillmakemistakesandintroducebugs.

Thus,ifI’mfacedwithmodifyingaprogramwithhundredsoflinesofcode,I’dratheritbestructuredintoasetoffunctionsandotherprogramelementsthatallowmetounderstandmoreeasilywhattheprogramisdoing.Iftheprogramlacksstructure,it’susuallyeasierformetoaddstructuretotheprogramfirst,andthenmakethechangeIneed.

Whenyouhavetoaddafeaturetoaprogrambutthecodeisnotstructuredinaconvenientway,firstrefactortheprogramtomakeiteasytoaddthefeature,thenaddthefeature.

Inthiscase,Ihaveacoupleofchangesthattheuserswouldliketomake.First,theywantastatementprintedinHTML.Considerwhatimpactthischangewouldhave.I’mfacedwithaddingconditionalstatementsaroundeverystatementthataddsastringtotheresult.Thatwilladdahostofcomplexitytothefunction.Facedwiththat,mostpeopleprefertocopythemethodandchangeittoemitHTML.Makingacopymaynotseemtooonerousatask,butitsetsupallsortsofproblemsforthefuture.Anychangestothecharginglogicwouldforcemetoupdatebothmethods—andtoensuretheyareupdatedconsistently.IfI’mwritingaprogramthatwillneverchangeagain,thiskindofcopy-and-

pasteisfine.Butifit’salong-livedprogram,thenduplicationisamenace.

Thisbringsmetoasecondchange.Theplayersarelookingtoperformmorekindsofplays:theyhopetoaddhistory,pastoral,pastoral-comical,historical-pastoral,tragical-historical,tragical-comical-historical-pastoral,sceneindividable,andpoemunlimitedtotheirrepertoire.Theyhaven’texactlydecidedyetwhattheywanttodoandwhen.Thischangewillaffectboththewaytheirplaysarechargedforandthewayvolumecreditsarecalculated.AsanexperienceddeveloperIcanbesurethatwhateverschemetheycomeupwith,theywillchangeitagainwithinsixmonths.Afterall,whenfeaturerequestscome,theycomenotassinglespiesbutinbattalions.

Again,thatstatementmethodiswherethechangesneedtobemadetodealwithchangesinclassificationandchargingrules.ButifIcopystatementtohtmlStatement,I’dneedtoensurethatanychangesareconsistent.Furthermore,astherulesgrowincomplexity,it’sgoingtobehardertofigureoutwheretomakethechangesandhardertodothemwithoutmakingamistake.

Letmestressthatit’sthesechangesthatdrivetheneedtoperformrefactoring.Ifthecodeworksanddoesn’teverneedtochange,it’sperfectlyfinetoleaveitalone.Itwouldbenicetoimproveit,butunlesssomeoneneedstounderstandit,itisn’tcausinganyrealharm.Yetassoonassomeonedoesneedtounderstandhowthatcodeworks,andstrugglestofollowit,thenyouhavetodosomethingaboutit.

TheFirstStepinRefactoring

WheneverIdorefactoring,thefirststepisalwaysthesame.IneedtoensureIhaveasolidsetoftestsforthatsectionofcode.ThetestsareessentialbecauseeventhoughIwillfollowrefactoringsstructuredtoavoidmostoftheopportunitiesforintroducingbugs,I’mstillhumanandstillmakemistakes.Thelargeraprogram,themorelikelyitisthatmychangeswillcausesomethingtobreakinadvertently—inthedigitalage,frailty’snameissoftware.

Sincethestatementreturnsastring,whatIdoiscreateafewinvoices,giveeachinvoiceafewperformancesofvariouskindsofplays,andgeneratethestatementstrings.IthendoastringcomparisonbetweenthenewstringandsomereferencestringsthatIhavehand-checked.IsetupallofthesetestsusingatestingframeworksoIcanrunthemwithjustasimplekeystrokeinmy

developmentenvironment.Theteststakeonlyafewsecondstorun,andasyouwillsee,Irunthemoften.

Animportantpartofthetestsisthewaytheyreporttheirresults.Theyeithergogreen,meaningthatallthestringsareidenticaltothereferencestrings,orred,showingalistoffailures—thelinesthatturnedoutdifferently.Thetestsarethusself-checking.Itisvitaltomaketestsself-checking.IfIdon’t,I’dendupspendingtimehand-checkingvaluesfromthetestagainstvaluesonadeskpad,andthatwouldslowmedown.Moderntestingframeworksprovideallthefeaturesneededtowriteandrunself-checkingtests.

Beforeyoustartrefactoring,makesureyouhaveasolidsuiteoftests.Thesetestsmustbeself-checking.

AsIdotherefactoring,I’llleanonthetests.Ithinkofthemasabugdetectortoprotectmeagainstmyownmistakes.BywritingwhatIwanttwice,inthecodeandinthetest,Ihavetomakethemistakeconsistentlyinbothplacestofoolthedetector.Bydouble-checkingmywork,Ireducethechanceofdoingsomethingwrong.Althoughittakestimetobuildthetests,Iendupsavingthattime,withconsiderableinterest,byspendinglesstimedebugging.ThisissuchanimportantpartofrefactoringthatIdevoteafullchaptertoit(BuildingTests,p.85).

DecomposingthestatementFunction

Whenrefactoringalongfunctionlikethis,Imentallytrytoidentifypointsthatseparatedifferentpartsoftheoverallbehavior.Thefirstchunkthatleapstomyeyeistheswitchstatementinthemiddle.

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

constplay=plays[perf.playID];

letthisAmount=0;

switch(play.type){

case"tragedy":

thisAmount=40000;

if(perf.audience>30){

thisAmount+=1000*(perf.audience-30);

}

break;

case"comedy":

thisAmount=30000;

if(perf.audience>20){

thisAmount+=10000+500*(perf.audience-20);

}

thisAmount+=300*perf.audience;

break;

default:

thrownewError(`unknowntype:${play.type}`);

}

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

}

AsIlookatthischunk,Iconcludethatit’scalculatingthechargeforoneperformance.Thatconclusionisapieceofinsightaboutthecode.ButasWardCunninghamputsit,thisunderstandingisinmyhead—anotoriouslyvolatileformofstorage.Ineedtopersistitbymovingitfrommyheadbackintothecodeitself.Thatway,shouldIcomebacktoitlater,thecodewilltellmewhatit’sdoing—Idon’thavetofigureitoutagain.

Thewaytoputthatunderstandingintocodeistoturnthatchunkofcodeintoitsownfunction,namingitafterwhatitdoes—somethinglikeamountFor(aPerformance).WhenIwanttoturnachunkofcodeintoafunctionlikethis,Ihaveaprocedurefordoingitthatminimizesmychancesofgettingitwrong.Iwrotedownthisprocedureand,tomakeiteasytoreference,nameditExtractFunction(106).

First,IneedtolookinthefragmentforanyvariablesthatwillnolongerbeinscopeonceI’veextractedthecodeintoitsownfunction.Inthiscase,Ihave

three:perf,play,andthisAmount.Thefirsttwoareusedbytheextractedcode,butnotmodified,soIcanpasstheminasparameters.Modifiedvariablesneedmorecare.Here,thereisonlyone,soIcanreturnit.Icanalsobringitsinitializationinsidetheextractedcode.Allofwhichyieldsthis:

functionstatement…

functionamountFor(perf,play){

letthisAmount=0;

switch(play.type){

case"tragedy":

thisAmount=40000;

if(perf.audience>30){

thisAmount+=1000*(perf.audience-30);

}

break;

case"comedy":

thisAmount=30000;

if(perf.audience>20){

thisAmount+=10000+500*(perf.audience-20);

}

thisAmount+=300*perf.audience;

break;

default:

thrownewError(`unknowntype:${play.type}`);

}

returnthisAmount;

}

WhenIuseaheaderlike“functionsomeName…”initalicsforsomecode,thatmeansthatthefollowingcodeiswithinthescopeofthefunction,file,orclassnamedintheheader.ThereisusuallyothercodewithinthatscopethatIwon’tshow,asI’mnotdiscussingitatthemoment.

TheoriginalstatementcodenowcallsthisfunctiontopopulatethisAmount:

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

constplay=plays[perf.playID];

letthisAmount=amountFor(perf,play);

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

OnceI’vemadethischange,IimmediatelycompileandtesttoseeifI’vebrokenanything.It’sanimportanthabittotestaftereveryrefactoring,howeversimple.Mistakesareeasytomake—atleast,Ifindthemeasytomake.TestingaftereachchangemeansthatwhenImakeamistake,Ionlyhaveasmallchangetoconsiderinordertospottheerror,whichmakesitfareasiertofindandfix.Thisistheessenceoftherefactoringprocess:smallchangesandtestingaftereachchange.IfItrytodotoomuch,makingamistakewillforcemeintoatrickydebuggingepisodethatcantakealongtime.Smallchanges,enablingatightfeedbackloop,arethekeytoavoidingthatmess.

IusecompileheretomeandoingwhateverisneededtomaketheJavaScriptexecutable.SinceJavaScriptisdirectlyexecutable,thatmaymeannothing,butinothercasesitmaymeanmovingcodetoanoutputdirectoryand/orusingaprocessorsuchasBabel[bib-babel].

Refactoringchangestheprogramsinsmallsteps,soifyoumakeamistake,itiseasytofindwherethebugis.

ThisbeingJavaScript,IcanextractamountForintoanestedfunctionofstatement.ThisishelpfulasitmeansIdon’thavetopassdatathat’sinsidethescopeofthecontainingfunctiontothenewlyextractedfunction.Thatdoesn’tmakeadifferenceinthiscase,butit’sonelessissuetodealwith.

Inthiscasethetestspassed,somynextstepistocommitthechangetomylocalversioncontrolsystem.Iuseaversioncontrolsystem,suchasgitormercurial,thatallowsmetomakeprivatecommits.Icommitaftereachsuccessfulrefactoring,soIcaneasilygetbacktoaworkingstateshouldImessuplater.I

thensquashchangesintomoresignificantcommitsbeforeIpushthechangestoasharedrepository.

ExtractFunction(106)isacommonrefactoringtoautomate.IfIwasprogramminginJava,IwouldhaveinstinctivelyreachedforthekeysequenceformyIDEtoperformthisrefactoring.AsIwritethis,thereisnosuchrobustsupportforthisrefactoringinJavaScripttools,soIhavetodothismanually.It’snothard,althoughIhavetobecarefulwiththoselocallyscopedvariables.

OnceI’veusedExtractFunction(106),ItakealookatwhatI’veextractedtoseeifthereareanyquickandeasythingsIcandotoclarifytheextractedfunction.ThefirstthingIdoisrenamesomeofthevariablestomakethemclearer,suchaschangingthisAmounttoresult

functionstatement…

functionamountFor(perf,play){

letresult=0;

switch(play.type){

case"tragedy":

result=40000;

if(perf.audience>30){

result+=1000*(perf.audience-30);

}

break;

case"comedy":

result=30000;

if(perf.audience>20){

result+=10000+500*(perf.audience-20);

}

result+=300*perf.audience;

break;

default:

thrownewError(`unknowntype:${play.type}`);

}

returnresult;

}

It’smycodingstandardtoalwayscallthereturnvaluefromafunction“result”.ThatwayIalwaysknowitsrole.Again,Icompile,test,andcommit.ThenImoveontothefirstargument.

functionstatement…

functionamountFor(aPerformance,play){

letresult=0;

switch(play.type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${play.type}`);

}

returnresult;

}

Again,thisisfollowingmycodingstyle.WithadynamicallytypedlanguagesuchasJavaScript,it’susefultokeeptrackoftypes—hence,mydefaultnameforaparameterincludesthetypename.Iuseanindefinitearticlewithitunlessthereissomespecificroleinformationtocaptureinthename.IlearnedthisconventionfromKentBeck[bib-beck-sbpp]andcontinuetofindithelpful.

Anyfoolcanwritecodethatacomputercanunderstand.Goodprogrammerswritecodethathumanscanunderstand.

Isthisrenamingworththeeffort?Absolutely.Goodcodeshouldclearlycommunicatewhatitisdoing,andvariablenamesareakeytoclearcode.Neverbeafraidtochangenamestoimproveclarity.Withgoodfind-and-replacetools,itisusuallynotdifficult;testing,andstatictypinginalanguagethatsupportsit,willhighlightanyoccurrencesyoumiss.Andwithautomatedrefactoringtools,it’strivialtorenameevenwidelyusedfunctions.

Thenextitemtoconsiderforrenamingistheplayparameter,butIhaveadifferentfateforthat.

RemovingtheplayVariable

AsIconsidertheparameterstoamountFor,Ilooktoseewheretheycomefrom.aPerformancecomesfromtheloopvariable,sonaturallychangeswitheachiterationthroughtheloop.Butplayiscomputedfromtheperformance,sothere’snoneedtopassitinasaparameteratall—IcanjustrecalculateitwithinamountFor.WhenI’mbreakingdownalongfunction,Iliketogetridofvariableslikeplay,becausetemporaryvariablescreatealotoflocallyscopednamesthatcomplicateextractions.TherefactoringIwillusehereisReplaceTempwithQuery(176).

Ibeginbyextractingtheright-handsideoftheassignmentintoafunction.

functionstatement…

functionplayFor(aPerformance){

returnplays[aPerformance.playID];

}

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

constplay=playFor(perf);

letthisAmount=amountFor(perf,play);

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===play.type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${play.name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Icompile-test-commit,andthenuseInlineVariable(123).

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

constplay=playFor(perf);

letthisAmount=amountFor(perf,playFor(perf));

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Icompile-test-commit.Withthatinlined,IcanthenapplyChangeFunctionDeclaration(124)toamountFortoremovetheplayparameter.Idothisintwosteps.First,IusethenewfunctioninsideamountFor.

functionstatement…

functionamountFor(aPerformance,play){

letresult=0;

switch(playFor(aPerformance).type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${playFor(aPerformance).type}`);

}

returnresult;

}

Icompile-test-commit,andthendeletetheparameter.

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

letthisAmount=amountFor(perf,playFor(perf));

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(thisAmount/100)}(${perf.audience}seats)\n`;

totalAmount+=thisAmount;

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

functionstatement…

functionamountFor(aPerformance,play){

letresult=0;

switch(playFor(aPerformance).type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${playFor(aPerformance).type}`);

}

returnresult;

}

Andcompile-test-commitagain.

Thisrefactoringalarmssomeprogrammers.Previously,thecodetolookuptheplaywasexecutedonceineachloopiteration;now,it’sexecutedthrice.I’lltalkabouttheinterplayofrefactoringandperformancelater,butforthemomentI’lljustobservethatthischangeisunlikelytosignificantlyaffectperformance,andevenifitwere,itismucheasiertoimprovetheperformanceofawell-factoredcodebase.

Thegreatbenefitofremovinglocalvariablesisthatitmakesitmucheasiertodoextractions,sincethereislesslocalscopetodealwith.Indeed,usuallyI’lltakeoutlocalvariablesbeforeIdoanyextractions.

NowthatI’mdonewiththeargumentstoamountFor,Ilookbackatwhereit’scalled.It’sbeingusedtosetatemporaryvariablethat’snotupdatedagain,soIinlinethatvariable.

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

ExtractingVolumeCredits

Here’sthecurrentstateofthestatementfunctionbody.

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

//addvolumecredits

volumeCredits+=Math.max(perf.audience-30,0);

//addextracreditforeverytencomedyattendees

if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

NowIgetthebenefitfromremovingtheplayvariableasitmakesiteasiertoextractthevolumecreditscalculationbyremovingoneofthelocallyscopedvariables.

Istillhavetodealwiththeothertwo.Again,perfiseasytopassin,butvolumeCreditsisabitmoretrickyasitisanaccumulatorupdatedineachpassoftheloop.Somybestbetistoinitializeashadowofitinsidetheextractedfunctionandreturnit.

functionstatement…

functionvolumeCreditsFor(perf){

letvolumeCredits=0;

volumeCredits+=Math.max(perf.audience-30,0);

if("comedy"===playFor(perf).type)volumeCredits+=Math.floor(perf.audience/5);

returnvolumeCredits;

}

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Iremovetheunnecessary(and,inthiscase,downrightmisleading)comment.

Icompile-test-committhat,andthenrenamethevariablesinsidethenewfunction.

functionstatement…

functionvolumeCreditsFor(aPerformance){

letresult=0;

result+=Math.max(aPerformance.audience-30,0);

if("comedy"===playFor(aPerformance).type)result+=Math.floor(aPerformance.audience/5);

returnresult;

}

I’veshownitinonestep,butasbeforeIdidtherenamesoneatatime,withacompile-test-commitaftereach.

RemovingtheformatVariable

Let’slookatthemainstatementmethodagain:

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

constformat=newIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

AsIsuggestedbefore,temporaryvariablescanbeaproblem.Theyareonlyusefulwithintheirownroutine,andthereforetheyencouragelong,complexroutines.Mynextmove,then,istoreplacesomeofthem.Theeasiestoneisformat.Thisisacaseofassigningafunctiontoatemp,whichIprefertoreplacewithadeclaredfunction.

functionstatement…

functionformat(aNumber){

returnnewIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format(aNumber);

}

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

//printlineforthisorder

result+=`${playFor(perf).name}:${format(amountFor(perf)/100)}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${format(totalAmount/100)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Althoughchangingafunctionvariabletoadeclaredfunctionisarefactoring,Ihaven’tnameditandincludeditinthecatalog.TherearemanyrefactoringsthatIdidn’tfeelimportantenoughforthat.Thisoneisbothsimpletodoandrelativelyrare,soIdidn’tthinkitwasworthwhile.

I’mnotkeenonthename—“format”doesn’treallyconveyenoughofwhatit’sdoing.“formatAsUSD”wouldbeabittoolong-windedsinceit’sbeingusedinastringtemplate,particularlywithinthissmallscope.Ithinkthefactthatit’sformattingacurrencyamountisthethingtohighlighthere,soIpickanamethatsuggeststhatandapplyChangeFunctionDeclaration(124)

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

//printlineforthisorder

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

functionstatement…

functionusd(aNumber){

returnnewIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format(aNumber/100

}

Namingisbothimportantandtricky.Breakingalargefunctionintosmalleronesonlyaddsvalueifthenamesaregood.Withgoodnames,Idon’thavetoreadthebodyofthefunctiontoseewhatitdoes.Butit’shardtogetnamesrightthefirsttime,soIusethebestnameIcanthinkofforthemoment,anddon’thesitatetorenameitlater.Often,ittakesasecondpassthroughsomecodetorealizewhatthebestnamereallyis.

AsI’mchangingthename,Ialsomovetheduplicateddivisionby100intothefunction.Storingmoneyasintegercentsisacommonapproach—itavoidsthedangersofstoringfractionalmonetaryvaluesasfloatsbutallowsmetousearithmeticoperators.WheneverIwanttodisplaysuchapenny-integernumber,however,Ineedadecimal,somyformattingfunctionshouldtakecareofthedivision.

RemovingTotalVolumeCredits

MynexttargetvariableisvolumeCredits.Thisisatrickiercase,asit’sbuiltupduringtheiterationsoftheloop.Myfirstmove,then,istouseSplitLoop(226)toseparatetheaccumulationofvolumeCredits.

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letvolumeCredits=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

//printlineforthisorder

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

}

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Withthatdone,IcanuseSlideStatements(221)tomovethedeclarationofthe

variablenexttotheloop.

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

//printlineforthisorder

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

letvolumeCredits=0;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

}

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

GatheringtogethereverythingthatupdatesthevolumeCreditsvariablemakesiteasiertodoReplaceTempwithQuery(176).Asbefore,thefirststepistoapplyExtractFunction(106)totheoverallcalculationofthevariable.

functionstatement…

functiontotalVolumeCredits(){

letvolumeCredits=0;

for(letperfofinvoice.performances){

volumeCredits+=volumeCreditsFor(perf);

}

returnvolumeCredits;

}

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

//printlineforthisorder

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

letvolumeCredits=totalVolumeCredits();

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${volumeCredits}credits\n`;

returnresult;

Onceeverythingisextracted,IcanapplyInlineVariable(123):

toplevel…

functionstatement(invoice,plays){

lettotalAmount=0;

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

//printlineforthisorder

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

totalAmount+=amountFor(perf);

}

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

LetmepauseforabittotalkaboutwhatI’vejustdonehere.Firstly,Iknowreaderswillagainbeworryingaboutperformancewiththischange,asmanypeoplearewaryofrepeatingaloop.Butmostofthetime,re-runningalooplikethishasanegligibleeffectonperformance.Ifyoutimedthecodebeforeandafterthisrefactoring,youwouldprobablynotnoticeanysignificantchangeinspeed—andthat’susuallythecase.Mostprogrammers,evenexperiencedones,arepoorjudgesofhowcodeactuallyperforms.Manyofourintuitionsarebrokenbyclevercompilers,moderncachingtechniques,andthelike.Theperformanceofsoftwareusuallydependsonjustafewpartsofthecode,andchangesanywhereelsedon’tmakeanappreciabledifference.

But“mostly”isn’tthesameas“alwaysly.”Sometimesarefactoringwillhaveasignificantperformanceimplication.Eventhen,Iusuallygoaheadanddoit,becauseit’smucheasiertotunetheperformanceofwell-factoredcode.IfIintroduceasignificantperformanceissueduringrefactoring,Ispendtimeonperformance-tuningafterwards.ItmaybethatthisleadstoreversingsomeoftherefactoringIdidearlier—butmostofthetime,duetotherefactoring,Icanapplyamoreeffectiveperformance-tuningenhancementinstead.Iendupwithcodethat’sbothclearerandfaster.

So,myoveralladviceonperformancewithrefactoringis:Mostofthetimeyoushouldignoreit.Ifyourrefactoringintroducesperformanceslow-downs,finishrefactoringfirstanddoperformancetuningafterwards.

ThesecondaspectIwanttocallyourattentiontoishowsmallthestepsweretoremovevolumeCredits.Herearethefoursteps,eachfollowedbycompiling,testing,andcommittingtomylocalsourcecoderepository:

SplitLoop(226)toisolatetheaccumulation

SlideStatements(221)tobringtheinitializingcodenexttotheaccumulation

ExtractFunction(106)tocreateafunctionforcalculatingthetotal

InlineVariable(123)toremovethevariablecompletely

IconfessIdon’talwaystakequiteasshortstepsasthese—butwheneverthingsgetdifficult,myfirstreactionistotakeshortersteps.Inparticular,shouldatestfailduringarefactoring,ifIcan’timmediatelyseeandfixtheproblem,I’llreverttomylastgoodcommitandredowhatIjustdidwithsmallersteps.ThatworksbecauseIcommitsofrequentlyandbecausesmallstepsarethekeytomovingquickly,particularlywhenworkingwithdifficultcode.

IthenrepeatthatsequencetoremovetotalAmount.Istartbysplittingtheloop(compile-test-commit),thenIslidethevariableinitialization(compile-test-commit),andthenIextractthefunction.Thereisawrinklehere:Thebestnameforthefunctionis“totalAmount”,butthat’sthenameofthevariable,andIcan’thavebothatthesametime.SoIgivethenewfunctionarandomnamewhenIextractit(andcompile-test-commit)

functionstatement…

functionappleSauce(){

lettotalAmount=0;

for(letperfofinvoice.performances){

totalAmount+=amountFor(perf);

}

returntotalAmount;

}

toplevel…

functionstatement(invoice,plays){

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

lettotalAmount=appleSauce();

result+=`Amountowedis${usd(totalAmount)}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

ThenIinlinethevariable(compile-test-commit)andrenamethefunctiontosomethingmoresensible(compile-test-commit).

toplevel…

functionstatement(invoice,plays){

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functionstatement…

functiontotalAmount(){

lettotalAmount=0;

for(letperfofinvoice.performances){

totalAmount+=amountFor(perf);

}

returntotalAmount;

}

Ialsotaketheopportunitytochangethenamesinsidemyextractedfunctionstoadheretomyconvention.

functionstatement…

functiontotalAmount(){

letresult=0;

for(letperfofinvoice.performances){

result+=amountFor(perf);

}

returnresult;

}

functiontotalVolumeCredits(){

letresult=0;

for(letperfofinvoice.performances){

result+=volumeCreditsFor(perf);

}

returnresult;

}

Status:LotsofNestedFunctions

Nowisagoodtimetopauseandtakealookattheoverallstateofthecode:

functionstatement(invoice,plays){

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functiontotalAmount(){

letresult=0;

for(letperfofinvoice.performances){

result+=amountFor(perf);

}

returnresult;

}

functiontotalVolumeCredits(){

letresult=0;

for(letperfofinvoice.performances){

result+=volumeCreditsFor(perf);

}

returnresult;

}

functionusd(aNumber){

returnnewIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format(aNumber/100);

}

functionvolumeCreditsFor(aPerformance){

letresult=0;

result+=Math.max(aPerformance.audience-30,0);

if("comedy"===playFor(aPerformance).type)result+=Math.floor(aPerformance.audience/5);

returnresult;

}

functionplayFor(aPerformance){

returnplays[aPerformance.playID];

}

functionamountFor(aPerformance){

letresult=0;

switch(playFor(aPerformance).type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${playFor(aPerformance).type}`);

}

returnresult;

}

}

Thestructureofthecodeismuchbetternow.Thetop-levelstatementfunctionisnowjustsevenlinesofcode,andallitdoesislayingouttheprintingofthestatement.Allthecalculationlogichasbeenmovedouttoahandfulofsupportingfunctions.Thismakesiteasiertounderstandeachindividualcalculationaswellastheoverallflowofthereport.

SplittingthePhasesofCalculationandFormatting

Sofar,myrefactoringhasfocusedonaddingenoughstructuretothefunctionsothatIcanunderstanditandseeitintermsofitslogicalparts.Thisisoftenthecaseearlyinrefactoring.Breakingdowncomplicatedchunksintosmallpiecesisimportant,asisnamingthingswell.Now,IcanbegintofocusmoreonthefunctionalitychangeIwanttomake—specifically,providinganHTMLversionofthisstatement.Inmanyways,it’snowmucheasiertodo.Withallthecalculationcodesplitout,allIhavetodoiswriteanHTMLversionofthesevenlinesofcodeatthetop.Theproblemisthatthesebroken-outfunctionsarenestedwithinthetextualstatementmethod,andIdon’twanttocopyandpastethemintoanewfunction,howeverwellorganized.IwantthesamecalculationfunctionstobeusedbythetextandHTMLversionsofthestatement.

Therearevariouswaystodothis,butoneofmyfavoritetechniquesisSplitPhase(154).Myaimhereistodividethelogicintotwoparts:onethatcalculatesthedatarequiredforthestatement,theotherthatrendersitintotextorHTML.Thefirstphasecreatesanintermediatedatastructurethatitpassestothesecond.

IstartaSplitPhase(154)byapplyingExtractFunction(106)tothecodethatmakesupthesecondphase.Inthiscase,that’sthestatementprintingcode,whichisinfacttheentirecontentofstatement.This,togetherwithallthenestedfunctions,goesintoitsowntop-levelfunctionwhichIcallrenderPlainText.

functionstatement(invoice,plays){

returnrenderPlainText(invoice,plays);

}

functionrenderPlainText(invoice,plays){

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functiontotalAmount(){…}

functiontotalVolumeCredits(){…}

functionusd(aNumber){…}

functionvolumeCreditsFor(aPerformance){…}

functionplayFor(aPerformance){…}

functionamountFor(aPerformance){…}

Idomyusualcompile-test-commit,thencreateanobjectthatwillactasmyintermediatedatastructurebetweenthetwophases.IpassthisdataobjectinasanargumenttorenderPlainText(compile-test-commit).

functionstatement(invoice,plays){

conststatementData={};

returnrenderPlainText(statementData,invoice,plays);

}

functionrenderPlainText(data,invoice,plays){

letresult=`Statementfor${invoice.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functiontotalAmount(){…}

functiontotalVolumeCredits(){…}

functionusd(aNumber){…}

functionvolumeCreditsFor(aPerformance){…}

functionplayFor(aPerformance){…}

functionamountFor(aPerformance){…}

InowexaminetheotherargumentsusedbyrenderPlainText.Iwanttomovethedatathatcomesfromthemintotheintermediatedatastructure,sothatallthecalculationcodemovesintothestatementfunctionandrenderPlainTextoperatessolelyondatapassedtoitthroughthedataparameter.

Myfirstmoveistotakethecustomerandaddittotheintermediateobject(compile-test-commit).

functionstatement(invoice,plays){

conststatementData={};

statementData.customer=invoice.customer;

returnrenderPlainText(statementData,invoice,plays);

}

}

functionrenderPlainText(data,invoice,plays){

letresult=`Statementfor${data.customer}\n`;

for(letperfofinvoice.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

Similarly,Iaddtheperformances,whichallowsmetodeletetheinvoiceparametertorenderPlainText(compile-test-commit).

functionstatement(invoice,plays){

conststatementData={};

statementData.customer=invoice.customer;

statementData.performances=invoice.performances;

returnrenderPlainText(statementData,invoice,plays);

}

functionrenderPlainText(data,invoice,plays){

letresult=`Statementfor${data.customer}\n`;

for(letperfofdata.performances){

result+=`${playFor(perf).name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

NowI’dliketheplaynametocomefromtheintermediatedata.Todothis,Ineedtoenrichtheperformancerecordwithdatafromtheplay(compile-test-commit).

functionstatement(invoice,plays){

conststatementData={};

statementData.customer=invoice.customer;

statementData.performances=invoice.performances.map(enrichPerformance);

returnrenderPlainText(statementData,plays);

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

returnresult;

}

Atthemoment,I’mjustmakingacopyoftheperformanceobject,butI’llshortlyadddatatothisnewrecord.ItakeacopybecauseIdon’twanttomodifythedatapassedintothefunction.IprefertotreatdataasimmutableasmuchasIcan—mutablestatequicklybecomessomethingrotten.

Theidiomresult=Object.assign({},aPerformance)looksveryoddtopeopleunfamiliartoJavaScript.Itperformsashallowcopy.I’dprefertohaveafunctionforthis,butit’soneofthosecaseswheretheidiomissobakedintoJavaScriptusagethatwritingmyownfunctionwouldlookoutofplaceforJavaScriptprogrammers.

NowIhaveaspotfortheplay,Ineedtoaddit.Todothat,IneedtoapplyMoveFunction(196)toplayForandstatement(compile-test-commit).

functionstatement…

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

result.play=playFor(aPerformance);

returnresult;

}

functionplayFor(aPerformance){

returnplays[aPerformance.playID];

}

IthenreplaceallthereferencestoplayForinrenderPlainTexttousethedatainstead(compile-test-commit).

functionrenderPlainText…

letresult=`Statementfor${data.customer}\n`;

for(letperfofdata.performances){

result+=`${perf.play.name}:${usd(amountFor(perf))}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functionvolumeCreditsFor(aPerformance){

letresult=0;

result+=Math.max(aPerformance.audience-30,0);

if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);

returnresult;

}

functionamountFor(aPerformance){

letresult=0;

switch(aPerformance.play.type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${aPerformance.play.type}`);

}

returnresult;

}

IthenmoveamountForinasimilarway(compile-test-commit).

functionstatement…

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

result.play=playFor(result);

result.amount=amountFor(result);

returnresult;

}

functionamountFor(aPerformance){…}

functionrenderPlainText…

letresult=`Statementfor${data.customer}\n`;

for(letperfofdata.performances){

result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(totalAmount())}\n`;

result+=`Youearned${totalVolumeCredits()}credits\n`;

returnresult;

functiontotalAmount(){

letresult=0;

for(letperfofdata.performances){

result+=perf.amount;

}

returnresult;

}

Next,Imovethevolumecreditscalculation(compile-test-commit).

functionstatement…

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

result.play=playFor(result);

result.amount=amountFor(result);

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

functionvolumeCreditsFor(aPerformance){…}

functionrenderPlainText…

functiontotalVolumeCredits(){

letresult=0;

for(letperfofdata.performances){

result+=perf.volumeCredits;

}

returnresult;

}

Finally,Imovethetwocalculationsofthetotals.

functionstatement…

conststatementData={};

statementData.customer=invoice.customer;

statementData.performances=invoice.performances.map(enrichPerformance);

statementData.totalAmount=totalAmount(statementData);

statementData.totalVolumeCredits=totalVolumeCredits(statementData);

returnrenderPlainText(statementData,plays);

functiontotalAmount(data){…}

functiontotalVolumeCredits(data){…}

functionrenderPlainText…

letresult=`Statementfor${data.customer}\n`;

for(letperfofdata.performances){

result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(data.totalAmount)}\n`;

result+=`Youearned${data.totalVolumeCredits}credits\n`;

returnresult;

AlthoughIcouldhavemodifiedthebodiesofthesetotalsfunctionstousethestatementDatavariable(asit’swithinscope),Iprefertopasstheexplicitparameter.

And,onceI’mdonewithcompile-test-commitafterthemove,Ican’tresistacouplequickshotsofReplaceLoopwithPipeline(230).

functionrenderPlainText…

functiontotalAmount(data){

returndata.performances

.reduce((total,p)=>total+p.amount,0);

}

functiontotalVolumeCredits(data){

returndata.performances

.reduce((total,p)=>total+p.volumeCredits,0);

}

Inowextractallthefirst-phasecodeintoitsownfunction(compile-test-commit).

toplevel…

functionstatement(invoice,plays){

returnrenderPlainText(createStatementData(invoice,plays));

}

functioncreateStatementData(invoice,plays){

conststatementData={};

statementData.customer=invoice.customer;

statementData.performances=invoice.performances.map(enrichPerformance);

statementData.totalAmount=totalAmount(statementData);

statementData.totalVolumeCredits=totalVolumeCredits(statementData);

returnstatementData;

Sinceit’sclearlyseparatenow,Imoveittoitsownfile(andalterthenameofthereturnedresulttomatchmyusualconvention).

statement.js…

importcreateStatementDatafrom'./createStatementData.js';

createStatementData.js…

exportdefaultfunctioncreateStatementData(invoice,plays){

constresult={};

result.customer=invoice.customer;

result.performances=invoice.performances.map(enrichPerformance);

result.totalAmount=totalAmount(result);

result.totalVolumeCredits=totalVolumeCredits(result);

returnresult;

functionenrichPerformance(aPerformance){…}

functionplayFor(aPerformance){…}

functionamountFor(aPerformance){…}

functionvolumeCreditsFor(aPerformance){…}

functiontotalAmount(data){…}

functiontotalVolumeCredits(data){…}

Onefinalswingofcompile-test-commit—andnowit’seasytowriteanHTMLversion.

statement.js…

functionhtmlStatement(invoice,plays){

returnrenderHtml(createStatementData(invoice,plays));

}

functionrenderHtml(data){

letresult=`<h1>Statementfor${data.customer}</h1>\n`;

result+="<table>\n";

result+="<tr><th>play</th><th>seats</th><th>cost</th></tr>";

for(letperfofdata.performances){

result+=`<tr><td>${perf.play.name}</td><td>${perf.audience}</td>`;

result+=`<td>${usd(perf.amount)}</td></tr>\n`;

}

result+="</table>\n";

result+=`<p>Amountowedis<em>${usd(data.totalAmount)}</em></p>\n`;

result+=`<p>Youearned<em>${data.totalVolumeCredits}</em>credits</p>\n`;

returnresult;

}

functionusd(aNumber){…}

(Imovedusdtothetoplevel,sothatrenderHtmlcoulduseit.)

Status:SeparatedintoTwoFiles(andPhases)

Thisisagoodmomenttotakestockagainandthinkaboutwherethecodeisnow.Ihavetwofilesofcode.

statement.js

importcreateStatementDatafrom'./createStatementData.js';

functionstatement(invoice,plays){

returnrenderPlainText(createStatementData(invoice,plays));

}

functionrenderPlainText(data,plays){

letresult=`Statementfor${data.customer}\n`;

for(letperfofdata.performances){

result+=`${perf.play.name}:${usd(perf.amount)}(${perf.audience}seats)\n`;

}

result+=`Amountowedis${usd(data.totalAmount)}\n`;

result+=`Youearned${data.totalVolumeCredits}credits\n`;

returnresult;

}

functionhtmlStatement(invoice,plays){

returnrenderHtml(createStatementData(invoice,plays));

}

functionrenderHtml(data){

letresult=`<h1>Statementfor${data.customer}</h1>\n`;

result+="<table>\n";

result+="<tr><th>play</th><th>seats</th><th>cost</th></tr>";

for(letperfofdata.performances){

result+=`<tr><td>${perf.play.name}</td><td>${perf.audience}</td>`;

result+=`<td>${usd(perf.amount)}</td></tr>\n`;

}

result+="</table>\n";

result+=`<p>Amountowedis<em>${usd(data.totalAmount)}</em></p>\n`;

result+=`<p>Youearned<em>${data.totalVolumeCredits}</em>credits</p>\n`;

returnresult;

}

functionusd(aNumber){

returnnewIntl.NumberFormat("en-US",

{style:"currency",currency:"USD",

minimumFractionDigits:2}).format(aNumber/100);

}

createStatementData.js

exportdefaultfunctioncreateStatementData(invoice,plays){

constresult={};

result.customer=invoice.customer;

result.performances=invoice.performances.map(enrichPerformance);

result.totalAmount=totalAmount(result);

result.totalVolumeCredits=totalVolumeCredits(result);

returnresult;

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

result.play=playFor(result);

result.amount=amountFor(result);

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

functionplayFor(aPerformance){

returnplays[aPerformance.playID]

}

functionamountFor(aPerformance){

letresult=0;

switch(aPerformance.play.type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${aPerformance.play.type}`);

}

returnresult;

}

functionvolumeCreditsFor(aPerformance){

letresult=0;

result+=Math.max(aPerformance.audience-30,0);

if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);

returnresult;

}

functiontotalAmount(data){

returndata.performances

.reduce((total,p)=>total+p.amount,0);

}

functiontotalVolumeCredits(data){

returndata.performances

.reduce((total,p)=>total+p.volumeCredits,0);

}

IhavemorecodethanIdidwhenIstarted:70lines(notcountinghtmlStatement)asopposedto44,mostlyduetotheextrawrappinginvolvedinputtingthingsinfunctions.Ifallelseisequal,morecodeisbad—butrarelyisallelseequal.Theextracodebreaksupthelogicintoidentifiableparts,separatingthecalculationsofthestatementsfromthelayout.Thismodularitymakesiteasierformetounderstandthepartsofthecodeandhowtheyfittogether.Brevityisthesoulofwit,butclarityisthesoulofevolvablesoftware.AddingthismodularityallowstometosupporttheHTMLversionofthecodewithoutanyduplicationofthecalculations.

Whenprogramming,followthecampingrule:Alwaysleavethecodebasehealthierthanwhenyoufoundit.

TherearemorethingsIcoulddotosimplifytheprintinglogic,butthiswilldoforthemoment.IalwayshavetostrikeabalancebetweenalltherefactoringsI

coulddoandaddingnewfeatures.Atthemoment,mostpeopleunder-prioritizerefactoring—buttherestillisabalance.Myruleisavariationonthecampingrule:Alwaysleavethecodebasehealthierthanwhenyoufoundit.Itwillneverbeperfect,butitshouldbebetter.

ReorganizingtheCalculationsbyType

NowI’llturnmyattentiontothenextfeaturechange:supportingmorecategoriesofplays,eachwithitsownchargingandvolumecreditscalculations.Atthemoment,tomakechangeshereIhavetogointothecalculationfunctionsandedittheconditionsinthere.ThethisAmountfunctionhighlightsthecentralrolethetypeofplayhasinthechoiceofcalculations—butconditionallogiclikethistendstodecayasfurthermodificationsaremadeunlessit’sreinforcedbymorestructuralelementsoftheprogramminglanguage.

Therearevariouswaystointroducestructuretomakethisexplicit,butinthiscaseanaturalapproachistypepolymorphism—aprominentfeatureofclassicalobject-orientation.ClassicalOOhaslongbeenacontroversialfeatureintheJavaScriptworld,buttheECMAScript2015versionprovidesasoundsyntaxandstructureforit.Soitmakessensetouseitinarightsituation—likethisone.

Myoverallplanistosetupaninheritancehierarchywithcomedyandtragedysubclassesthatcontainthecalculationlogicforthosecases.Callerscallapolymorphicamountfunctionthatthelanguagewilldispatchtothedifferentcalculationsforthecomediesandtragedies.I’llmakeasimilarstructureforthevolumecreditscalculation.Todothis,Iutilizeacoupleofrefactorings.ThecorerefactoringisReplaceConditionalwithPolymorphism(271),whichchangesahunkofconditionalcodewithpolymorphism.ButbeforeIcandoReplaceConditionalwithPolymorphism(271),Ineedtocreateaninheritancestructureofsomekind.Ineedtocreateaclasstohosttheamountandvolumecreditfunctions.

Ibeginbyreviewingthecalculationcode.(OneofthepleasantconsequencesofthepreviousrefactoringisthatIcannowignoretheformattingcode,solongasIproducethesameoutputdatastructure.Icanfurthersupportthisbyaddingteststhatprobetheintermediatedatastructure.)

createStatementData.js…

exportdefaultfunctioncreateStatementData(invoice,plays){

constresult={};

result.customer=invoice.customer;

result.performances=invoice.performances.map(enrichPerformance);

result.totalAmount=totalAmount(result);

result.totalVolumeCredits=totalVolumeCredits(result);

returnresult;

functionenrichPerformance(aPerformance){

constresult=Object.assign({},aPerformance);

result.play=playFor(result);

result.amount=amountFor(result);

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

functionplayFor(aPerformance){

returnplays[aPerformance.playID]

}

functionamountFor(aPerformance){

letresult=0;

switch(aPerformance.play.type){

case"tragedy":

result=40000;

if(aPerformance.audience>30){

result+=1000*(aPerformance.audience-30);

}

break;

case"comedy":

result=30000;

if(aPerformance.audience>20){

result+=10000+500*(aPerformance.audience-20);

}

result+=300*aPerformance.audience;

break;

default:

thrownewError(`unknowntype:${aPerformance.play.type}`);

}

returnresult;

}

functionvolumeCreditsFor(aPerformance){

letresult=0;

result+=Math.max(aPerformance.audience-30,0);

if("comedy"===aPerformance.play.type)result+=Math.floor(aPerformance.audience/5);

returnresult;

}

functiontotalAmount(data){

returndata.performances

.reduce((total,p)=>total+p.amount,0);

}

functiontotalVolumeCredits(data){

returndata.performances

.reduce((total,p)=>total+p.volumeCredits,0);

}

CreatingaPerformanceCalculator

TheenrichPerformancefunctionisthekey,sinceitpopulatestheintermediatedatastructurewiththedataforeachperformance.Currently,itcallstheconditionalfunctionsforamountandvolumecredits.WhatIneedittodoiscallthosefunctionsonahostclass.Sincethatclasshostsfunctionsforcalculatingdataaboutperformances,I’llcallitaperformancecalculator.

functioncreateStatementData…

functionenrichPerformance(aPerformance){

constcalculator=newPerformanceCalculator(aPerformance);

constresult=Object.assign({},aPerformance);

result.play=playFor(result);

result.amount=amountFor(result);

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

toplevel…

classPerformanceCalculator{

constructor(aPerformance){

this.performance=aPerformance;

}

}

Sofar,thisnewobjectisn’tdoinganything.Iwanttomovebehaviorintoit—andI’dliketostartwiththesimplestthingtomove,whichistheplayrecord.Strictly,Idon’tneedtodothis,asit’snotvaryingpolymorphically,butthiswayI’llkeepallthedatatransformsinoneplace,andthatconsistencywillmakethecodeclearer.

Tomakethiswork,IwilluseChangeFunctionDeclaration(124)topasstheperformance’splayintothecalculator.

functioncreateStatementData…

functionenrichPerformance(aPerformance){

constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance)

constresult=Object.assign({},aPerformance);

result.play=calculator.play;

result.amount=amountFor(result);

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

classPerformanceCalculator…

classPerformanceCalculator{

constructor(aPerformance,aPlay){

this.performance=aPerformance;

this.play=aPlay;

}

}

(I’mnotsayingcompile-test-commitallthetimeanymore,asIsuspectyou’regettingtiredofreadingit.ButIstilldoitateveryopportunity.Idosometimesgettiredofdoingit—andgivemistakesthechancetobiteme.ThenIlearnandgetbackintotherhythm.)

MovingFunctionsintotheCalculator

ThenextbitoflogicImoveisrathermoresubstantialforcalculatingtheamountforaperformance.I’vemovedfunctionsaroundcasuallywhilerearrangingnestedfunctions—butthisisadeeperchangeinthecontextofthefunction,soI’llstepthroughtheMoveFunction(196)refactoring.Thefirstpartofthisrefactoringistocopythelogicovertoitsnewcontext—thecalculatorclass.Then,Iadjustthecodetofitintoitsnewhome,changingaPerformancetothis.performanceandplayFor(aPerformance)tothis.play.

classPerformanceCalculator…

getamount(){

letresult=0;

switch(this.play.type){

case"tragedy":

result=40000;

if(this.performance.audience>30){

result+=1000*(this.performance.audience-30);

}

break;

case"comedy":

result=30000;

if(this.performance.audience>20){

result+=10000+500*(this.performance.audience-20);

}

result+=300*this.performance.audience;

break;

default:

thrownewError(`unknowntype:${this.play.type}`);

}

returnresult;

}

Icancompileatthispointtocheckforanycompile-timeerrors.“Compiling”inmydevelopmentenvironmentoccursasIexecutethecode,sowhatIactuallydoisrunBabel[bib-babel].Thatwillbeenoughtocatchanysyntaxerrorsinthenewfunction—butlittlemorethanthat.Evenso,thatcanbeausefulstep.

Oncethenewfunctionfitsitshome,Itaketheoriginalfunctionandturnitintoadelegatingfunctionsoitcallsthenewfunction.

functioncreateStatementData…

functionamountFor(aPerformance){

returnnewPerformanceCalculator(aPerformance,playFor(aPerformance)).amount;

}

NowIcancompile-test-committoensurethecodeisworkingproperlyinitsnewhome.Withthatdone,IuseInlineFunction(115)tocallthenewfunctiondirectly(compile-test-commit).

functioncreateStatementData…

functionenrichPerformance(aPerformance){

constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance));

constresult=Object.assign({},aPerformance);

result.play=calculator.play;

result.amount=calculator.amount;

result.volumeCredits=volumeCreditsFor(result);

returnresult;

}

Irepeatthesameprocesstomovethevolumecreditscalculation.

functioncreateStatementData…

functionenrichPerformance(aPerformance){

constcalculator=newPerformanceCalculator(aPerformance,playFor(aPerformance));

constresult=Object.assign({},aPerformance);

result.play=calculator.play;

result.amount=calculator.amount;

result.volumeCredits=calculator.volumeCredits;

returnresult;

}

classPerformanceCalculator…

getvolumeCredits(){

letresult=0;

result+=Math.max(this.performance.audience-30,0);

if("comedy"===this.play.type)result+=Math.floor(this.performance

returnresult;

}

MakingthePerformanceCalculatorPolymorphic

NowthatIhavethelogicinaclass,it’stimetoapplythepolymorphism.ThefirststepistouseReplaceTypeCodewithSubclasses(361)tointroducesubclassesinsteadofthetypecode.Forthis,IneedtocreatesubclassesoftheperformancecalculatorandusetheappropriatesubclassincreatePerformanceData.Inordertogettherightsubclass,Ineedtoreplacetheconstructorcallwithafunction,sinceJavaScriptconstructorscan’treturnsubclasses.SoIuseReplaceConstructorwithFactoryFunction(332).

functioncreateStatementData…

functionenrichPerformance(aPerformance){

constcalculator=createPerformanceCalculator(aPerformance,playFor(aPerformance));

constresult=Object.assign({},aPerformance);

result.play=calculator.play;

result.amount=calculator.amount;

result.volumeCredits=calculator.volumeCredits;

returnresult;

}

toplevel…

functioncreatePerformanceCalculator(aPerformance,aPlay){

returnnewPerformanceCalculator(aPerformance,aPlay);

}

Withthatnowafunction,Icancreatesubclassesoftheperformancecalculatorandgetthecreationfunctiontoselectwhichonetoreturn.

toplevel…

functioncreatePerformanceCalculator(aPerformance,aPlay){

switch(aPlay.type){

case"tragedy":returnnewTragedyCalculator(aPerformance,aPlay);

case"comedy":returnnewComedyCalculator(aPerformance,aPlay);

default:

thrownewError(`unknowntype:${aPlay.type}`);

}

}

classTragedyCalculatorextendsPerformanceCalculator{

}

classComedyCalculatorextendsPerformanceCalculator{

}

Thissetsupthestructureforthepolymorphism,soIcannowmoveontoReplaceConditionalwithPolymorphism(271).

Istartwiththecalculationoftheamountfortragedies.

classTragedyCalculator…

getamount(){

letresult=40000;

if(this.performance.audience>30){

result+=1000*(this.performance.audience-30);

}

returnresult;

}

Justhavingthismethodinthesubclassisenoughtooverridethesuperclassconditional.Butifyou’reasparanoidasIam,youmightdothis:

classPerformanceCalculator…

getamount(){

letresult=0;

switch(this.play.type){

case"tragedy":

throw'badthing';

case"comedy":

result=30000;

if(this.performance.audience>20){

result+=10000+500*(this.performance.audience-20);

}

result+=300*this.performance.audience;

break;

default:

thrownewError(`unknowntype:${this.play.type}`);

}

returnresult;

}

Icouldhaveremovedthecasefortragedyandletthedefaultbranchthrowanerror.ButIliketheexplicitthrow—anditwillonlybethereforacouplemoreminutes(whichiswhyIthrewastring,notabettererrorobject).

Afteracompile-test-commitofthat,Imovethecomedycasedowntoo.

classComedyCalculator…

getamount(){

letresult=30000;

if(this.performance.audience>20){

result+=10000+500*(this.performance.audience-20);

}

result+=300*this.performance.audience;

returnresult;

}

Icannowremovethesuperclassamountmethod,asitshouldneverbecalled.Butit’skindertomyfutureselftoleaveatombstone.

classPerformanceCalculator…

getamount(){

thrownewError('subclassresponsibility');

}

Thenextconditionaltoreplaceisthevolumecreditscalculation.Lookingatthediscussionoffuturecategoriesofplays,Inoticethatmostplaysexpecttocheckifaudienceisabove30,withonlysomecategoriesintroducingavariation.Soitmakessensetoleavethemorecommoncaseonthesuperclassasadefault,andletthevariationsoverrideitasnecessary.SoIjustpushdownthecaseforcomedies:

classPerformanceCalculator…

getvolumeCredits(){

returnMath.max(this.performance.audience-30,0);

}

classComedyCalculator…

getvolumeCredits(){

returnsuper.volumeCredits+Math.floor(this.performance.audience/5);

}

Status:CreatingtheDatawiththePolymorphicCalculator

Timetoreflectonwhatintroducingthepolymorphiccalculatordidtothecode.

createStatementData.js

exportdefaultfunctioncreateStatementData(invoice,plays){

constresult={};

result.customer=invoice.customer;

result.performances=invoice.performances.map(enrichPerformance);

result.totalAmount=totalAmount(result);

result.totalVolumeCredits=totalVolumeCredits(result);

returnresult;

functionenrichPerformance(aPerformance){

constcalculator=createPerformanceCalculator(aPerformance,playFor(aPerformance));

constresult=Object.assign({},aPerformance);

result.play=calculator.play;

result.amount=calculator.amount;

result.volumeCredits=calculator.volumeCredits;

returnresult;

}

functionplayFor(aPerformance){

returnplays[aPerformance.playID]

}

functiontotalAmount(data){

returndata.performances

.reduce((total,p)=>total+p.amount,0);

}

functiontotalVolumeCredits(data){

returndata.performances

.reduce((total,p)=>total+p.volumeCredits,0);

}

}

functioncreatePerformanceCalculator(aPerformance,aPlay){

switch(aPlay.type){

case"tragedy":returnnewTragedyCalculator(aPerformance,aPlay);

case"comedy":returnnewComedyCalculator(aPerformance,aPlay);

default:

thrownewError(`unknowntype:${aPlay.type}`);

}

}

classPerformanceCalculator{

constructor(aPerformance,aPlay){

this.performance=aPerformance;

this.play=aPlay;

}

getamount(){

thrownewError('subclassresponsibility');

}

getvolumeCredits(){

returnMath.max(this.performance.audience-30,0);

}

}

classTragedyCalculatorextendsPerformanceCalculator{

getamount(){

letresult=40000;

if(this.performance.audience>30){

result+=1000*(this.performance.audience-30);

}

returnresult;

}

}

classComedyCalculatorextendsPerformanceCalculator{

getamount(){

letresult=30000;

if(this.performance.audience>20){

result+=10000+500*(this.performance.audience-20);

}

result+=300*this.performance.audience;

returnresult;

}

getvolumeCredits(){

returnsuper.volumeCredits+Math.floor(this.performance.audience/5);

}

}

Again,thecodehasincreasedinsizeasI’veintroducedstructure.Thebenefithereisthatthecalculationsforeachkindofplayaregroupedtogether.Ifmostofthechangeswillbetothiscode,itwillbehelpfultohaveitclearlyseparatedlikethis.Addinganewkindofplayrequireswritinganewsubclassandaddingitto

thecreationfunction.

Theexamplegivessomeinsightastowhenusingsubclasseslikethisisuseful.Here,I’vemovedtheconditionallookupfromtwofunctions(amountForandvolumeCreditsFortoasingleconstructorfunctioncreatePerformanceCalculator.Themorefunctionstherearethatdependonthesametypeofpolymorphism,themoreusefulthisapproachbecomes.

AnalternativetowhatI’vedoneherewouldbetohavecreatePerformanceDatareturnthecalculatoritself,insteadofthecalculatorpopulatingtheintermediatedatastructure.OneofthenicefeaturesofJavaScript’sclasssystemisthatwithit,usinggetterslookslikeregulardataaccess.Mychoiceonwhethertoreturntheinstanceorcalculateseparateoutputdatadependsonwhoisusingthedownstreamdatastructure.Inthiscase,Ipreferredtoshowhowtousetheintermediatedatastructuretohidethedecisiontouseapolymorphiccalculator.

FinalThoughts

Thisisasimpleexample,butIhopeitwillgiveyouafeelingforwhatrefactoringislike.I’veusedseveralrefactorings,includingExtractFunction(106),InlineVariable(123),MoveFunction(196),andReplaceConditionalwithPolymorphism(271)

Therewerethreemajorstagestothisrefactoringepisode:decomposingtheoriginalfunctionintoasetofnestedfunctions,usingSplitPhase(154)toseparatethecalculationandprintingcode,andfinallyintroducingapolymorphiccalculatorforthecalculationlogic.Eachoftheseaddedstructuretothecode,enablingmetobettercommunicatewhatthecodewasdoing.

Asisoftenthecasewithrefactoring,theearlystagesweremostlydrivenbytryingtounderstandwhatwasgoingon.Acommonsequenceis:Readthecode,gainsomeinsight,anduserefactoringtomovethatinsightfromyourheadbackintothecode.Theclearercodethenmakesiteasiertounderstandit,leadingtodeeperinsightsandabeneficialpositivefeedbackloop.TherearestillsomeimprovementsIcouldmake,butIfeelI’vedoneenoughtopassmytestofleavingthecodesignificantlybetterthanhowIfoundit.

Thetruetestofgoodcodeisishoweasyitistochangeit.

I’mtalkingaboutimprovingthecode—butprogrammerslovetoargueaboutwhatgoodcodelookslike.Iknowsomepeopleobjecttomypreferenceforsmall,well-namedfunctions.Ifweconsiderthistobeamatterofaesthetics,wherenothingiseithergoodorbadbutthinkingmakesitso,welackanyguidebutpersonaltaste.Ibelieve,however,thatwecangobeyondtasteandsaythatthetruetestofgoodcodeishoweasyitistochangeit.Codeshouldbeobvious:Whensomeoneneedstomakeachange,theyshouldbeabletofindthecodetobechangedeasilyandtomakethechangequicklywithoutintroducinganyerrors.Ahealthycodebasemaximizesourproductivity,allowingustobuildmorefeaturesforourusersbothfasterandmorecheaply.Tokeepcodehealthy,payattentiontowhatisgettingbetweentheprogrammingteamandthatideal,thenrefactortogetclosertotheideal.

Butthemostimportantthingtolearnfromthisexampleistherhythmofrefactoring.WheneverI’veshownpeoplehowIrefactor,theyaresurprisedbyhowsmallmystepsare,eachstepleavingthecodeinaworkingstatethatcompilesandpassesitstests.IwasjustassurprisedmyselfwhenKentBeckshowedmehowtodothisinahotelroominDetroittwodecadesago.Thekeytoeffectiverefactoringisrecognizingthatyougofasterwhenyoutaketinysteps,thecodeisneverbroken,andyoucancomposethosesmallstepsintosubstantialchanges.Rememberthat—andtherestissilence.

Chapter2PrinciplesinRefactoringTheexampleinthepreviouschaptershouldhavegivenyouadecentfeelofwhatrefactoringis.Nowyouhavethat,it’sagoodtimetostepbackandtalkaboutsomeofthebroaderprinciplesinrefactoring.

DefiningRefactoring

Likemanytermsinsoftwaredevelopment,“refactoring”isoftenusedverylooselybypractitioners.Iusethetermmoreprecisely,andfinditusefultouseitinthatmorepreciseform.(ThesedefinitionsarethesameasthoseIgaveinthefirsteditionofthisbook.)Theterm“refactoring”canbeusedeitherasanounoraverb.Thenoun’sdefinitionis:

Refactoring(noun):achangemadetotheinternalstructureofsoftwaretomakeiteasiertounderstandandcheapertomodifywithoutchangingitsobservablebehavior.

ThisdefinitioncorrespondstothenamedrefactoringsI’vementionedintheearlierexamples,suchasExtractFunction(106)andReplaceConditionalwithPolymorphism(271).

Theverb’sdefinitionis:

Refactoring(verb):torestructuresoftwarebyapplyingaseriesofrefactoringswithoutchangingitsobservablebehavior.

SoImightspendacoupleofhoursrefactoring,duringwhichIwouldapplyafewdozenindividualrefactorings.

Overtheyears,manypeopleintheindustryhavetakentouse“refactoring”tomeananykindofcodecleanup—butthedefinitionsabovepointtoaparticularapproachtocleaningupcode.Refactoringisallaboutapplyingsmallbehavior-preservingstepsandmakingabigchangebystringingtogetherasequenceofthesebehavior-preservingsteps.Eachindividualrefactoringiseitherprettysmall

itselforacombinationofsmallsteps.Asaresult,whenI’mrefactoring,mycodedoesn’tspendmuchtimeinabrokenstate,allowingmetostopatanymomentevenifIhaven’tfinished.

Ifsomeonesaystheircodewasbrokenforacoupleofdayswhiletheyarerefactoring,youcanbeprettysuretheywerenotrefactoring

Iuse“restructuring”asageneraltermtomeananykindofreorganizingorcleaningupofacodebase,andseerefactoringasaparticularkindofrestructuring.Refactoringmayseeminefficienttopeoplewhofirstcomeacrossitandwatchmemakinglotsoftinysteps,whenasinglebiggerstepwoulddo.Butthetinystepsallowmetogofasterbecausetheycomposesowell—and,crucially,becauseIdon’tspendanytimedebugging.

Inmydefinitions,Iusethephrase“observablebehavior.”Thisisadeliberatelylooseterm,indicatingthatthecodeshould,overall,dojustthesamethingsitdidbeforeIstarted.Itdoesn’tmeanitwillworkexactlythesame—forexample,ExtractFunction(106)willalterthecallstack,soperformancecharacteristicsmightchange—butnothingshouldchangethattheusershouldcareabout.Inparticular,interfacestomodulesoftenchangeduetosuchrefactoringsasChangeFunctionDeclaration(124)andMoveFunction(196).AnybugsthatInoticeduringrefactoringshouldstillbepresentafterrefactoring(thoughIcanfixlatentbugsthatnobodyhasobservedyet).

Refactoringisverysimilartoperformanceoptimization,asbothinvolvecarryingoutcodemanipulationsthatdon’tchangetheoverallfunctionalityoftheprogram.Thedifferenceisthepurpose:Refactoringisalwaysdonetomakethecode“easiertounderstandandcheapertomodify”.Thismightspeedthingsuporslowthingsdown.Withperformanceoptimization,Ionlycareaboutspeedinguptheprogram,andampreparedtoendupwithcodethatishardertoworkwithifIreallyneedthatimprovedperformance.

TheTwoHats

KentBeckcameupwithametaphorofthetwohats.WhenIuserefactoringtodevelopsoftware,Idividemytimebetweentwodistinctactivities:addingfunctionalityandrefactoring.WhenIaddfunctionality,Ishouldn’tbechangingexistingcode;I’mjustaddingnewcapabilities.Imeasuremyprogressbyaddingtestsandgettingtheteststowork.WhenIrefactor,Imakeapointofnotadding

functionality;Ionlyrestructurethecode.Idon’taddanytests(unlessIfindacaseImissedearlier);IonlychangetestswhenIhavetoaccommodateachangeinaninterface.

AsIdevelopsoftware,Ifindmyselfswappinghatsfrequently.Istartbytryingtoaddanewcapability,thenIrealizethiswouldbemucheasierifthecodewerestructureddifferently.SoIswaphatsandrefactorforawhile.Oncethecodeisbetterstructured,Iswaphatsbackandaddthenewcapability.OnceIgetthenewcapabilityworking,IrealizeIcodeditinawaythat’sawkwardtounderstand,soIswaphatsagainandrefactor.Allthismighttakeonlytenminutes,butduringthistimeI’malwaysawareofwhichhatI’mwearingandthesubtledifferencethatmakestohowIprogram.

WhyShouldWeRefactor?

Idon’twanttoclaimrefactoringisthecureforallsoftwareills.Itisno“silverbullet.”Yetitisavaluabletool—apairofsilverpliersthathelpsyoukeepagoodgriponyourcode.Refactoringisatoolthatcan—andshould—beusedforseveralpurposes.

RefactoringImprovestheDesignofSoftware

Withoutrefactoring,theinternaldesign—thearchitecture—ofsoftwaretendstodecay.Aspeoplechangecodetoachieveshort-termgoals,oftenwithoutafullcomprehensionofthearchitecture,thecodelosesitsstructure.Itbecomesharderformetoseethedesignbyreadingthecode.Lossofthestructureofcodehasacumulativeeffect.Theharderitistoseethedesigninthecode,theharderitisformetopreserveit,andthemorerapidlyitdecays.Regularrefactoringhelpskeepsthecodeinshape.

Poorlydesignedcodeusuallytakesmorecodetodothesamethings,oftenbecausethecodequiteliterallydoesthesamethinginseveralplaces.Thusanimportantaspectofimprovingdesignistoeliminateduplicatecode.It’snotthatreducingtheamountofcodewillmakethesystemrunanyfaster—theeffectonthefootprintoftheprogramsrarelyissignificant.Reducingtheamountofcodedoes,however,makeabigdifferenceinmodificationofthecode.Themorecodethereis,theharderitistomodifycorrectly.There’smorecodeformetounderstand.Ichangethisbitofcodehere,butthesystemdoesn’tdowhatI

expectbecauseIdidn’tchangethatbitovertherethatdoesmuchthesamethinginaslightlydifferentcontext.Byeliminatingduplication,Iensurethatthecodesayseverythingonceandonlyonce,whichistheessenceofgooddesign.

RefactoringMakesSoftwareEasiertoUnderstand

Programmingisinmanywaysaconversationwithacomputer.Iwritecodethattellsthecomputerwhattodo,anditrespondsbydoingexactlywhatItellit.Intime,IclosethegapbetweenwhatIwantittodoandwhatItellittodo.ProgrammingisallaboutsayingexactlywhatIwant.Buttherearelikelytobeotherusersofmysourcecode.Inafewmonths,ahumanwilltrytoreadmycodetomakesomechanges.Thatuser,whoweoftenforget,isactuallythemostimportant.Whocaresifthecomputertakesafewmorecyclestocompilesomething?Yetitdoesmatterifittakesaprogrammeraweektomakeachangethatwouldhavetakenonlyanhourwithproperunderstandingofmycode.

ThetroubleisthatwhenI’mtryingtogettheprogramtowork,I’mnotthinkingaboutthatfuturedeveloper.Ittakesachangeofrhythmtomakethecodeeasiertounderstand.Refactoringhelpsmemakemycodemorereadable.Beforerefactoring,Ihavecodethatworksbutisnotideallystructured.Alittletimespentonrefactoringcanmakethecodebettercommunicateitspurpose—saymoreclearlywhatIwant.

I’mnotnecessarilybeingaltruisticaboutthis.Often,thisfuturedeveloperismyself.Thismakesrefactoringevenmoreimportant.I’maverylazyprogrammer.OneofmyformsoflazinessisthatIneverrememberthingsaboutthecodeIwrite.Indeed,IdeliberatelytrynotrememberanythingIcanlookup,becauseI’mafraidmybrainwillgetfull.ImakeapointoftryingtoputeverythingIshouldrememberintothecodesoIdon’thavetorememberit.ThatwayI’mlessworriedaboutMaudite[bib-maudite]killingoffmybraincells.

RefactoringHelpsMeFindBugs

Helpinunderstandingthecodealsomeanshelpinspottingbugs.IadmitI’mnotterriblygoodatfindingbugs.Somepeoplecanreadalumpofcodeandseebugs;Icannot.However,IfindthatifIrefactorcode,Iworkdeeplyonunderstandingwhatthecodedoes,andIputthatnewunderstandingrightbackintothecode.Byclarifyingthestructureoftheprogram,Iclarifycertain

assumptionsI’vemade—toapointwhereevenIcan’tavoidspottingthebugs.

ItremindsmeofastatementKentBeckoftenmakesabouthimself:“I’mnotagreatprogrammer;I’mjustagoodprogrammerwithgreathabits.”Refactoringhelpsmebemuchmoreeffectiveatwritingrobustcode.

RefactoringHelpsMeProgramFaster

Intheend,alltheearlierpointscomedowntothis:Refactoringhelpsmedevelopcodemorequickly.

Thissoundscounterintuitive.WhenItalkaboutrefactoring,peoplecaneasilyseethatitimprovesquality.Betterinternaldesign,readability,reducingbugs—alltheseimprovequality.Butdoesn’tthetimeIspendonrefactoringreducethespeedofdevelopment?

WhenItalktosoftwaredeveloperswhohavebeenworkingonasystemforawhile,Ioftenhearthattheywereabletomakeprogressrapidlyatfirst,butnowittakesmuchlongertoaddnewfeatures.Everynewfeaturerequiresmoreandmoretimetounderstandhowtofititintotheexistingcodebase,andonceit’sadded,bugsoftencropupthattakeevenlongertofix.Thecodebasestartslookinglikeaseriesofpatchescoveringpatches,andittakesanexerciseinarchaeologytofigureouthowthingswork.Thisburdenslowsdownaddingnewfeatures—tothepointthatdeveloperswishtheycouldstartagainfromablankslate.

Icanvisualizethisstateofaffairswiththefollowingpseudo-graph:

Butsometeamsreportadifferentexperience.Theyfindtheycanaddnewfeaturesfasterbecausetheycanleveragetheexistingthingsbyquicklybuilding

onwhat’salreadythere.

Thedifferencebetweenthesetwoistheinternalqualityofthesoftware.SoftwarewithagoodinternaldesignallowsmetoeasilyfindhowandwhereIneedtomakechangestoaddanewfeature.Goodmodularityallowsmetoonlyhavetounderstandasmallsubsetofthecodebasetomakeachange.Ifthecodeisclear,I’mlesslikelytointroduceabug,andifIdo,thedebuggingeffortismucheasier.Donewell,mycodebaseturnsintoaplatformforbuildingnewfeaturesforitsdomain.

IrefertothiseffectastheDesignStaminaHypothesis(https://martinfowler.com/bliki/DesignStaminaHypothesis.html):Byputtingoureffortintoagoodinternaldesign,weincreasethestaminaofthesoftwareeffort,allowingustogofasterforlonger.Ican’tprovethatthisisthecase,whichiswhyIrefertoitasahypothesis.Butitexplainsmyexperience,togetherwiththeexperienceofhundredsofgreatprogrammersthatI’vegottoknowovermycareer.

Twentyyearsago,theconventionalwisdomwasthattogetthiskindofgooddesign,ithadtobecompletedbeforestartingtoprogram—becauseoncewewrotethecode,wecouldonlyfacedecay.Refactoringchangesthispicture.We

nowknowwecanimprovethedesignofexistingcode—sowecanformandimproveadesignovertime,evenastheneedsoftheprogramchange.Sinceitisverydifficulttodoagooddesignup-front,refactoringbecomesvitaltoachievingthatvirtuouspathofrapidfunctionality.

WhenShouldWeRefactor?

RefactoringissomethingIdoeveryhourIprogram.Ihavenoticedanumberofwaysitfitsintomyworkflow.

TheRuleofThree

Here’saguidelineDonRobertsgaveme:Thefirsttimeyoudosomething,youjustdoit.Thesecondtimeyoudosomethingsimilar,youwinceattheduplication,butyoudotheduplicatethinganyway.Thethirdtimeyoudosomethingsimilar,yourefactor.

Orforthosewholikebaseball:Threestrikes,thenyourefactor.

PreparatoryRefactoring—MakingItEasiertoAddaFeature

ThebesttimetorefactorisjustbeforeIneedtoaddanewfeaturetothecodebase.AsIdothis,Ilookattheexistingcodeand,often,seethatifitwerestructuredalittledifferently,myworkwouldbemucheasier.Perhapsthere’sfunctionthatdoesalmostallthatIneed,buthassomeliteralvaluesthatconflictwithmyneeds.WithoutrefactoringImightcopythefunctionandchangethosevalues.Butthatleadstoduplicatedcode—ifIneedtochangeitinthefuture,I’llhavetochangebothspots(and,worse,findthem).Andcopy-pastewon’thelpmeifIneedtomakeasimilarvariationforanewfeatureinthefuture.Sowithmyrefactoringhaton,IuseParameterizeFunction(308).OnceI’vedonethat,allIneedtodoiscallthefunctionwiththeparametersIneed.

It’slikeIwanttogo100mileseastbutinsteadofjusttraipsingthroughthewoods,I’mgoingtodrive20milesnorthtothehighwayandthenI’mgoingtogo100mileseastatthreetimesthespeedIcouldhaveifIjustwentstraightthere.Whenpeoplearepushingyoutojustgostraightthere,sometimesyouneedtosay,“Wait,Ineedtocheckthemapandfindthequickestroute.”Thepreparatoryrefactoringdoesthatforme.

TODOaddlink

—JessicaKerr

Thesamehappenswhenfixingabug.OnceI’vefoundthecauseoftheproblem,IseethatitwouldbemucheasiertofixshouldIunifythethreebitsofcopiedcodecausingtheerrorintoone.Orperhapsseparatingsomeupdatelogicfromquerieswillmakeiteasiertoavoidthetanglingthat’scausingtheerror.Byrefactoringtoimprovethesituation,Ialsoincreasethechancesthatthebugwillstayfixed,andreducethechancesthatotherswillappearinthesamecrevicesofthecode.

ComprehensionRefactoring:MakingCodeEasiertoUnderstand

BeforeIcanchangesomecode,Ineedtounderstandwhatitdoes.Thiscodemayhavebeenwrittenbymeorbysomeoneelse.WheneverIhavetothinktounderstandwhatthecodeisdoing,IaskmyselfifIcanrefactorthecodetomakethatunderstandingmoreimmediatelyapparent.Imaybelookingatsomeconditionallogicthat’sstructuredawkwardly.Imayhavewantedtousesomeexistingfunctionsbutspentseveralminutesfiguringoutwhattheydidbecausetheywerenamedbadly.

AtthatpointIhavesomeunderstandinginmyhead,butmyheadisn’taverygoodrecordofsuchdetails.AsWardCunninghamputsit,byrefactoringImovetheunderstandingfrommyheadintothecodeitself.Ithentestthatunderstandingbyrunningthesoftwaretoseeifitstillworks.IfImovemyunderstandingintothecode,itwillbepreservedlongerandbevisibletomycolleagues.

Thatdoesn’tjusthelpmeinthefuture—itoftenhelpsmerightnow.Earlyon,Idocomprehensionrefactoringonlittledetails.IrenameacouplevariablesnowthatIunderstandwhattheyare,orIchopalongfunctionintosmallerparts.Then,asthecodegetsclearer,IfindIcanseethingsaboutthedesignthatIcouldnotseebefore.HadInotchangedthecode,Iprobablyneverwouldhaveseenthesethings,becauseI’mjustnotcleverenoughtovisualizeallthesechangesinmyhead.RalphJohnsondescribestheseearlyrefactoringsaswipingthedirtoffawindowsoyoucanseebeyond.WhenI’mstudyingcode,refactoringleadsmetohigherlevelsofunderstandingthatIwouldotherwisemiss.Thosewhodismisscomprehensionrefactoringasuselessfiddlingwiththe

codedon’trealizethatbyforegoingittheyneverseetheopportunitieshiddenbehindtheconfusion.

Litter-PickupRefactoring

AvariationofcomprehensionrefactoringiswhenIunderstandwhatthecodeisdoing,butrealizethatit’sdoingitbadly.Thelogicisunnecessarilyconvoluted,orIseefunctionsthatarenearlyidenticalandcanbereplacedbyasingleparameterizedfunction.There’sabitofatrade-offhere.Idon’twanttospendalotoftimedistractedfromthetaskI’mcurrentlydoing,butIalsodon’twanttoleavethetrashlyingaroundandgettinginthewayoffuturechanges.Ifit’seasytochange,I’lldoitrightaway.Ifit’sabitmoreefforttofix,ImightmakeanoteofitandfixitwhenI’mdonewithmyimmediatetask.

Sometimes,ofcourse,it’sgoingtotakeafewhourstofix,andIhavemoreurgentthingstodo.Eventhen,however,it’susuallyworthwhiletomakeitalittlebitbetter.Astheoldcampingadagesays,alwaysleavethecampsitecleanerthanwhenyoufoundit.IfImakeitalittlebettereachtimeIpassthroughthecode,overtimeitwillgetfixed.ThenicethingaboutrefactoringisthatIdon’tbreakthecodewitheachsmallstep—so,sometimes,ittakesmonthstocompletethejobbutthecodeisneverbrokenevenwhenI’mpartwaythroughit.

PlannedandOpportunisticRefactoring

Theexamplesabove—preparatory,comprehension,litter-pickuprefactoring—areallopportunistic.Idon’tsetasidetimeatthebeginningtospendonrefactoring—instead,Idorefactoringaspartofaddingafeatureorfixingabug.It’spartofmynaturalflowofprogramming.WhetherI’maddingafeatureorfixingabug,refactoringhelpsmedotheimmediatetaskandalsosetsmeuptomakefutureworkeasier.Thisisanimportantpointthat’sfrequentlymissed.Refactoringisn’tanactivitythat’sseparatedfromprogramming—anymorethanyousetasidetimetowriteifstatements.Idon’tputtimeonmyplanstodorefactoring;mostrefactoringhappenswhileI’mdoingotherthings.

Youhavetorefactorwhenyourunintouglycode—butexcellentcodeneedsplentyofrefactoringtoo.

It’salsoacommonerrortoseerefactoringassomethingpeopledotofixpastmistakesorcleanupuglycode.Certainlyyouhavetorefactorwhenyourunintouglycode,butexcellentcodeneedsplentyofrefactoringtoo.WheneverIwritecode,I’mmakingtradeoffs—howmuchtoIneedtoparameterize,wheretodrawthelinesbetweenfunctions?ThetradeoffsImadecorrectlyforyesterday’sfeaturesetmaynolongerbetherightonesforthenewfeaturesI’maddingtoday.TheadvantageisthatcleancodeiseasiertorefactorwhenIneedtochangethosetradeoffstoreflectthenewreality.

foreachdesiredchange,makethechangeeasy(warning:thismaybehard),thenmaketheeasychange

TODOaddlink

—KentBeck

Foralongtime,peoplethoughtofwritingsoftwareasaprocessofaccretion:Toaddnewfeatures,weshouldbemostlyaddingnewcode.Butgooddevelopersknowthat,often,thefastestwaytoaddanewfeatureistochangethecodetomakeiteasytoadd.Softwareshouldthusbeneverthoughtofas“done.”Asnewcapabilitiesareneeded,thesoftwarechangestoreflectthat.Thosechangescanoftenbegreaterintheexistingcodethaninthenewcode.

Allthisdoesn’tmeanthatplannedrefactoringisalwayswrong.Ifateamhasneglectedrefactoring,itoftenneedsdedicatedtimetogettheircodebaseintoabetterstatefornewfeatures,andaweekspentrefactoringnowcanrepayitselfoverthenextcoupleofmonths.Sometimes,evenwithregularrefactoringI’llseeaproblemareagrowtothepointwhenitneedssomeconcertedefforttofix.Butsuchplannedrefactoringepisodesshouldberare.Mostrefactoringeffortshouldbetheunremarkable,opportunistickind.

OnebitofadviceI’veheardistoseparaterefactoringworkandnewfeatureadditionsintoseparateversion-controlcommits.Thebigadvantageofthisisthattheycanbereviewedandapprovedindependently.I’mnotconvincedofthis,however.Toooften,therefactoringsarecloselyinterwovenwithaddingnewfeatures,andit’snotworththetimetoseparatethemout.Thiscanalsoremovethecontextfortherefactoring,makingtherefactoringcommitshardtojustify.Eachteamshouldexperimenttofindwhatworksforthem;justrememberthatseparatingrefactoringcommitsisnotaself-evidentprinciple—it’sonly

worthwhileifitmakeslifeeasier.

Long-TermRefactoring

Mostrefactoringcanbecompletedwithinafewminutes—hoursatmost.Buttherearesomelargerrefactoringeffortsthatcantakeateamweekstocomplete.Perhapstheyneedtoreplaceanexistinglibrarywithanewone.Orpullsomesectionofcodeoutintoacomponentthattheycansharewithanotherteam.Orfixsomenastymessofdependenciesthattheyhadallowedtobuildup.

Eveninsuchcases,I’mreluctanttohaveateamdodedicatedrefactoring.Often,ausefulstrategyistoagreetograduallyworkontheproblemoverthecourseofthenextfewweeks.Wheneveranyonegoesnearanycodethat’sintherefactoringzone,theymoveitalittlewayinthedirectiontheywanttoimprove.Thistakesadvantageofthefactthatrefactoringdoesn’tbreakthecode—eachsmallchangeleaveseverythinginastill-workingstate.Tochangefromonelibrarytoanother,startbyintroducinganewabstractionthatcanactasaninterfacetoeitherlibrary.Oncethecallingcodeusesthisabstraction,it’smucheasiertoswitchonelibraryforanother.(ThistacticiscalledBranchByAbstraction(https://martinfowler.com/bliki/BranchByAbstraction.html))

RefactoringinaCodeReview

Someorganizationsdoregularcodereviews;thosethatdon’twoulddobetteriftheydid.Codereviewshelpspreadknowledgethroughadevelopmentteam.Reviewshelpmoreexperienceddeveloperspassknowledgetothoselessexperienced.Theyhelpmorepeopleunderstandmoreaspectsofalargesoftwaresystem.Theyarealsoveryimportantinwritingclearcode.Mycodemaylookcleartomebutnottomyteam.That’sinevitable—it’shardforpeopletoputthemselvesintheshoesofsomeoneunfamiliarwithwhatevertheyareworkingon.Reviewsalsogivetheopportunityformorepeopletosuggestusefulideas.Icanonlythinkofsomanygoodideasinaweek.Havingotherpeoplecontributemakesmylifeeasier,soIalwayslookforreviews.

I’vefoundthatrefactoringhelpsmereviewsomeoneelse’scode.BeforeIstartedusingrefactoring,Icouldreadthecode,understandittosomedegree,andmakesuggestions.Now,whenIcomeupwithideas,Iconsiderwhethertheycanbeeasilyimplementedthenandtherewithrefactoring.Ifso,Irefactor.

WhenIdoitafewtimes,Icanseemoreclearlywhatthecodelookslikewiththesuggestionsinplace.Idon’thavetoimaginewhatitwouldbelike—Icanseeit.Asaresult,IcancomeupwithasecondlevelofideasthatIwouldneverhaverealizedhadInotrefactored.

Refactoringalsohelpsgetmoreconcreteresultsfromthecodereview.Notonlyaretheresuggestions;manysuggestionsareimplementedthereandthen.Youendupwithmuchmoreofasenseofaccomplishmentfromtheexercise.

HowI’dembedrefactoringintoacodereviewdependsonthenatureofthereview.Thecommonpullrequestmodel,whereareviewerlooksatcodewithouttheoriginalauthor,doesn’tworktoowell.It’sbettertohavetheoriginalauthorofthecodepresentbecausetheauthorcanprovidecontextonthecodeandfullyappreciatethereviewers’intentionsfortheirchanges.I’vehadmybestexperienceswiththisbysittingone-on-onewiththeoriginalauthor,goingthroughthecodeandrefactoringaswego.Thelogicalconclusionofthisstyleispairprogramming:continuouscodereviewembeddedwithintheprocessofprogramming.

WhatDoITellMyManager?

OneofthemostcommonquestionsI’vebeenaskedis,“Howtotellamanageraboutrefactoring?”I’vecertainlyseenplaceswererefactoringhasbecomeadirtyword—withmanagers(andcustomers)believingthatrefactoringiseithercorrectingerrorsmadeearlier,orworkthatdoesn’tyieldvaluablefeatures.Thisisexacerbatedbyteamsschedulingweeksofpurerefactoring—especiallyifwhattheyarereallydoingisnotrefactoringbutlesscarefulrestructuringthatcausesbreakagesinthecodebase.

Toamanagerwhoisgenuinelysavvyabouttechnologyandunderstandsthedesignstaminahypothesis,refactoringisn’thardtojustify.Suchmanagersshouldbeencouragingrefactoringonaregularbasisandbelookingforsignsthatindicateateamisn’tdoingenough.Whileitdoeshappenthatteamsdotoomuchrefactoring,it’smuchrarerthanteamsnotdoingenough.

Ofcourse,manymanagersandcustomerdon’thavethetechnicalawarenesstoknowhowcodebasehealthimpactsproductivity.InthesecasesIgivemymorecontroversialadvice:Don’ttell!

Subversive?Idon’tthinkso.Softwaredevelopersareprofessionals.Ourjobistobuildeffectivesoftwareasrapidlyaswecan.Myexperienceisthatrefactoringisabigaidtobuildingsoftwarequickly.IfIneedtoaddanewfunctionandthedesigndoesnotsuitthechange,Ifindit’squickertorefactorfirstandthenaddthefunction.IfIneedtofixabug,Ineedtounderstandhowthesoftwareworks—andIfindrefactoringisthefastestwaytodothis.Aschedule-drivenmanagerwantsmetodothingsthefastestwayIcan;howIdoitismyresponsibility.I’mbeingpaidformyexpertiseinprogrammingnewcapabilitiesfast,andthefastestwayisbyrefactoring—thereforeIrefactor.

WhenShouldINotRefactor?

ItmaysoundlikeIalwaysrecommendrefactoring—buttherearecaseswhenit’snotworthwhile.

IfIrunacrosscodethatisamess,butIdon’tneedtomodifyit,thenIdon’tneedtorefactorit.SomeuglycodethatIcantreatasanAPImayremainugly.It’sonlywhenIneedtounderstandhowitworksthatrefactoringgivesmeanybenefit.

Anothercaseiswhenit’seasiertorewriteitthantorefactorit.Thisisatrickydecision.Often,Ican’ttellhoweasyitistorefactorsomecodeunlessIspendsometimetryingandthusgetasenseofhowdifficultitis.Thedecisiontorefactororrewriterequiresgoodjudgmentandexperience,andIcan’treallyboilitdownintoapieceofsimpleadvice.

ProblemswithRefactoring

Wheneveranyoneadvocatesforsometechnique,tool,orarchitecture,Ialwayslookforproblems.Fewthingsinlifeareallsunshineandclearskies.Youneedtounderstandthetrade-offstodecidewhenandwheretoapplysomething.Idothinkrefactoringisavaluabletechnique—onethatshouldbeusedmorebymostteams.Butthereareproblemsassociatedwithit,andit’simportanttounderstandhowtheymanifestthemselvesandhowwecanreacttothem.

SlowingDownNewFeatures

Ifyoureadtheprevioussection,youshouldalreadyknowmyresponse.

Althoughmanypeopleseetimespentrefactoringasslowingdownthedevelopmentofnewfeatures,thewholepurposeofrefactoringistospeedthingsup.Butwhilethisistrue,it’salsotruethattheperceptionofrefactoringasslowingthingsdownisstillcommon—andperhapsthebiggestbarriertopeopledoingenoughrefactoring.

Thewholepurposeofrefactoringistomakeusprogramfaster,producingmorevaluewithlesseffort.

Thereisagenuinetrade-offhere.IdorunintosituationswhereIseea(large-scale)refactoringthatreallyneedstobedone,butthenewfeatureIwanttoaddissosmallthatIprefertoadditandleavethelargerrefactoringalone.That’sajudgmentcall—partofmyprofessionalskillsasaprogrammer.Ican’teasilydescribe,letalonequantify,howImakethattrade-off.

I’mveryconsciousthatpreparatoryrefactoringoftenmakesachangeeasier,soIcertainlywilldoitifIseethatitmakesmynewfeatureeasiertoimplement.I’malsomoreinclinedtorefactorifthisisaproblemI’veseenbefore—sometimesittakesmeacoupleoftimesseeingsomeparticularuglinessbeforeIdecidetorefactoritaway.Conversely,I’mmorelikelytonotrefactorifit’spartofthecodeIrarelytouchandthecostoftheinconvenienceisn’tsomethingIfeelveryoften.Sometimes,IdelayarefactoringbecauseI’mnotsurewhatimprovementtodo,althoughatothertimesI’lltrysomethingasanexperimenttoseeifitmakesthingsbetter.

Still,theevidenceIhearfrommycolleaguesintheindustryisthattoolittlerefactoringisfarmoreprevalentthantoomuch.Inotherwords,mostpeopleshouldtrytorefactormoreoften.Youmayhavetroubletellingthedifferenceinproductivitybetweenahealthyandasicklycodebasebecauseyouhaven’thadenoughexperienceofahealthycodebase—ofthepowerthatcomesfromeasilycombiningexistingpartsintonewconfigurationstoquicklyenablecomplicatednewfeatures.

Althoughit’softenmanagersthatarecriticizedforthecounter-productivehabitofsquelchingrefactoringinthenameofspeed,I’veoftenseendevelopersdoittothemselves.Sometimes,theythinktheyshouldn’tberefactoringeventhoughtheirleadershipisactuallyinfavor.Ifyou’reatechleadinateam,it’simportanttoshowteammembersthatyouvalueimprovingthehealthofacodebase.ThatjudgmentImentionedearlieronwhethertorefactorornotissomethingthat

takesyearsofexperiencetobuildup.Thosewithlessexperienceinrefactoringneedlotsofmentoringtoacceleratethemthroughtheprocess.

ButIthinkthemostdangerouswaythatpeoplegettrappediswhentheytrytojustifyrefactoringintermsof“cleancode,”“goodengineeringpractice,”orsimilarmoralreasons.Thepointofrefactoringisn’ttoshowhowsparklyacodebaseis—itispurelyeconomic.Werefactorbecauseitmakesusfaster—fastertoaddfeatures,fastertofixbugs.It’simportanttokeepthatinfrontofyourmindandinfrontofcommunicationwithothers.Theeconomicbenefitsofrefactoringshouldalwaysbethedrivingfactor,andthemorethatisunderstoodbydevelopers,managers,andcustomers,themoreofthe“gooddesign”curvewe’llsee.

CodeOwnership

Manyrefactoringsinvolvemakingchangesthataffectnotjusttheinternalsofamodulebutitsrelationshipswithotherpartsofasystem.IfIwanttorenameafunction,andIcanfindallthecallerstoafunction,IsimplyapplyChangeFunctionDeclaration(124)andchangethedeclarationandthecallersinonechange.Butsometimesthissimplerefactoringisn’tpossible.PerhapsthecallingcodeisownedbyadifferentteamandIdon’thavewriteaccesstotheirrepository.PerhapsthefunctionisadeclaredAPIusedbymycustomers—soIcan’teventellifit’sbeingused,letalonebywhoandhowmuch.Suchfunctionsarepartofapublishedinterface—aninterfacethatisusedbyclientsindependentofthosewhodeclaretheinterface.

CodeownershipboundariesgetinthewayofrefactoringbecauseIcannotmakethekindsofchangesIwantwithoutbreakingmyclients.Thisdon’tpreventrefactoring—Icanstilldoagreatdeal—butitdoesimposelimitations.Whenrenamingafunction,IneedtouseChangeFunctionDeclaration(124)andtoretaintheolddeclarationasapass-throughtothenewone.Thiscomplicatestheinterface—butitisthepriceImustpaytoavoidbreakingmyclients.Imaybeabletomarktheoldinterfaceasdeprecatedand,intime,retireit,butsometimesIhavetoretainthatinterfaceforever.

Duetothesecomplexities,Irecommendagainstfine-grainedstrongcodeownership.Someorganizationslikeanypieceofcodetohaveasingleprogrammerasanowner,andonlyallowthatprogrammertochangeit.I’veseenateamofthreepeopleoperateinsuchawaythateachonepublishedinterfaces

totheothertwo.Thisledtoallsortsofgyrationstomaintaininterfaceswhenitwouldhavebeenmucheasiertogointothecodebaseandmaketheedits.Mypreferenceistoallowteamownershipofcode—sothatanyoneinthesameteamcanmodifytheteam’scode,eveniforiginallywrittenbysomeoneelse.Programmersmayhaveindividualresponsibilityforareasofasystem,butthatshouldimplythattheymonitorchangestotheirareaofresponsibility,notblockthembydefault.

Suchamorepermissiveownershipschemecanevenexistacrossteams.Someteamsencourageanopen-sourcelikemodelwherepeoplefromotherteamscanchangeabranchoftheircodeandsendthecommitintobeapproved.Thisallowsoneteamtochangetheclientsoftheirfunctions—theycandeletetheolddeclarationsoncetheircommitstotheirclientshavebeenaccepted.Thiscanoftenbeagoodcompromisebetweenstrongcodeownershipandchaoticchangesinlargesystems.

Branches

AsIwritethis,acommonapproachinteamsisforeachteammembertoworkonabranchofthecodebaseusingaversioncontrolsystem,anddoconsiderableworkonthatbranchbeforeintegratingwithamainline(oftencalledmasterortrunk)sharedacrosstheteam.Often,thisinvolvesbuildingawholefeatureonabranch,notintegratingintothemainlineuntilthefeatureisreadytobereleasedintoproduction.Fansofthisapproachclaimthatitkeepsthemainlineclearofanyin-processcode,providesaclearversionhistoryoffeatureadditions,andallowsfeaturestoberevertedeasilyshouldtheycauseproblems.

Therearedownsidestofeaturebrancheslikethis.ThelongerIworkonanisolatedbranch,theharderthejobofintegratingmyworkwithmainlineisgoingtobewhenI’mdone.Mostpeoplereducethispainbyfrequentlymergingorre-basingfrommainlinetomybranch.Butthisdoesn’treallysolvetheproblemwhenseveralpeopleareworkingonindividualfeaturebranches.Idistinguishbetweenmergingandintegration.IfImergemainlineintomycode,thisisaonewaymovement—mybranchchangesbutthemainlinedoesn’t.Iuse“integrate”tomeanatwo-wayprocessthatpullschangesfrommainlineintomybranchandthenpushestheresultbackintomainline,changingboth.IfRachelisworkingonherbranchIdon’tseeherchangesuntilsheintegrateswithmainline;atthatpoint,Ihavetomergeherchangesintomyfeaturebranch,whichmaymeanconsiderablework.Thehardpartofthisworkisdealingwithsemantic

changes.Modernversioncontrolsystemscandowonderswithmergingcomplexchangestotheprogramtext,buttheyareblindtothesemanticsofthecode.IfI’vechangedthenameofafunction,myversioncontroltoolmayeasilyintegratemychangeswithRachel’s.Butif,inherbranch,sheaddedacalltoafunctionthatI’verenamedinmine,thecodewillfail.

Theproblemofcomplicatedmergesgetsexponentiallyworseasthelengthoffeaturebranchesincreases.Integratingbranchesthatarefourweeksoldismorethantwiceashardasthosethatareacoupleofweeksold.Manypeople,therefore,argueforkeepingfeaturebranchesshort—perhapsjustacoupleofdays.Others,suchasme,wantthemevenshorterthanthat.ThisisanapproachcalledContinuousIntegration(CI),alsoknownasTrunk-BasedDevelopment.WithCI,eachteammemberintegrateswithmainlineatleastonceperday.Thispreventsanybranchesdivertingtoofarfromeachotherandthusgreatlyreducesthecomplexityofmerges.CIdoesn’tcomeforfree:Itmeansyouusepracticestoensurethemainlineishealthy,learntobreaklargefeaturesintosmallerchunks,andusefeaturetoggles(akafeatureflags)toswitchoffanyin-processfeaturesthatcan’tbebrokendown.

FansofCIlikeitpartlybecauseitreducesthecomplexityofmerges,butthedominantreasontofavorCIisthatit’sfarmorecompatiblewithrefactoring.Refactoringsofteninvolvemakinglotsoflittlechangesalloverthecodebase—whichareparticularlypronetosemanticmergeconflicts(suchasrenamingawidelyusedfunction).Manyofushaveseenfeature-branchingteamsthatfindrefactoringssoexacerbatemergeproblemsthattheystoprefactoring.CIandrefactoringworkwelltogether,whichiswhyKentBeckcombinedtheminExtremeProgramming.

I’mnotsayingthatyoushouldneverusefeaturebranches.Iftheyaresufficientlyshort,theirproblemsaremuchreduced.(Indeed,usersofCIusuallyalsousebranches,butintegratethemwithmainlineeachday.)Featurebranchesmaybetherighttechniqueforopensourceprojectswhereyouhaveinfrequentcommitsfromprogrammerswhoyoudon’tknowwell(andthusdon’ttrust).Butinafull-timedevelopmentteam,thecostthatfeaturebranchesimposeonrefactoringisexcessive.Evenifyoudon’tgotofullCI,Icertainlyurgeyoutointegrateasfrequentlyaspossible.Youshouldalsoconsidertheobjectiveevidence[bib-forsgren]thatteamsthatuseCIaremoreeffectiveinsoftwaredelivery.

Testing

Oneofthekeycharacteristicsofrefactoringisthatitdoesn’tchangetheobservablebehavioroftheprogram.IfIfollowtherefactoringscarefully,Ishouldn’tbreakanything—butwhatifImakeamistake?(Or,knowingme,s/if/when.)Mistakeshappen,buttheyaren’taproblemprovidedIcatchthemquickly.Sinceeachrefactoringisasmallchange,ifIbreakanything,Ionlyhaveasmallchangetolookattofindthefault—andifIstillcan’tspotit,Icanrevertmyversioncontroltothelastworkingversion.

Thekeyhereisbeingabletocatchanerrorquickly.Todothis,realistically,Ineedtobeabletorunacomprehensivetestsuiteonthecode—andrunitquickly,sothatI’mnotdeterredfromrunningitfrequently.Thismeansthatinmostcases,ifIwanttorefactor,Ineedtohaveself-testingcode(https://martinfowler.com/bliki/SelfTestingCode.html).

Tosomereaders,self-testingcodesoundslikearequirementsosteepastobeunrealizable.Butoverthelastcoupleofdecades,I’veseenmanyteamsbuildsoftwarethisway.Ittakesattentionanddedicationtotesting,butthebenefitsmakeitreallyworthwhile.Self-testingcodenotonlyenablesrefactoring—italsomakesitmuchsafertoaddnewfeatures,sinceIcanquicklyfindandkillanybugsIintroduce.Thekeypointhereisthatwhenatestfails,IcanlookatthechangeI’vemadebetweenwhenthetestswerelastrunningcorrectlyandthecurrentcode.Withfrequenttestruns,thatwillbeonlyafewlinesofcode.Byknowingitwasthosefewlinesthatcausedthefailure,Icanmuchmoreeasilyfindthebug.

Thisalsoanswersthosewhoareconcernedthatrefactoringcarriestoomuchriskofintroducingbugs.Withoutself-testingcode,that’sareasonableworry—whichiswhyIputsomuchemphasisonhavingsolidtests.

Thereisanotherwaytodealwiththetestingproblem.IfIuseanenvironmentthathasgoodautomatedrefactorings,Icantrustthoserefactoringsevenwithoutrunningtests.Icanthenrefactor,providingIonlyusethoserefactoringsthataresafelyautomated.Thisremovesalotofnicerefactoringsfrommymenu,butstillleavesmeenoughtodeliversomeusefulbenefits.I’dstillratherhaveself-testingcode,butit’sanoptionthatisusefultohaveinthetoolkit.

Thisalsoinspiresastyleofrefactoringthatonlyusesalimitedsetof

refactoringsthatcanbeprovensafe.Suchrefactoringsrequirecarefullyfollowingthesteps,andarelanguage-specific.Butteamsusingthemhavefoundtheycandousefulrefactoringonlargecodebaseswithpoortestcoverage.Idon’tfocusonthatinthisbook,asit’sanewer,lessdescribedandunderstoodtechniquethatinvolvesdetailed,language-specificactivity.(Itis,however,somethingIhopetalkaboutmoreonmywebsiteinthefuture.Foratasteofit,seeJayBazuzi’sdescription[bib-bazuzi-safe]ofasaferwaytodoExtractFunction(106)inC++.)

Self-testingcodeis,unsurprisingly,closelyassociatedwithContinuousIntegration—itisthemechanismthatweusetocatchsemanticintegrationconflicts.SuchtestingpracticesareanothercomponentofExtremeProgrammingandakeypartofContinuousDelivery.

LegacyCode

MostpeoplewouldregardabiglegacyasaGoodThing—butthat’soneofthecaseswhereprogrammers’viewisdifferent.Legacycodeisoftencomplex,frequentlycomeswithpoortests,and,aboveall,iswrittenbySomeoneElse(shudder).

Refactoringcanbeafantastictooltohelpunderstandalegacysystem.Functionswithmisleadingnamescanberenamedsotheymakesense,awkwardprogrammingconstructssmoothedout,andtheprogramturnedfromaroughrocktoapolishedgem.Butthedragonguardingthishappytaleisthecommonlackoftests.Ifyouhaveabiglegacysystemwithnotests,youcan’tsafelyrefactoritintoclarity.

Theobviousanswertothisproblemisthatyouaddtests.Butwhilethissoundsasimple,iflaborious,procedure,it’softenmuchmoretrickyinpractice.Usually,asystemisonlyeasytoputundertestifitwasdesignedwithtestinginmind—inwhichcaseitwouldhavethetestsandIwouldn’tbeworryingaboutit.

There’snosimpleroutetodealingwiththis.ThebestadviceIcangiveistogetacopyofWorkingEffectivelywithLegacyCode[bib-feathers-welc]andfollowitsguidance.Don’tbeworriedbytheageofthebook—itsadviceisjustastruemorethanadecadelater.Tosummarizecrudely,itadvisesyoutogetthesystemundertestbyfindingseamsintheprogramwhereyoucaninserttests.Creatingtheseseamsinvolvesrefactoring—whichismuchmoredangeroussinceit’s

donewithouttests,butisanecessaryrisktomakeprogress.Thisisasituationwheresafe,automatedrefactoringscanbeagodsend.Ifallthissoundsdifficult,that’sbecauseitis.Sadly,there’snoshortcuttogettingoutofaholethisdeep—whichiswhyI’msuchastrongproponentofwritingself-testingcodefromthestart.

EvenwhenIdohavetests,Idon’tadvocatetryingtorefactoracomplicatedlegacymessintobeautifulcodeallatonce.WhatIprefertodoistackleitinrelevantpieces.EachtimeIpassthroughasectionofthecode,Itrytomakeitalittlebitbetter—again,likeleavingacampsitecleanerthanwhenIfoundit.Ifthisisalargesystem,I’lldomorerefactoringinareasIvisitfrequently—whichistherightthingtodobecause,ifIneedtovisitcodefrequently,I’llgetabiggerpayoffbymakingiteasiertounderstand.

Databases

WhenIwrotethefirsteditionofthisbook,Isaidthatrefactoringdatabaseswasaproblemarea.But,withinayearofthebook’spublication,thatwasnolongerthecase.MycolleaguePramodSadalagedevelopedanapproachtoevolutionarydatabasedesign[bib-evo-db]anddatabaserefactoring[bib-refact-db]thatisnowwidelyused.Theessenceofthetechniqueistocombinethestructuralchangestoadatabase’sschemaandaccesscodewithdatamigrationscriptsthatcaneasilycomposetohandlelargechanges.

Considerasimpleexampleofrenamingafield(column).AsinChangeFunctionDeclaration(124),Ineedtofindtheoriginaldeclarationofthestructureandallthecallersofthisstructureandchangetheminasinglechange.Thecomplication,however,isthatIalsohavetotransformanydatathatusestheoldfieldtousethenewone.Iwriteasmallhunkofcodethatcarriesoutthistransformandstoreitinversioncontrol,togetherwiththecodethatchangesanydeclaredstructureandaccessroutines.Then,wheneverIneedtomigratebetweentwoversionsofthedatabase,Irunallthemigrationscriptsthatexistbetweenmycurrentcopyofthedatabaseandmydesiredversion.

Aswithregularrefactoring,thekeyhereisthateachindividualchangeissmallyetcapturesacompletechange,sothesystemstillrunsafterapplyingthemigration.Keepingthemsmallmeanstheyareeasytowrite,butIcanstringmanyofthemintoasequencethatcanmakeasignificantchangetothedatabase’sstructureandthedatastoredinit.

Onedifferencefromregularrefactoringsisthatdatabasechangesoftenarebestseparatedovermultiplereleasestoproduction.Thismakesiteasytoreverseanychangethatcausesaprobleminproduction.So,whenrenamingafield,myfirstcommitwouldaddthenewdatabasefieldbutnotuseit.Imaythensetuptheupdatessotheyupdatebotholdandnewfieldsatonce.Icanthengraduallymovethereadersovertothenewfield.Onlyoncetheyhaveallmovedtothenewfield,andI’vegivenalittletimeforanybugstoshowthemselves,wouldIremovethenow-unusedoldfield.Thisapproachtodatabasechangesisanexampleofageneralapproachofparallelchange(https://martinfowler.com/bliki/ParallelChange.html)(alsocalledexpand-contract).

Refactoring,Architecture,andYagni

Refactoringhasprofoundlychangedhowpeoplethinkaboutsoftwarearchitecture.Earlyinmycareer,Iwastaughtthatsoftwaredesignandarchitecturewassomethingtobeworkedon,andmostlycompleted,beforeanyonestartedwritingcode.Oncethecodewaswritten,itsarchitecturewasfixedandcouldonlydecayduetocarelessness.

Refactoringchangesthisperspective.Itallowsmetosignificantlyalterthearchitectureofsoftwarethat’sbeenrunninginproductionforyears.Refactoringcanimprovethedesignofexistingcode,asthisbook’ssubtitleimplies.ButasIindicatedearlier,changinglegacycodeisoftenchallenging,especiallywhenitlacksdecenttests.

Therealimpactofrefactoringonarchitectureisinhowitcanbeusedtoformawell-designedcodebasethatcanrespondgracefullytochangingneeds.Thebiggestissuewithfinishingarchitecturebeforecodingisthatsuchanapproachassumestherequirementsforthesoftwarecanbeunderstoodearlyon.Butexperienceshowsthatthisisoften,evenusually,anunachievablegoal.Repeatedly,Isawpeopleonlyunderstandwhattheyreallyneededfromsoftwareoncethey’dhadachancetouseit,andsawtheimpactitmadetotheirwork.

Onewayofdealingwithfuturechangesistoputflexibilitymechanismsintothesoftware.AsIwritesomefunction,Icanseethatithasageneralapplicability.TohandlethedifferentcircumstancesthatIanticipateittobeusedin,IcanseeadozenparametersIcouldaddtothatfunction.Theseparametersareflexibility

mechanisms—and,likemostmechanisms,theyarenotafreelunch.Addingallthoseparameterscomplicatesthefunctionfortheonecaseit’susedrightnow.IfImissaparameter,alltheparameterizationIhaveaddedmakesitharderformetoaddmore.IfindIoftengetmyflexibilitymechanismswrong—eitherbecausethechangingneedsdidn’tworkoutthewayIexpectedormymechanismdesignwasfaulty.OnceItakeallthatintoaccount,mostofthetimemyflexibilitymechanismsactuallyslowdownmyabilitytoreacttochange.

Withrefactoring,Icanuseadifferentstrategy.InsteadofspeculatingonwhatflexibilityIwillneedinthefutureandwhatmechanismswillbestenablethat,Ibuildsoftwarethatsolvesonlythecurrentlyunderstoodneeds,butImakethissoftwareexcellentlydesignedforthoseneeds.Asmyunderstandingoftheusers’needschanges,Iuserefactoringtoadaptthearchitecturetothosenewdemands.Icanhappilyincludemechanismsthatdon’tincreasecomplexity(suchassmall,well-namedfunctions)butanyflexibilitythatcomplicatesthesoftwarehastoproveitselfbeforeIincludeit.IfIdon’thavedifferentvaluesforaparameterfromthecallers,Idon’taddittotheparameterlist.ShouldthetimecomethatIneedtoaddit,thenParameterizeFunction(308)isaneasyrefactoringtoapply.Ioftenfinditusefultoestimatehowharditwouldbetouserefactoringlatertosupportananticipatedchange.OnlyifIcanseethatitwouldbesubstantiallyhardertorefactorlaterdoIconsideraddingaflexibilitymechanismnow.

Thisapproachtodesigngoesundervariousnames:simpledesign,incrementaldesign,oryagni[bib-yagni](originallyanacronymfor“youaren’tgoingtoneedit”).Yagnidoesn’timplythatarchitecturalthinkingdisappears,althoughitissometimesnaivelyappliedthatway.Ithinkofyagniasadifferentstyleofincorporatingarchitectureanddesignintothedevelopmentprocess—astylethatisn’tcrediblewithoutthefoundationofrefactoring.

Adoptingyagnidoesn’tmeanIneglectallup-frontarchitecturalthinking.Therearestillcaseswhererefactoringchangesaredifficultandsomepreparatorythinkingcansavetime.Butthebalancehasshiftedalongway—I’mmuchmoreinclinedtodealwithissueslaterwhenIunderstandthembetter.Allthishasledtoagrowingdisciplineofevolutionaryarchitecture[bib-evo-arch]wherearchitectsexplorethepatternsandpracticesthattakeadvantageofourabilitytoiterateoverarchitecturaldecisions.

RefactoringandtheWiderSoftwareDevelopmentProcess

Ifyou’vereadtheearliersectiononproblems,onelessonyou’veprobablydrawnisthattheeffectivenessofrefactoringistiedtoothersoftwarepracticesthatateamuses.Indeed,refactoring’searlyadoptionwasaspartofExtremeProgramming[bib-xp](XP),aprocesswhichwasnotableforputtingtogetherasetofrelativelyunusualandinterdependentpractices—suchascontinuousintegration,self-testingcode,andrefactoring(thelattertwowovenintotest-drivendevelopment).

ExtremeProgrammingwasoneofthefirstagilesoftwaremethods[bib-agile]and,forseveralyears,ledtheriseofagiletechniques.Enoughprojectsnowuseagilemethodsthatagilethinkingisgenerallyregardedasmainstream—butinrealitymost“agile”projectsonlyusethename.Toreallyoperateinanagileway,ateamhastobecapableandenthusiasticrefactorers—andforthat,manyaspectsoftheirprocesshavetoalignwithmakingrefactoringaregularpartoftheirwork.

Thefirstfoundationforrefactoringisself-testingcode.Bythis,ImeanthatthereisasuiteofautomatedteststhatIcanrunandbeconfidentthat,ifImadeanerrorinmyprogramming,sometestwillfail.ThisissuchanimportantfoundationforrefactoringthatI’llspendachaptertalkingmoreaboutthis.

Torefactoronateam,it’simportantthateachmembercanrefactorwhentheyneedtowithoutinterferingwithothers’work.ThisiswhyIencourageContinuousIntegration.WithCI,eachmember’srefactoringeffortsarequicklysharedwiththeircolleagues.Nooneendsupbuildingnewworkoninterfacesthatarebeingremoved,andiftherefactoringisgoingtocauseaproblemwithsomeoneelse’swork,weknowaboutthisquickly.Self-testingcodeisalsoakeyelementofContinuousIntegration,sothereisastrongsynergybetweenthethreepracticesofself-testingcode,continuousintegration,andrefactoring.

Withthistrioofpracticesinplace,weenabletheYagnidesignapproachthatItalkedaboutintheprevioussection.Refactoringandyagnipositivelyreinforceeachother:Notjustisrefactoring(anditspre-requisites)afoundationforyagni—yagnimakesiteasiertodorefactoring.Thisisbecauseit’seasiertochangeasimplesystemthanonethathaslotsofspeculativeflexibilityincluded.Balancethesepractices,andyoucangetintoavirtuouscirclewithacodebasethatrespondsrapidlytochangingneedsandisreliable.

Withthesecorepracticesinplace,wehavethefoundationtotakeadvantageof

theotherelementsoftheagilemindset.ContinuousDeliverykeepsoursoftwareinanalways-releasablestate.Thisiswhatallowsmanyweborganizationstoreleaseupdatesmanytimesaday—butevenifwedon’tneedthat,itreducesriskandallowsustoscheduleourreleasestosatisfybusinessneedsratherthantechnologicalconstraints.Withafirmtechnicalfoundation,wecandrasticallyreducethetimeittakestogetagoodideaintoproductioncode,allowingustobetterserveourcustomers.Furthermore,thesepracticesincreasethereliabilityofoursoftware,withlessbugstospendtimefixing.

Statedlikethis,itallsoundsrathersimple—butinpracticeitisn’t.Softwaredevelopment,whatevertheapproach,isatrickybusiness,withcomplexinteractionsbetweenpeopleandmachines.TheapproachIdescribehereisaprovenwaytohandlethiscomplexity,butlikeanyapproach,itrequirespracticeandskill.

RefactoringandPerformance

Acommonconcernwithrefactoringistheeffectithasontheperformanceofaprogram.Tomakethesoftwareeasiertounderstand,Ioftenmakechangesthatwillcausetheprogramtorunslower.Thisisanimportantissue.Idon’tbelongtotheschoolofthoughtthatignoresperformanceinfavorofdesignpurityorinhopesoffasterhardware.Softwarehasbeenrejectedforbeingtooslow,andfastermachinesmerelymovethegoalposts.Refactoringcancertainlymakesoftwaregomoreslowly—butitalsomakesthesoftwaremoreamenabletoperformancetuning.Thesecrettofastsoftware,inallbuthardreal-timecontexts,istowritetunablesoftwarefirstandthentuneitforsufficientspeed.

I’veseenthreegeneralapproachestowritingfastsoftware.Themostseriousoftheseistimebudgeting,oftenusedinhardreal-timesystems.Asyoudecomposethedesign,yougiveeachcomponentabudgetforresources—timeandfootprint.Thatcomponentmustnotexceeditsbudget,althoughamechanismforexchangingbudgetedresourcesisallowed.Timebudgetingfocusesattentiononhardperformancetimes.Itisessentialforsystems,suchasheartpacemakers,inwhichlatedataisalwaysbaddata.Thistechniqueisinappropriateforotherkindsofsystems,suchasthecorporateinformationsystemswithwhichIusuallywork.

Thesecondapproachistheconstantattentionapproach.Here,every

programmer,allthetime,doeswhatevershecantokeepperformancehigh.Thisisacommonapproachthatisintuitivelyattractive—butitdoesnotworkverywell.Changesthatimproveperformanceusuallymaketheprogramhardertoworkwith.Thisslowsdevelopment.Thiswouldbeacostworthpayingiftheresultingsoftwarewerequicker—butusuallyitisnot.Theperformanceimprovementsarespreadallaroundtheprogram;eachimprovementismadewithanarrowperspectiveoftheprogram’sbehavior,andoftenwithamisunderstandingofhowacompiler,runtime,andhardwarebehaves.

ItTakesAwhiletoCreateNothing

TheChryslerComprehensiveCompensationpayprocesswasrunningtooslowly.Althoughwewerestillindevelopment,itbegantobotherus,becauseitwasslowingdownthetests.

KentBeck,MartinFowler,andIdecidedwe’dfixitup.WhileIwaitedforustogettogether,Iwasspeculating,onthebasisofmyextensiveknowledgeofthesystem,aboutwhatwasprobablyslowingitdown.Ithoughtofseveralpossibilitiesandchattedwithfolksaboutthechangesthatwereprobablynecessary.Wecameupwithsomereallygoodideasaboutwhatwouldmakethesystemgofaster.

ThenwemeasuredperformanceusingKent’sprofiler.NoneofthepossibilitiesIhadthoughtofhadanythingtodowiththeproblem.Instead,wefoundthatthesystemwasspendinghalfitstimecreatinginstancesofdate.Evenmoreinterestingwasthatalltheinstanceshadthesamecoupleofvalues.

Whenwelookedatthedate-creationlogic,wesawsomeopportunitiesforoptimizinghowthesedateswerecreated.Theywereallgoingthroughastringconversioneventhoughnoexternalinputswereinvolved.Thecodewasjustusingstringconversionforconvenienceoftyping.Maybewecouldoptimizethat.

Thenwelookedathowthesedateswerebeingused.Itturnedoutthatthehugebulkofthemwereallcreatinginstancesofdaterange,anobjectwithafromdateandatodate.Lookingaroundlittlemore,werealizedthatmostofthesedaterangeswereempty!

Asweworkedwithdaterange,weusedtheconventionthatanydaterangethatendedbeforeitstartedwasempty.It’sagoodconventionandfitsinwellwith

howtheclassworks.Soonafterwestartedusingthisconvention,werealizedthatjustcreatingadaterangethatstartsafteritendswasn’tclearcode,soweextractedthatbehaviorintoafactorymethodforemptydateranges.

Wehadmadethatchangetomakethecodeclearer,butwereceivedanunexpectedpayoff.Wecreatedaconstantemptydaterangeandadjustedthefactorymethodtoreturnthatobjectinsteadofcreatingiteverytime.Thatchangedoubledthespeedofthesystem,enoughfortheteststobebearable.Ittookusaboutfiveminutes.

Ihadspeculatedwithvariousmembersoftheteam(KentandMartindenyparticipatinginthespeculation)onwhatwaslikelywrongwithcodeweknewverywell.Wehadevensketchedsomedesignsforimprovementswithoutfirstmeasuringwhatwasgoingon.

Wewerecompletelywrong.Asidefromhavingareallyinterestingconversation,weweredoingnogoodatall.

Thelessonis:Evenifyouknowexactlywhatisgoingoninyoursystem,measureperformance,don’tspeculate.You’lllearnsomething,andninetimesoutoften,itwon’tbethatyouwereright!

—RonJeffries

Theinterestingthingaboutperformanceisthatinmostprograms,mostoftheirtimeisspentinasmallfractionofthecode.IfIoptimizeallthecodeequally,I’llendupwith90percentofmyworkwastedbecauseit’soptimizingcodethatisn’trunmuch.Thetimespentmakingtheprogramfast—thetimelostbecauseoflackofclarity—isallwastedtime.

Thethirdapproachtoperformanceimprovementtakesadvantageofthis90-percentstatistic.Inthisapproach,Ibuildmyprograminawell-factoredmannerwithoutpayingattentiontoperformanceuntilIbeginadeliberateperformanceoptimizationexercise.Duringthisperformanceoptimization,Ifollowaspecificprocesstotunetheprogram.

Ibeginbyrunningtheprogramunderaprofilerthatmonitorstheprogramandtellsmewhereitisconsumingtimeandspace.ThiswayIcanfindthatsmallpartoftheprogramwheretheperformancehotspotslie.IthenfocusonthoseperformancehotspotsusingthesameoptimizationsIwoulduseintheconstant-

attentionapproach.ButsinceI’mfocusingmyattentiononahotspot,I’mgettingmuchmoreeffectwithlesswork.Evenso,Iremaincautious.Asinrefactoring,Imakethechangesinsmallsteps.AftereachstepIcompile,test,andreruntheprofiler.IfIhaven’timprovedperformance,Ibackoutthechange.IcontinuetheprocessoffindingandremovinghotspotsuntilIgettheperformancethatsatisfiesmyusers.

Havingawell-factoredprogramhelpswiththisstyleofoptimizationintwoways.First,itgivesmetimetospendonperformancetuning.Withwell-factoredcode,Icanaddfunctionalitymorequickly.Thisgivesmemoretimetofocusonperformance.(ProfilingensuresIspendthattimeontherightplace.)Second,withawell-factoredprogramIhavefinergranularityformyperformanceanalysis.Myprofilerleadsmetosmallerpartsofthecode,whichareeasiertotune.Withclearercode,Ihaveabetterunderstandingofmyoptionsandofwhatkindoftuningwillwork.

I’vefoundthatrefactoringhelpsmewritefastsoftware.ItslowsthesoftwareintheshorttermwhileI’mrefactoring,butmakesiteasiertotuneduringoptimization.Iendupwellahead.

WhereDidRefactoringComeFrom?

I’venotsucceededinpinningdownthebirthoftheterm“refactoring.”Goodprogrammershavealwaysspentatleastsometimecleaninguptheircode.Theydothisbecausetheyhavelearnedthatcleancodeiseasiertochangethancomplexandmessycode,andgoodprogrammersknowthattheyrarelywritecleancodethefirsttimearound.

Refactoringgoesbeyondthis.Inthisbook,I’madvocatingrefactoringasakeyelementinthewholeprocessofsoftwaredevelopment.TwoofthefirstpeopletorecognizetheimportanceofrefactoringwereWardCunninghamandKentBeck,whoworkedwithSmalltalkfromthe1980sonward.Smalltalkisanenvironmentthateventhenwasparticularlyhospitabletorefactoring.Itisaverydynamicenvironmentthatallowsyoutoquicklywritehighlyfunctionalsoftware.Smalltalkhadaveryshortcompile-link-executecycleforitstime,whichmadeiteasytochangethingsquicklyatatimewhereovernightcompilecycleswerenotunknown.Itisalsoobject-orientedandthusprovidespowerfultoolsforminimizingtheimpactofchangebehindwell-definedinterfaces.WardandKent

exploredsoftwaredevelopmentapproachesgearedtothiskindofenvironment,andtheirworkdevelopedintoExtremeProgramming.Theyrealizedthatrefactoringwasimportantinimprovingtheirproductivityand,eversince,havebeenworkingwithrefactoring,applyingittoserioussoftwareprojectsandrefiningit.

WardandKent’sideaswereastronginfluenceontheSmalltalkcommunity,andthenotionofrefactoringbecameanimportantelementintheSmalltalkculture.AnotherleadingfigureintheSmalltalkcommunityisRalphJohnson,aprofessorattheUniversityofIllinoisatUrbana-Champaign,whoisfamousasoneoftheauthorsoftheGangofFour[bib-gof]bookondesignpatterns.OneofRalph’sbiggestinterestsisindevelopingsoftwareframeworks.Heexploredhowrefactoringcanhelpdevelopanefficientandflexibleframework.

BillOpdykewasoneofRalph’sdoctoralstudentsandwasparticularlyinterestedinframeworks.HesawthepotentialvalueofrefactoringandsawthatitcouldbeappliedtomuchmorethanSmalltalk.Hisbackgroundwasintelephoneswitchdevelopment,inwhichagreatdealofcomplexityaccruesovertimeandchangesaredifficulttomake.Bill’sdoctoralresearchlookedatrefactoringfromatoolbuilder’sperspective.BillwasinterestedinrefactoringsthatwouldbeusefulforC++frameworkdevelopment;heresearchedthenecessarysemantics-preservingrefactoringsandshowedhowtoprovetheyweresemantics-preservingandhowatoolcouldimplementtheseideas.Bill’sdoctoralthesis[bib-opdyke]wasthefirstsubstantialworkonrefactoring.

IremembermeetingBillattheOOPSLAconferencein1992.Wesatinacaféandhetoldmeabouthisresearch.Irememberthinking,“Interesting,butnotreallythatimportant.”Boy,wasIwrong!

JohnBrantandDonRobertstooktherefactoringtoolideasmuchfurthertoproducetheRefactoringBrowser,thefirstrefactoringtool,appropriatelyfortheSmalltalkenvironment.

Andme?I’dalwaysbeeninclinedtocleancode,butI’dneverconsideredittobethatimportant.Then,IworkedonaprojectwithKentandsawthewayheusedrefactoring.Isawthedifferenceitmadeinproductivityandquality.Thatexperienceconvincedmethatrefactoringwasaveryimportanttechnique.Iwasfrustrated,however,becausetherewasnobookthatIcouldgivetoaworkingprogrammer,andnoneoftheexpertsabovehadanyplanstowritesuchabook.

So,withtheirhelp,Idid—whichledtothefirsteditionofthisbook.

Fortunately,theconceptofrefactoringcaughtonintheindustry.Thebooksoldwell,andrefactoringenteredthevocabularyofmostprogrammers.Moretoolsappeared,especiallyforJava.Onedownsideofthispopularityhasbeenpeopleusing“refactoring”loosely,tomeananykindofrestructuring.Despitethis,however,ithasbecomeamainstreampractice.

AutomatedRefactorings

Perhapsthebiggestchangetorefactoringinthelastdecadeorsoistheavailabilityoftoolsthatsupportautomatedrefactoring.IfIwanttorenameamethodinJavaandI’musingIntelliJIDEA[bib-intellij]orEclipse[bib-eclipse](tomentionjusttwo),Icandoitbypickinganitemoffthemenu.Thetoolcompletestherefactoringforme—andI’musuallysufficientlyconfidentinitsworkthatIdon’tbotherrunningthetestsuite.

ThefirsttoolthatdidthiswastheSmalltalkRefactoringBrowser,writtenbyJohnBrandtandDonRoberts.TheideatookoffintheJavacommunityveryrapidlyatthebeginningofthecentury.WhenJetBrainslaunchedtheirIntelliJIDEAIDE,automatedrefactoringwasoneofthecompellingfeatures.IBMfollowedsuitshortlyafterwardswithrefactoringtoolsinVisualAgeforJava.VisualAgedidn’thaveabigimpact,butmuchofitscapabilitieswerereimplementedinEclipse,includingtherefactoringsupport.

RefactoringalsocametoC#,initiallyviaJetBrains’sResharper,aplug-inforVisualStudio.Lateron,theVisualStudioteamaddedsomerefactoringcapabilities.

It’snowprettycommontofindsomekindofrefactoringsupportineditorsandtools,althoughtheactualcapabilitiesvaryafairbit.Someofthisvariationisduetothetool,someiscausedbythelimitationsofwhatyoucandowithautomatedrefactoringindifferentlanguages.I’mnotgoingtoanalyzethecapabilitiesofdifferenttoolshere,butIthinkitisworthtalkingabitaboutsomeoftheunderlyingprinciples.

Acrudewaytoautomatearefactoringistodotextmanipulation,suchasasearch/replacetochangeaname,orsomesimplereorganizingofcodeforExtractVariable(119).Thisisaverycrudeapproachthatcertainlycan’tbe

trustedwithoutre-runningtests.Itcan,however,beahandyfirststep.I’llusesuchmacrosinEmacstospeedupmyrefactoringworkwhenIdon’thavemoresophisticatedrefactoringsavailabletome.

Todorefactoringproperly,thetoolhastooperateonthesyntaxtreeofthecode,notonthetext.Manipulatingthesyntaxtreeismuchmorereliabletopreservewhatthecodeisdoing.Thisiswhyatthemoment,mostrefactoringcapabilitiesarepartofpowerfulIDEs—theyusethesyntaxtreenotjustforrefactoringbutalsoforcodenavigation,linting,andthelike.Thiscollaborationbetweentextandsyntaxtreeiswhattakesthembeyondtexteditors.

Refactoringisn’tjustunderstandingandupdatingthesyntaxtree.Thetoolalsoneedstofigureouthowtore-renderthecodeintotextbackintheeditorview.Allinall,implementingdecentrefactoringisachallengingprogrammingexercise—onethatI’mmostlyunawareofasIgailyusethetools.

Manyrefactoringsaremademuchsaferwhenappliedinalanguagewithstatictyping.ConsiderthesimpleChangeFunctionDeclaration(124).ImighthaveaddClientmethodsonmySalesmanclassandonmyServerclass.Iwanttorenametheoneonmysalesman,butitisdifferentinintentfromtheoneonmyserver,whichIdon’twanttorename.Withoutstatictyping,thetoolwillfinditdifficulttotellwhetheranycalltoaddClientisintendedforthesalesman.Intherefactoringbrowser,itwouldgeneratealistofcallsitesandIwouldmanuallydecidewhichonestochange.Thismakesitanon-saferefactoringthatforcesmetorerunthetests.Suchatoolisstillhelpful—buttheequivalentoperationinJavacanbecompletelysafeandautomatic.Sincethetoolcanresolvethemethodtothecorrectclasswithstatictyping,Icanbeconfidentthatthetoolchangesonlythemethodsitoughtto.

Toolsoftengofurther.IfIrenameavariable,Icanbepromptedforchangestocommentsthatusethatname.IfIuseExtractFunction(106),thetoolspotssomecodethatduplicatesthenewfunction’sbodyandofferstoreplaceitwithacall.ProgrammingwithpowerfulrefactoringslikethisisacompellingreasontouseanIDEratherthanstickwithafamiliartexteditor.PersonallyI’mabiguserofEmacs,butwhenworkinginJavaIpreferIntelliJIDEAorEclipse—inlargepartduetotherefactoringsupport.

Whilesophisticatedrefactoringtoolsarealmostmagicalintheirabilitytosafelyrefactorcode,therearesomeedgecaseswheretheyslipup.Lessmaturetools

strugglewithreflectivecalls,suchasMethod.invokeinJava(althoughmorematuretoolshandlethisquitewell).Soevenwithmostly-saferefactorings,it’swisetorunthetestsuiteeverysooftentoensurenothinghasgonepear-shaped.UsuallyI’mrefactoringwithamixofautomatedandmanualrefactorings,soIrunmytestsoftenenough.

ThepowerofusingthesyntaxtreetoanalyzeandrefactorprogramsisacompellingadvantageforIDEsoversimpletexteditors,butmanyprogrammersprefertheflexibilityoftheirfavoritetexteditorandwouldliketohaveboth.Atechnologythat’scurrentlygainingmomentumisLanguageServers[bib-lang-server]:softwarethatwillformasyntaxtreeandpresentanAPItotexteditors.Suchlanguageserverscansupportmanytexteditorsandprovidecommandstodosophisticatedcodeanalysisandrefactoringoperations.

GoingFurther

Itseemsalittlestrangetobetalkingaboutfurtherreadinginonlythesecondchapter,butthisisasgoodaspotasanytopointoutthereismorematerialoutthereonrefactoringthatgoesbeyondthebasicsinthisbook.

Thisbookhastaughtrefactoringtomanypeople,butIhavefocusedmoreonarefactoringreferencethanontakingreadersthroughthelearningprocess.Ifyouarelookingforsuchabook,IsuggestBillWake’sRefactoringWorkbook[bib-wake-workbook]thatcontainsmanyexercisestopracticerefactoring.

Manyofthosewhopioneeredrefactoringwerealsoactiveinthesoftwarepatternscommunity.JoshKerievskytiedthesetwoworldscloselytogetherwithRefactoringtoPatterns[bib-r2p],whichlooksatthemostvaluablepatternsfromthehugelyinfluential“GangofFour”book[bib-gof]andshowshowtouserefactoringtoevolvetowardsthem.

Thisbooksconcentratesonrefactoringingeneral-purposeprogramming,butrefactoringalsoappliesinspecializedareas.TwothathavegotusefulattentionareDatabaseRefactoring[bib-refact-db](byScottAmblerandPramodSadalage)andRefactoringHTML[bib-refact-html](byElliotteRustyHarold).

Althoughitdoesn’thaverefactoringinthetitle,alsoworthincludingisMichaelFeathers’sWorkingEffectivelywithLegacyCode[bib-feathers-welc],whichisprimarilyabookabouthowtothinkaboutrefactoringanoldercodebasewith

poortestcoverage.

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

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

Chapter3BadSmellsinCodebyKentBeckandMartinFowler

Ifitstinks,changeit.—GrandmaBeck,discussingchild-rearingphilosophy

Bynowyouhaveagoodideaofhowrefactoringworks.Butjustbecauseyouknowhowdoesn’tmeanyouknowwhen.Decidingwhentostartrefactoring—andwhentostop—isjustasimportanttorefactoringasknowinghowtooperatethemechanicsofit.

Nowcomesthedilemma.Itiseasytoexplainhowtodeleteaninstancevariableorcreateahierarchy.Thesearesimplematters.Tryingtoexplainwhenyoushoulddothesethingsisnotsocut-and-dried.Insteadofappealingtosomevaguenotionofprogrammingaesthetics(which,frankly,iswhatweconsultantsusuallydo),Iwantedsomethingabitmoresolid.

WhenIwaswritingthefirsteditionofthisbook,IwasmullingoverthisissuewhenIvisitedKentBeckinZurich.Perhapshewasundertheinfluenceoftheodorsofhisnewborndaughteratthetime,buthehadcomeupwiththenotionofdescribingthe“when”ofrefactoringintermsofsmells.

“Smells,”yousay,“andthatissupposedtobebetterthanvagueaesthetics?”Well,yes.Wehavelookedatlotsofcode,writtenforprojectsthatspanthegamutfromwildlysuccessfultonearlydead.Indoingso,wehavelearnedtolookforcertainstructuresinthecodethatsuggest—sometimes,screamfor—thepossibilityofrefactoring.(Weareswitchingoverto“we”inthischaptertoreflectthefactthatKentandIwrotethischapterjointly.Youcantellthedifferencebecausethefunnyjokesaremineandtheothersarehis.)

Onethingwewon’ttrytogiveyouisprecisecriteriaforwhenarefactoringisoverdue.Inourexperience,nosetofmetricsrivalsinformedhumanintuition.Whatwewilldoisgiveyouindicationsthatthereistroublethatcanbesolvedbyarefactoring.Youwillhavetodevelopyourownsenseofhowmanyinstancevariablesorhowmanylinesofcodeinamethodaretoomany.

Usethischapterandthetableontheinsidebackcoverasawaytogiveyouinspirationwhenyou’renotsurewhatrefactoringstodo.Readthechapter(orskimthetable)andtrytoidentifywhatitisyou’resmelling,thengototherefactoringswesuggesttoseewhethertheywillhelpyou.Youmaynotfindtheexactsmellyoucandetect,buthopefullyitshouldpointyouintherightdirection.

MysteriousName

Puzzlingoversometexttounderstandwhat’sgoingonisagreatthingifyou’rereadingadetectivenovel,butnotwhenyou’rereadingcode.WemayfantasizeaboutbeingInternationalMenofMystery,butourcodeneedstobemundaneandclear.Oneofthemostimportantpartsofclearcodeisgoodnames,soweputalotofthoughtintonamingfunctions,modules,variables,classes,sotheyclearlycommunicatewhattheydoandhowtousethem.

Sadly,however,namingisoneofthetwohardthings[bib-2hard]inprogramming.So,perhapsthemostcommonrefactoringswedoaretherenames:ChangeFunctionDeclaration(124)(torenameafunction),RenameVariable(137),andRenameField(244).Peopleareoftenafraidtorenamethings,thinkingit’snotworththetrouble,butagoodnamecansavehoursofpuzzledincomprehensioninthefueture.

Renamingisnotjustanexerciseinchangingnames.Whenyoucan’tthinkofagoodnameforsomething,it’softenasignofadeeperdesignmalaise.Puzzlingoveratrickynamehasoftenledustosignificantsimplificationstoourcode.

DuplicatedCode

Ifyouseethesamecodestructureinmorethanoneplace,youcanbesurethatyourprogramwillbebetterifyoufindawaytounifythem.Duplicationmeansthateverytimeyoureadthesecopies,youneedtoreadthemcarefullytoseeifthere’sanydifference.Ifyouneedtochangetheduplicatedcode,youhavetofindandcatcheachduplication.

Thesimplestduplicatedcodeproblemiswhenyouhavethesameexpressionintwomethodsofthesameclass.ThenallyouhavetodoisExtractFunction(106)andinvokethecodefrombothplaces.Ifyouhavecodethat’ssimilar,but

notquiteidentical,seeifyoucanuseSlideStatements(221)toarrangethecodesothesimilaritemsarealltogetherforeasyextraction.Iftheduplicatefragmentsareinsubclassesofacommonbaseclass,youcanusePullUpMethod(348)toavoidcallingonefromanother.

LongFunction

Inourexperience,theprogramsthatlivebestandlongestarethosewithshortfunctions.Programmersnewtosuchacodebaseoftenfeelthatnocomputationevertakesplace—thattheprogramisanendlesssequenceofdelegation.Whenyouhavelivedwithsuchaprogramforafewyears,however,youlearnjusthowvaluableallthoselittlefunctionsare.Allofthepayoffsofindirection—explanation,sharing,andchoosing—aresupportedbysmallfunctions

Sincetheearlydaysofprogramming,peoplehaverealizedthatthelongerafunctionis,themoredifficultitistounderstand.Olderlanguagescarriedanoverheadinsubroutinecalls,whichdeterredpeoplefromsmallfunctions.Modernlanguageshaveprettymucheliminatedthatoverheadforin-processcalls.Thereisstilloverheadforthereaderofthecodebecauseyouhavetoswitchcontexttoseewhatthefunctiondoes.Developmentenvironmentsthatallowyoutoquicklyjumpbetweenafunctioncallanditsdeclaration,ortoseebothfunctionsatonce,helpeliminatethisstep,buttherealkeytomakingiteasytounderstandsmallfunctionsisgoodnaming.Ifyouhaveagoodnameforafunction,youmostlydon’tneedtolookatitsbody.

Theneteffectisthatyoushouldbemuchmoreaggressiveaboutdecomposingfunctions.Aheuristicwefollowisthatwheneverwefeeltheneedtocommentsomething,wewriteafunctioninstead.Suchafunctioncontainsthecodethatwewantedtocommentbutisnamedaftertheintentionofthecoderatherthanthewayitworks.Wemaydothisonagroupoflinesorevenonasinglelineofcode.Wedothisevenifthemethodcallislongerthanthecodeitreplaces—providedthemethodnameexplainsthepurposeofthecode.Thekeyhereisnotfunctionlengthbutthesemanticdistancebetweenwhatthemethoddoesandhowitdoesit.

Ninety-ninepercentofthetime,allyouhavetodotoshortenafunctionisExtractFunction(106).Findpartsofthefunctionthatseemtogonicelytogetherandmakeanewone.

Ifyouhaveafunctionwithlotsofparametersandtemporaryvariables,theygetinthewayofextracting.IfyoutrytouseExtractFunction(106),youenduppassingsomanyparameterstotheextractedmethodthattheresultisscarcelymorereadablethantheoriginal.YoucanoftenuseReplaceTempwithQuery(176)toeliminatethetemps.LonglistsofparameterscanbeslimmeddownwithIntroduceParameterObject(140)andPreserveWholeObject(317).

Ifyou’vetriedthatandyoustillhavetoomanytempsandparameters,it’stimetogetouttheheavyartillery:ReplaceFunctionwithCommand(335).

Howdoyouidentifytheclumpsofcodetoextract?Agoodtechniqueistolookforcomments.Theyoftensignalthiskindofsemanticdistance.Ablockofcodewithacommentthattellsyouwhatitisdoingcanbereplacedbyamethodwhosenameisbasedonthecomment.Evenasinglelineisworthextractingifitneedsexplanation.

Conditionalsandloopsalsogivesignsforextractions.UseDecomposeConditional(260)todealwithconditionalexpressions.AbigswitchstatementshouldhaveitslegsturnedintosinglefunctioncallswithExtractFunction(106).Ifthere’smorethanoneswitchstatementswitchingonthesamecondition,youshouldapplyReplaceConditionalwithPolymorphism(271).

Withloops,extracttheloopandthecodewithintheloopintoitsownmethod.Ifyoufindithardtogiveanextractedloopaname,thatmaybebecauseit’sdoingtwodifferentthings—inwhichcasedon’tbeafraidtouseSplitLoop(226)tobreakouttheseparatetasks.

LongParameterList

Inourearlyprogrammingdays,weweretaughttopassinasparameterseverythingneededbyafunction.Thiswasunderstandablebecausethealternativewasglobaldata,andglobaldataquicklybecomesevil.Butlongparameterlistsareoftenconfusingintheirownright.

Ifyoucanobtainoneparameterbyaskinganotherparameterforit,youcanuseReplaceParameterwithQuery(322)toremovethesecondparameter.Ratherthanpullinglotsofdataoutofanexistingdatastructure,youcanusePreserveWholeObject(317)topasstheoriginaldatastructureinstead.Ifseveralparametersalwaysfittogether,combinethemwithIntroduceParameterObject

(140).Ifaparameterisusedasaflagtodispatchdifferentbehavior,useRemoveFlagArgument(312).

Classesareagreatwaytoreduceparameterlistsizes.Theyareparticularlyusefulwhenmultiplefunctionsshareseveralparametervalues.Then,youcanuseCombineFunctionsintoClass(144)tocapturethosecommonvaluesasfields.Ifweputonourfunctionalprogramminghats,we’dsaythiscreatesasetofpartiallyappliedfunctions.

GlobalData

Sinceourearliestdaysofwritingsoftware,wewerewarnedoftheperilsofglobaldata—howitwasinventedbydemonsfromthefourthplaneofhell,whichistherestingplaceofanyprogrammerwhodarestouseit.And,althoughwearesomewhatskepticalaboutfireandbrimstone,it’sstilloneofthemostpungentodorswearelikelytoruninto.Theproblemwithglobaldataisthatitcanbemodifiedfromanywhereinthecodebase,andthere’snomechanismtodiscoverwhichbitofcodetouchedit.Timeandagain,thisleadstobugsthatbreedfromaformofspookyactionfromadistance—andit’sveryhardtofindoutwheretheerrantbitofprogramis.Themostobviousformofglobaldataisglobalvariables,butwealsoseethisproblemwithclassvariablesandsingletons.

OurkeydefensehereisEncapsulateVariable(132),whichisalwaysourfirstmovewhenconfrontedwithdatathatisopentocontaminationbyanypartofaprogram.Atleastwhenyouhaveitwrappedbyafunction,youcanstartseeingwhereit’smodifiedandstarttocontrolitsaccess.Then,it’sgoodtolimititsscopeasmuchaspossiblebymovingitwithinaclassormodulewhereonlythatmodule’scodecanseeit.

Globaldataisespeciallynastywhenit’smutable.Globaldatathatyoucanguaranteeneverchangesaftertheprogramstartsisrelativelysafe—ifyouhavealanguagethatcanenforcethatguarantee.

GlobaldataillustratesParacelsus’smaxim:Thedifferencebetweenapoisonandsomethingbenignisthedose.Youcangetawaywithsmalldosesofglobaldata,butitgetsexponentiallyhardertodealwiththemoreyouhave.Evenwithlittlebits,weliketokeepitencapsulated—that’sthekeytocopingwithchangesasthesoftwareevolves.

MutableData

Changestodatacanoftenleadtounexpectedconsequencesandtrickybugs.Icanupdatesomedatahere,notrealizingthatanotherpartofthesoftwareexpectssomethingdifferentandnowfails—afailurethat’sparticularlyhardtospotifitonlyhappensunderrareconditions.Forthisreason,anentireschoolofsoftwaredevelopment—functionalprogramming—isbasedonthenotionthatdatashouldneverchangeandthatupdatingadatastructureshouldalwaysreturnanewcopyofthestructurewiththechange,leavingtheolddatapristine.

Thesekindsoflanguages,however,arestillarelativelysmallpartofprogramming;manyofusworkinlanguagesthatallowvariablestovary.Butthisdoesn’tmeanweshouldignoretheadvantagesofimmutability—therearestillmanythingswecandotolimittherisksonunrestricteddataupdates.

YoucanuseEncapsulateVariable(132)toensurethatallupdatesoccurthroughnarrowfunctionsthatcanbeeasiertomonitorandevolve.Ifavariableisbeingupdatedtostoredifferentthings,useSplitVariable(240)bothtokeepthemseparateandavoidtheriskyupdate.TryasmuchaspossibletomovelogicoutofcodethatprocessestheupdatebyusingSlideStatements(221)andExtractFunction(106)toseparatetheside-effect-freecodefromanythingthatperformstheupdate.InAPIs,useSeparateQueryfromModifier(304)toensurecallersdon’tneedtocallcodethathassideeffectsunlesstheyreallyneedto.WeliketouseRemoveSettingMethod(329)assoonaswecan—sometimes,justtryingtofindclientsofasetterhelpsspotopportunitiestoreducethescopeofavariable.

Mutabledatathatcanbecalculatedelsewhereisparticularlypungent.It’snotjustarichsourceofconfusion,bugs,andmisseddinnersathome—it’salsounnecessary.WesprayitwithaconcentratedsolutionofvinegarandReplaceDerivedVariablewithQuery(248).

Mutabledataisn’tabigproblemwhenit’savariablewhosescopeisjustacoupleoflines—butitsriskincreasesasitsscopegrows.UseCombineFunctionsintoClass(144)orCombineFunctionsintoTransform(149)tolimithowmuchcodeneedstoupdateavariable.Ifavariablecontainssomedatawithinternalstructure,it’susuallybettertoreplacetheentirestructureratherthanmodifyitinplace,usingChangeReferencetoValue(252).

DivergentChange

Westructureoursoftwaretomakechangeeasier;afterall,softwareismeanttobesoft.Whenwemakeachange,wewanttobeabletojumptoasingleclearpointinthesystemandmakethechange.Whenyoucan’tdothis,youaresmellingoneoftwocloselyrelatedpungencies.

Divergentchangeoccurswhenonemoduleisoftenchangedindifferentwaysfordifferentreasons.Ifyoulookatamoduleandsay,“Well,IwillhavetochangethesethreefunctionseverytimeIgetanewdatabase;Ihavetochangethesefourfunctionseverytimethereisanewfinancialinstrument,”thisisanindicationofdivergentchange.Thedatabaseinteractionandfinancialprocessingproblemsareseparatecontexts,andwecanmakeourprogramminglifebetterbymovingsuchcontextsintoseparatemodules.Thatway,whenwehaveachangetoonecontext,weonlyhavetounderstandthatonecontextandignoretheother.Wealwaysfoundthistobeimportant,butnow,withourbrainsshrinkingwithage,itbecomesallthemoreimperative.Ofcourse,youoftendiscoverthisonlyafteryou’veaddedafewdatabasesorfinancialinstruments;contextboundariesareusuallyunclearintheearlydaysofaprogramandcontinuetoshiftasasoftwaresystem’scapabilitieschange.

Ifthetwoaspectsnaturallyformasequence—forexample,yougetdatafromthedatabaseandthenapplyyourfinancialprocessingonit—thenSplitPhase(154)separatesthetwowithacleardatastructurebetweenthem.Ifthere’smoreback-and-forthinthecalls,thencreateappropriatemodulesanduseMoveFunction(196)todividetheprocessingup.Iffunctionsmixthetwotypesofprocessingwithinthemselves,useExtractFunction(106)toseparatethembeforemoving.Ifthemodulesareclasses,thenExtractClass(180)helpsformalizehowtodothesplit.

ShotgunSurgery

Shotgunsurgeryissimilartodivergentchangebutistheopposite.Youwhiffthiswhen,everytimeyoumakeachange,youhavetomakealotoflittleeditstoalotofdifferentclasses.Whenthechangesareallovertheplace,theyarehardtofind,andit’seasytomissanimportantchange.

Inthiscase,youwanttouseMoveFunction(196)andMoveField(205)toput

allthechangesintoasinglemodule.Ifyouhaveabunchoffunctionsoperatingonsimilardata,useCombineFunctionsintoClass(144).Ifyouhavefunctionsthataretransformingorenrichingadatastructure,useCombineFunctionsintoTransform(149).SplitPhase(154)isoftenusefulhereifthecommonfunctionscancombinetheiroutputforaconsumingphaseoflogic.

Ausefultacticforshotgunsurgeryistouseinliningrefactorings,suchasInlineFunction(115)orInlineClass(184),topulltogetherpoorlyseparatedlogic.You’llendupwithaLongMethodoraLargeClass,butcanthenuseextractionstobreakitupintomoresensiblepieces.Eventhoughweareinordinatelyfondofsmallfunctionsandclassesinourcode,wearen’tafraidofcreatingsomethinglargeasanintermediatesteptoreorganization.

FeatureEnvy

Whenwemodularizeaprogram,wearetryingtoseparatethecodeintozonestomaximizetheinteractioninsideazoneandminimizeinteractionbetweenzones.AclassiccaseofFeatureEnvyoccurswhenafunctioninonemodulespendsmoretimecommunicatingwithfunctionsordatainsideanothermodulethanitdoeswithinitsownmodule.We’velostcountofthetimeswe’veseenafunctioninvokinghalf-a-dozengettermethodsonanotherobjecttocalculatesomevalue.Fortunately,thecureforthatcaseisobvious:Thefunctionclearlywantstobewiththedata,souseMoveFunction(196)togetitthere.Sometimes,onlyapartofafunctionsuffersfromenvy,inwhichcaseuseExtractFunction(106)onthejealousbit,andMoveFunction(196)togiveitadreamhome.

Ofcoursenotallcasesarecut-and-dried.Often,afunctionusesfeaturesofseveralmodules,sowhichoneshoulditlivewith?Theheuristicweuseistodeterminewhichmodulehasmostofthedataandputthefunctionwiththatdata.ThisstepisoftenmadeeasierifyouuseExtractFunction(106)tobreakthefunctionintopiecesthatgointodifferentplaces.

Ofcourse,thereareseveralsophisticatedpatternsthatbreakthisrule.FromtheGangofFour[bib-gof],StrategyandVisitorimmediatelyleaptomind.KentBeck’sSelfDelegation[bib-beck-sbpp]isanother.Usethesetocombatthedivergentchangesmell.Thefundamentalruleofthumbistoputthingstogetherthatchangetogether.Dataandthebehaviorthatreferencesthatdatausuallychangetogether—butthereareexceptions.Whentheexceptionsoccur,wemove

thebehaviortokeepchangesinoneplace.StrategyandVisitorallowyoutochangebehavioreasilybecausetheyisolatethesmallamountofbehaviorthatneedstobeoverridden,atthecostoffurtherindirection.

DataClumps

Dataitemstendtobelikechildren:Theyenjoyhangingaroundtogether.Often,you’llseethesamethreeorfourdataitemstogetherinlotsofplaces:asfieldsinacoupleofclasses,asparametersinmanymethodsignatures.Bunchesofdatathathangaroundtogetherreallyoughttofindahometogether.Thefirststepistolookforwheretheclumpsappearasfields.UseExtractClass(180)onthefieldstoturntheclumpsintoanobject.ThenturnyourattentiontomethodsignaturesusingIntroduceParameterObject(140)orPreserveWholeObject(317)toslimthemdown.Theimmediatebenefitisthatyoucanshrinkalotofparameterlistsandsimplifymethodcalling.Don’tworryaboutdataclumpsthatuseonlysomeofthefieldsofthenewobject.Aslongasyouarereplacingtwoormorefieldswiththenewobject,you’llcomeoutahead.

Agoodtestistoconsiderdeletingoneofthedatavalues.Ifyoudidthis,wouldtheothersmakeanysense?Iftheydon’t,it’sasuresignthatyouhaveanobjectthat’sdyingtobeborn.

You’llnoticethatweadvocatecreatingaclasshere,notasimplerecordstructure.Wedothisbecauseusingaclassgivesyoutheopportunitytomakeaniceperfume.Youcannowlookforcasesoffeatureenvy,whichwillsuggestbehaviorthatcanbemovedintoyournewclasses.We’veoftenseenthisasapowerfuldynamicthatcreatesusefulclassesandcanremovealotofduplicationandacceleratefuturedevelopment,allowingthedatatobecomeproductivemembersofsociety.

PrimitiveObsession

Mostprogrammingenvironmentsarebuiltonawidelyusedsetofprimitivetypes:integers,floatingpointnumbers,andstrings.Librariesmayaddsomeadditionalsmallobjectssuchasdates.Wefindmanyprogrammersarecuriouslyreluctanttocreatetheirownfundamentaltypeswhichareusefulfortheirdomain—suchasmoney,coordinates,orranges.Wethusseecalculationsthattreatmonetaryamountsasplainnumbers,orcalculationsofphysicalquantitiesthat

ignoreunits(addinginchestomillimeters),orlotsofcodedoingif(a<upper&&a>lower).

Stringsareparticularlycommonpetridishesforthiskindofodor:Atelephonenumberismorethanjustacollectionofcharacters.Ifnothingelse,apropertypecanoftenincludeconsistentdisplaylogicforwhenitneedstobedisplayedinauserinterface.Representingsuchtypesasstringsissuchacommonstenchthatpeoplecallthem“stringlytyped”variables.

YoucanmoveoutoftheprimitivecaveintothecentrallyheatedworldofmeaningfultypesbyusingReplacePrimitivewithObject(172).Iftheprimitiveisatypecodecontrollingconditionalbehavior,useReplaceTypeCodewithSubclasses(361)followedbyReplaceConditionalwithPolymorphism(271).

GroupsofprimitivesthatcommonlyappeartogetheraredataclumpsandshouldbecivilizedwithExtractClass(180)andIntroduceParameterObject(140).

RepeatedSwitches

Talktoatrueobject-orientedevangelistandthey’llsoongetontotheevilsofswitchstatements.They’llarguethatanyswitchstatementyouseeisbeggingforReplaceConditionalwithPolymorphism(271).We’veevenheardsomepeoplearguethatallconditionallogicshouldbereplacedwithpolymorphism,tossingmostifsintothedustbinofhistory.

Eveninourmorewild-eyedyouth,wewereneverunconditionallyopposedtotheconditional.Indeed,thefirsteditionofthisbookhadasmellentitled“switchstatements.”Thesmellwastherebecauseinthelate90’swefoundpolymorphismsadlyunderappreciated,andsawbenefitingettingpeopletoswitchover.

Thesedaysthereismorepolymorphismabout,anditisn’tthesimpleredflagthatitoftenwasfifteenyearsago.Furthermore,manylanguagessupportmoresophisticatedformsofswitchstatementsthatusemorethansomeprimitivecodeastheirbase.Sowenowfocusontherepeatedswitch,wherethesameconditionalswitchinglogic(eitherinaswitch/casestatementorinacascadeofif/elsestatements)popsupindifferentplaces.Theproblemwithsuchduplicateswitchesisthat,wheneveryouaddaclause,youhavetofindalltheswitchesandupdatethem.Againstthedarkforcesofsuchrepetition,polymorphism

providesanelegantweaponforamorecivilizedcodebase.

Loops

Loopshavebeenacorepartofprogrammingsincetheearliestlanguages.Butwefeeltheyarenomorerelevanttodaythanbell-bottomsandflockwallpaper.Wedisdainedthematthetimeofthefirstedition—butJava,likemostotherlanguagesatthetime,didn’tprovideabetteralternative.Thesedays,however,first-classfunctionsarewidelysupported,sowecanuseReplaceLoopwithPipeline(230)toretirethoseanachronisms.Wefindthatpipelineoperations,suchasfilterandmap,helpusquicklyseetheelementsthatareincludedintheprocessingandwhatisdonewiththem.

LazyElement

Welikeusingprogramelementstoaddstructure—providingopportunitiesforvariation,reuse,orjusthavingmorehelpfulnames.Butsometimesthestructureisn’tneeded.Itmaybeafunctionthat’snamedthesameasitsbodycodereads,oraclassthatisessentiallyonesimplefunction.Sometimes,thisreflectsafunctionthatwasexpectedtogrowandbepopularlater,butneverrealizeditsdreams.Sometimes,it’saclassthatusedtopayitsway,buthasbeendownsizedwithrefactoring.Eitherway,suchprogramelementsneedtodiewithdignity.UsuallythismeansusingInlineFunction(115)orInlineClass(184).Withinheritance,youcanuseCollapseHierarchy(378).

SpeculativeGenerality

BrianFootesuggestedthisnameforasmelltowhichweareverysensitive.Yougetitwhenpeoplesay,“Oh,Ithinkwe’llneedtheabilitytodothiskindofthingsomeday”andthusaddallsortsofhooksandspecialcasestohandlethingsthataren’trequired.Theresultisoftenhardertounderstandandmaintain.Ifallthismachinerywerebeingused,itwouldbeworthit.Butifitisn’t,itisn’t.Themachineryjustgetsintheway,sogetridofit.

Ifyouhaveabstractclassesthataren’tdoingmuch,useCollapseHierarchy(378).UnnecessarydelegationcanberemovedwithInlineFunction(115)andInlineClass(184).Functionswithunusedparametersshouldbesubjectto

ChangeFunctionDeclaration(124)toremovethoseparameters.YoushouldalsoapplyChangeFunctionDeclaration(124)toremoveanyunneededparameters,whichoftengettossedinforfuturevariationsthatnevercometopass.

Speculativegeneralitycanbespottedwhentheonlyusersofafunctionorclassaretestcases.Ifyoufindsuchananimal,deletethetestcaseandapplyRemoveDeadCode(236).

TemporaryField

Sometimesyouseeaclassinwhichafieldissetonlyincertaincircumstances.Suchcodeisdifficulttounderstand,becauseyouexpectanobjecttoneedallofitsfields.Tryingtounderstandwhyafieldistherewhenitdoesn’tseemtobeusedcandriveyounuts.

UseExtractClass(180)tocreateahomeforthepoororphanvariables.UseMoveFunction(196)toputallthecodethatconcernsthefieldsintothisnewclass.YoumayalsobeabletoeliminateconditionalcodebyusingIntroduceSpecialCase(287)tocreateanalternativeclassforwhenthevariablesaren’tvalid.

MessageChains

Youseemessagechainswhenaclientasksoneobjectforanotherobject,whichtheclientthenasksforyetanotherobject,whichtheclientthenasksforyetanotheranotherobject,andsoon.YoumayseetheseasalonglineofgetThismethods,orasasequenceoftemps.Navigatingthiswaymeanstheclientiscoupledtothestructureofthenavigation.Anychangetotheintermediaterelationshipscausestheclienttohavetochange.

ThemovetousehereisHideDelegate(187).Youcandothisatvariouspointsinthechain.Inprinciple,youcandothistoeveryobjectinthechain,butdoingthisoftenturnseveryintermediateobjectintoamiddleman.Often,abetteralternativeistoseewhattheresultingobjectisusedfor.SeewhetheryoucanuseExtractFunction(106)totakeapieceofthecodethatusesitandthenMoveFunction(196)topushitdownthechain.Ifseveralclientsofoneoftheobjectsinthechainwanttonavigatetherestoftheway,addamethodtodothat.

Somepeopleconsideranymethodchaintobeaterriblething.Weareknownforourcalm,reasonedmoderation.Well,atleastinthiscaseweare.

MiddleMan

Oneoftheprimefeaturesofobjectsisencapsulation—hidinginternaldetailsfromtherestoftheworld.Encapsulationoftencomeswithdelegation.Youaskadirectorwhethersheisfreeforameeting;shedelegatesthemessagetoherdiaryandgivesyouananswer.Allwellandgood.Thereisnoneedtoknowwhetherthedirectorusesadiary,anelectronicgizmo,orasecretarytokeeptrackofherappointments.

However,thiscangotoofar.Youlookataclass’sinterfaceandfindhalfthemethodsaredelegatingtothisotherclass.Afterawhile,itistimetouseRemoveMiddleMan(190)andtalktotheobjectthatreallyknowswhat’sgoingon.Ifonlyafewmethodsaren’tdoingmuch,useInlineFunction(115)toinlinethemintothecaller.Ifthereisadditionalbehavior,youcanuseReplaceSuperclasswithDelegate(397)orReplaceSubclasswithDelegate(379)tofoldthemiddlemanintotherealobject.Thatallowsyoutoextendbehaviorwithoutchasingallthatdelegation.

InsiderTrading

Softwarepeoplelikestrongwallsbetweentheirmodulesandcomplainbitterlyabouthowtradingdataaroundtoomuchincreasescoupling.Tomakethingswork,sometradehastooccur,butweneedtoreduceittoaminimumandkeepitallaboveboard.

ModulesthatwhispertoeachotherbythecoffeemachineneedtobeseparatedbyusingMoveFunction(196)andMoveField(205)toreducetheneedtochat.Ifmoduleshavecommoninterests,trytocreateathirdmoduletokeepthatcommonalityinawell-regulatedvehicle,oruseHideDelegate(187)tomakeanothermoduleactasanintermediary.

Inheritancecanoftenleadtocollusion.Subclassesarealwaysgoingtoknowmoreabouttheirparentsthantheirparentswouldlikethemtoknow.Ifit’stimetoleavehome,applyReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397)..

LargeClass

Whenaclassistryingtodotoomuch,itoftenshowsupastoomanyfields.Whenaclasshastoomanyfields,duplicatedcodecannotbefarbehind.

YoucanExtractClass(180)tobundleanumberofthevariables.Choosevariablestogotogetherinthecomponentthatmakessenseforeach.Forexample,“depositAmount”and“depositCurrency”arelikelytobelongtogetherinacomponent.Moregenerally,commonprefixesorsuffixesforsomesubsetofthevariablesinaclasssuggesttheopportunityforacomponent.Ifthecomponentmakessensewithinheritance,you’llfindExtractSuperclass(373)orReplaceTypeCodewithSubclasses(361)(whichessentiallyisextractingasubclass)areofteneasier.

Sometimesaclassdoesnotuseallofitsfieldsallofthetime.Ifso,youmaybeabletotheseextractionsmanytimes.

Aswithaclasswithtoomanyinstancevariables,aclasswithtoomuchcodeisaprimebreedinggroundforduplicatedcode,chaos,anddeath.Thesimplestsolution(havewementionedthatwelikesimplesolutions?)istoeliminateredundancyintheclassitself.Ifyouhavefivehundred-linemethodswithlotsofcodeincommon,youmaybeabletoturnthemintofiveten-linemethodswithanothertentwo-linemethodsextractedfromtheoriginal.

Theclientsofsuchaclassareoftenthebestclueforsplittinguptheclass.Lookatwhetherclientsuseasubsetofthefeaturesoftheclass.Eachsubsetisapossibleseparateclass.Onceyou’veidentifiedausefulsubset,useExtractClass(180),ExtractSuperclass(373),orReplaceTypeCodewithSubclasses(361)tobreakitout.

AlternativeClasseswithDifferentInterfaces

Oneofthegreatbenefitsofusingclassesisthesupportforsubstitution,allowingoneclasstoswapinforanotherintimesofneed.Butthisonlyworksiftheirinterfacesarethesame.UseChangeFunctionDeclaration(124)tomakefunctionsmatchup.Often,thisdoesn’tgofarenough;keepusingMoveFunction(196)tomovebehaviorintoclassesuntiltheprotocolsmatch.Ifthisleadstoduplication,youmaybeabletouseExtractSuperclass(373)toatone.

DataClass

Theseareclassesthathavefields,gettingandsettingmethodsforthefields,andnothingelse.Suchclassesaredumbdataholdersandareoftenbeingmanipulatedinfartoomuchdetailbyotherclasses.Insomestages,theseclassesmayhavepublicfields.Ifso,youshouldimmediatelyapplyEncapsulateRecord(160)beforeanyonenotices.UseRemoveSettingMethod(329)onanyfieldthatshouldnotbechanged.

Lookforwherethesegettingandsettingmethodsareusedbyotherclasses.TrytouseMoveFunction(196)tomovebehaviorintothedataclass.Ifyoucan’tmoveawholefunction,useExtractFunction(106)tocreateafunctionthatcanbemoved.

Dataclassesareoftenasignofbehaviorinthewrongplace,whichmeanscanmakebigprogressbymovingitfromtheclientintothedataclassitself.Butthereareexceptions,andoneofthebestexceptionsisarecordthat’sbeingusedasaresultrecordfromadistinctfunctioninvocation.Agoodexampleofthisistheintermediatedatastructureafteryou’veappliedSplitPhase(154).Akeycharacteristicofsucharesultrecordisthatit’simmutable(atleastinpractice).Immutablefieldsdon’tneedtobeencapsulatedandinformationderivedfromimmutabledatacanberepresentedasfieldsratherthangettingmethods.

RefusedBequest

Subclassesgettoinheritthemethodsanddataoftheirparents.Butwhatiftheydon’twantorneedwhattheyaregiven?Theyaregivenallthesegreatgiftsandpickjustafewtoplaywith.

Thetraditionalstoryisthatthismeansthehierarchyiswrong.YouneedtocreateanewsiblingclassandusePushDownMethod(357)andPushDownField(359)topushalltheunusedcodetothesibling.Thatwaytheparentholdsonlywhatiscommon.Often,you’llhearadvicethatallsuperclassesshouldbeabstract.

You’llguessfromoursnideuseof“traditional”thatwearen’tgoingtoadvisethis—atleastnotallthetime.Wedosubclassingtoreuseabitofbehaviorallthetime,andwefinditaperfectlygoodwayofdoingbusiness.Thereisasmell—

wecan’tdenyit—butusuallyitisn’tastrongsmell.So,wesaythatiftherefusedbequestiscausingconfusionandproblems,followthetraditionaladvice.However,don’tfeelyouhavetodoitallthetime.Ninetimesoutoftenthissmellistoofainttobeworthcleaning.

Thesmellofrefusedbequestismuchstrongerifthesubclassisreusingbehaviorbutdoesnotwanttosupporttheinterfaceofthesuperclass.Wedon’tmindrefusingimplementations—butrefusinginterfacegetsusonourhighhorses.Inthiscase,however,don’tfiddlewiththehierarchy;youwanttogutitbyapplyingReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397).

Comments

Don’tworry,wearen’tsayingthatpeopleshouldn’twritecomments.Inourolfac-toryanalogy,commentsaren’tabadsmell;indeedtheyareasweetsmell.Thereasonwementioncommentshereisthatcommentsareoftenusedasadeodorant.It’ssurprisinghowoftenyoulookatthicklycommentedcodeandnoticethatthecommentsaretherebecausethecodeisbad.

Commentsleadustobadcodethathasalltherottenwhiffswe’vediscussedintherestofthischapter.Ourfirstactionistoremovethebadsmellsbyrefactoring.Whenwe’refinished,weoftenfindthatthecommentsaresuperfluous.

Ifyouneedacommenttoexplainwhatablockofcodedoes,tryExtractFunction(106).Ifthemethodisalreadyextractedbutyoustillneedacommenttoexplainwhatitdoes,useChangeFunctionDeclaration(124)torenameit.Ifyouneedtostatesomerulesabouttherequiredstateofthesystem,useIntroduceAssertion(299).

Whenyoufeeltheneedtowriteacomment,firsttrytorefactorthecodesothatanycommentbecomessuperfluous.

Agoodtimetouseacommentiswhenyoudon’tknowwhattodo.Inadditiontodescribingwhatisgoingon,commentscanindicateareasinwhichyouaren’tsure.Acommentcanalsoexplainwhyyoudidsomething.Thiskindofinformationhelpsfuturemodifiers,especiallyforgetfulones.

Chapter4BuildingTestsRefactoringisavaluabletool,butitcan’tcomealone.Todorefactoringproperly,Ineedasolidsuiteofteststospotmyinevitablemistakes.Evenwithautomatedrefactoringtools,manyofmyrefactoringswillstillneedcheckingviaatestsuite.

Idon’tfindthistobeadisadvantage.Evenwithoutrefactoring,writinggoodtestsincreasesmyeffectivenessasaprogrammer.Thiswasasurpriseformeandiscounter-intuitiveformostprogrammers—soit’sworthexplainingwhy.

TheValueofSelf-TestingCode

Ifyoulookathowmostprogrammersspendtheirtime,you’llfindthatwritingcodeisactuallyquiteasmallfraction.Sometimeisspentfiguringoutwhatoughttobegoingon,sometimeisspentdesigning,butmosttimeisspentdebugging.I’msureeveryreadercanrememberlonghoursofdebugging—often,wellintothenight.Everyprogrammercantellastoryofabugthattookawholeday(ormore)tofind.Fixingthebugisusuallyprettyquick,butfindingitisanightmare.Andthen,whenyoudofixabug,there’salwaysachancethatanotheronewillappearandthatyoumightnotevennoticeittillmuchlater.Andyou’llspendagesfindingthatbug.

Theeventthatstartedmeontheroadtoself-testingcodewasatalkatOOPSLAin1992.Someone(Ithinkitwas“Bedarra”DaveThomas)saidoffhandedly,“Classesshouldcontaintheirowntests.”SoIdecidedtoincorporatetestsintothecodebasetogetherwiththeproductioncode.AsIwasalsodoingiterativedevelopment,ItriedaddingtestsasIcompletedeachiteration.TheprojectonwhichIwasworkingatthattimewasquitesmall,soweputoutiterationseveryweekorso.Runningthetestsbecamefairlystraightforward—butalthoughitwaseasy,itwasstillprettyboring.ThiswasbecauseeverytestproducedoutputtotheconsolethatIhadtocheck.NowI’maprettylazypersonandampreparedtoworkquitehardinordertoavoidwork.Irealizedthat,insteadoflookingatthescreentoseeifitprintedoutsomeinformationfromthemodel,Icouldgetthecomputertomakethattest.AllIhadtodowasputtheoutputIexpectedin

thetestcodeanddoacomparison.NowIcouldrunthetestsandtheywouldjustprint“OK”tothescreenifallwaswell.Thesoftwarewasnowself-testing.

Makesurealltestsarefullyautomaticandthattheychecktheirownresults.

Nowitwaseasytoruntests—aseasyascompiling.SoIstartedtoruntestseverytimeIcompiled.Soon,Ibegantonoticemyproductivityhadshotupward.IrealizedthatIwasn’tspendingsomuchtimedebugging.IfIaddedabugthatwascaughtbyaprevioustest,itwouldshowupassoonasIranthattest.Thetesthadworkedbefore,soIwouldknowthatthebugwasintheworkIhaddonesinceIlasttested.AndIranthetestsfrequently—whichmeansonlyafewminuteshadelapsed.IthusknewthatthesourceofthebugwasthecodeIhadjustwritten.Asitwasasmallamountofcodethatwasstillfreshinmymind,thebugwaseasytofind.Bugsthatwouldhaveotherwisetakenanhourormoretofindnowtookacoupleofminutesatmost.Notonlywasmysoftwareself-testing,butbyrunningthetestsfrequentlyIhadapowerfulbugdetector.

AsInoticedthis,Ibecamemoreaggressiveaboutdoingthetests.Insteadofwaitingfortheendofanincrement,Iwouldaddthetestsimmediatelyafterwritingabitoffunction.EverydayIwouldaddacoupleofnewfeaturesandtheteststotestthem.Ihardlyeverspentmorethanafewminuteshuntingforaregressionbug.

Asuiteoftestsisapowerfulbugdetectorthatdecapitatesthetimeittakestofindbugs.

Toolsforwritingandorganizingthesetestshavedevelopedagreatdealsincemyexperiments.WhileflyingfromSwitzerlandtoAtlantaforOOPSLA1997,KentBeckpairedwithErichGammatoporthisunittestingframeworkfromSmalltalktoJava.Theresultingframework,calledJUnit,hasbeenenormouslyinfluentialforprogramtesting,inspiringahugevarietyofsimilartools[bib-xunit]inlotsofdifferentlanguages.

Admittedly,itisnotsoeasytopersuadeotherstofollowthisroute.Writingthetestsmeansalotofextracodetowrite.Unlessyouhaveactuallyexperiencedhowitspeedsprogramming,self-testingdoesnotseemtomakesense.Thisisnothelpedbythefactthatmanypeoplehaveneverlearnedtowritetestsoreventothinkabouttests.Whentestsaremanual,theyaregut-wrenchinglyboring.Butwhentheyareautomatic,testscanactuallybequitefuntowrite.

Infact,oneofthemostusefultimestowritetestsisbeforeIstartprogramming.WhenIneedtoaddafeature,Ibeginbywritingthetest.Thisisn’tasbackwardasitsounds.Bywritingthetest,I’maskingmyselfwhatneedstobedonetoaddthefunction.Writingthetestalsoconcentratesmeontheinterfaceratherthantheimplementation(alwaysagoodthing).ItalsomeansIhaveaclearpointatwhichI’mdonecoding—whenthetestworks.

KentBeckbakedthishabitofwritingthetestfirstintoatechniquecalledTest-DrivenDevelopment(TDD)[bib-tdd].TheTest-DrivenDevelopmentapproachtoprogrammingreliesonshortcyclesofwritinga(failing)test,writingthecodetomakethattestwork,andrefactoringtoensuretheresultisascleanaspossible.Thistest-code-refactorcycleshouldoccurmanytimesperhour,andcanbeaveryproductiveandcalmingwaytowritecode.I’mnotgoingtodiscussitfurtherhere,butIdouseandwarmlyrecommendit.

That’senoughofthepolemic.AlthoughIbelieveeveryonewouldbenefitbywritingself-testingcode,itisnotthepointofthisbook.Thisbookisaboutrefactoring.Refactoringrequirestests.Ifyouwanttorefactor,youhavetowritetests.ThischaptergivesyouastartindoingthisforJavaScript.Thisisnotatestingbook,soI’mnotgoingtogointomuchdetail.I’vefound,however,thatwithtestingaremarkablysmallamountofworkcanhavesurprisinglybigbenefits.

Aswitheverythingelseinthisbook,Idescribethetestingapproachusingexamples.WhenIdevelopcode,IwritethetestsasIgo.Butsometimes,Ineedtorefactorsomecodewithouttests—thenIhavetomakethecodeself-testingbeforeIbegin.

SampleCodetoTest

Here’ssomecodetolookatandtest.Thecodesupportsasimpleapplicationthatallowsausertoexamineandmanipulateaproductionplan.The(crude)UIlookslikethis:

Theproductionplanhasademandandpriceforeachprovince.Eachprovincehasproducers,eachofwhichcanproduceacertainnumberofunitsataparticularprice.TheUIalsoshowshowmuchrevenueeachproducerwouldearniftheysellalltheirproduction.Atthebottom,thescreenshowstheshortfallinproduction(thedemandminusthetotalproduction)andtheprofitforthisplan.TheUIallowstheusertomanipulatethedemand,price,andtheindividualproducer’sproductionandcoststoseetheeffectontheproductionshortfallandprofits.Wheneverauserchangesanynumberinthedisplay,alltheothersupdateimmediately.

I’mshowingauserinterfacehere,soyoucansensehowthesoftwareisused,butI’monlygoingtoconcentrateonthebusinesslogicpartofthesoftware—thatis,theclassesthatcalculatetheprofitandtheshortfall,notthecodethatgeneratestheHTMLandhooksupthefieldchangestotheunderlyingbusinesslogic.Thischapterisjustanintroductiontotheworldofself-testingcode,soitmakessenseformetostartwiththeeasiestcase—whichiscodethatdoesn’tinvolveuserinterface,persistence,orexternalserviceinteraction.Suchseparation,however,isagoodideainanycase:Oncethiskindofbusinesslogicgetsatallcomplicated,IwillseparateitfromtheUImechanicssoIcanmoreeasilyreasonaboutitandtestit.

Thisbusinesslogiccodeinvolvestwoclasses:onethatrepresentsasingleproducer,andtheotherthatrepresentsawholeprovince.Theprovince’sconstructortakesaJavaScriptobject—onewecouldimaginebeingsuppliedbyaJSONdocument.

classProvince…

constructor(doc){

this._name=doc.name;

this._producers=[];

this._totalProduction=0;

this._demand=doc.demand;

this._price=doc.price;

doc.producers.forEach(d=>this.addProducer(newProducer(this,d)));

}

addProducer(arg){

this._producers.push(arg);

this._totalProduction+=arg.production;

}

toplevel…

functionsampleProvinceData(){

return{

name:"Asia",

producers:[

{name:"Byzantium",cost:10,production:9},

{name:"Attalia",cost:12,production:10},

{name:"Sinope",cost:10,production:6},

],

demand:30,

price:20

};

}

Ithasaccessorsforthevariousdatavalues:

classProvince…

getname(){returnthis._name;}

getproducers(){returnthis._producers.slice();}

gettotalProduction(){returnthis._totalProduction;}

settotalProduction(arg){this._totalProduction=arg;}

getdemand(){returnthis._demand;}

setdemand(arg){this._demand=parseInt(arg);}

getprice(){returnthis._price;}

setprice(arg){this._price=parseInt(arg);}

ThesetterswillbecalledwithstringsfromtheUIthatcontainthenumbers,soIneedtoparsethenumberstousethemreliablyincalculations.

Theproducerclassismostlyasimpledataholder:

classProducer…

constructor(aProvince,data){

this._province=aProvince;

this._cost=data.cost;

this._name=data.name;

this._production=data.production||0;

}

getname(){returnthis._name;}

getcost(){returnthis._cost;}

setcost(arg){this._cost=parseInt(arg);}

getproduction(){returnthis._production;}

setproduction(amountStr){

constamount=parseInt(amountStr);

constnewProduction=Number.isNaN(amount)?0:amount;

this._province.totalProduction+=newProduction-this._production;

this._production=newProduction;

}

Thewaythatsetproductionupdatesthederiveddataintheprovinceisugly,andwheneverIseethatIwanttorefactortoremoveit.ButIhavetowritetestsbeforethatIcanrefactorit.

Thecalculationfortheshortfallissimple.

classProvince…

getshortfall(){

returnthis._demand-this.totalProduction;

}

Thatfortheprofitisabitmoreinvolved

classProvince…

getprofit(){

returnthis.demandValue-this.demandCost;

}

getdemandCost(){

letremainingDemand=this.demand;

letresult=0;

this.producers

.sort((a,b)=>a.cost-b.cost)

.forEach(p=>{

constcontribution=Math.min(remainingDemand,p.production);

remainingDemand-=contribution;

result+=contribution*p.cost;

});

returnresult;

}

getdemandValue(){

returnthis.satisfiedDemand*this.price;

}

getsatisfiedDemand(){

returnMath.min(this._demand,this.totalProduction);

}

AFirstTest

Totestthiscode,I’llneedsomesortoftestingframework.Therearemanyoutthere,evenjustforJavaScript.TheoneI’lluseisMocha[bib-mocha],whichisreasonablycommonandwell-regarded.Iwon’tgointoafullexplanationofhowtousetheframework,justshowsomeexampletestswithit.Youshouldbeabletoadapt,easilyenough,adifferentframeworktobuildsimilartests.

Hereisasimpletestfortheshortfallcalculation:

describe('province',function(){

it('shortfall',function(){

constasia=newProvince(sampleProvinceData());

assert.equal(asia.shortfall,5);

});

});

TheMochaframeworkdividesupthetestcodeintoblocks,eachgroupingtogetherasuiteoftests.Eachtestappearsinanitblock.Forthissimplecase,thetesthastwosteps.Thefirststepsetsupsomefixture—dataandobjectsthatareneededforthetest:inthiscase,aloadedprovinceobject.Thesecondlineverifiessomecharacteristicofthatfixture—inthiscase,thattheshortfallistheamountthatshouldbeexpectedgiventheinitialdata.

Differentdevelopersusethedescriptivestringsinthedescribeanditblocksdifferently.Somewouldwriteasentencethatexplainswhatthetestistesting,butothersprefertoleavethemempty,arguingthatthedescriptivesentenceisjustduplicatingthecodeinthesamewayacommentdoes.IliketoputinjustenoughtoidentifywhichtestiswhichwhenIgetfailures.

IfIrunthistestinaNodeJSconsole,theoutputlookslikethis:

!

1passing(61ms)

Notethesimplicityofthefeedback—justasummaryofhowmanytestsarerunandhowmanyhavepassed.

Alwaysmakesureatestwillfailwhenitshould.

WhenIwriteatestagainstexistingcodelikethis,it’snicetoseethatalliswell—butI’mnaturallyskeptical.Particularly,onceIhavealotoftestsrunning,I’malwaysnervousthatatestisn’treallyexercisingthecodethewayIthinkitis,andthuswon’tcatchabugwhenIneeditto.SoIliketoseeeverytestfailatleastoncewhenIwriteit.Myfavoritewayofdoingthatistotemporarilyinjectafaultintothecode,forexample:

classProvince…

getshortfall(){

returnthis._demand-this.totalProduction*2;

}

Here’swhattheconsolenowlookslike:

!

0passing(72ms)

1failing

1)provinceshortfall:

AssertionError:expected-20toequal5

atContext.<anonymous>(src/tester.js:10:12)

Theframeworkindicateswhichtestfailedandgivessomeinformationaboutthenatureofthefailure—inthiscase,whatvaluewasexpectedandwhatvalue

actuallyturnedup.Ithereforenoticeatoncethatsomethingfailed—andIcanimmediatelyseewhichtestsfailed,givingmeaclueastowhatwentwrong(and,inthiscase,confirmingthefailurewaswhereIinjectedit).

Runtestsfrequently.Runthoseexercisingthecodeyou’reworkingonatleasteveryfewminutes;runalltestsatleastdaily.

Inarealsystem,Imighthavethousandsoftests.Agoodtestframeworkallowsmetorunthemeasilyandtoquicklyseeifanyhavefailed.Thissimplefeedbackisessentialtoself-testingcode.WhenIwork,I’llberunningtestsveryfrequently—checkingprogresswithnewcodeorcheckingformistakeswithrefactoring.

TheMochaframeworkcanusedifferentlibraries,whichitcallsassertionlibraries,toverifythefixtureforatest.BeingJavaScript,thereareaquadzillionofthemoutthere,someofwhichmaystillbecurrentwhenyou’rereadingthis.TheoneI’musingatthemomentisChai[bib-chai].Chaiallowsmetowritemyvalidationseitherusingan“assert”style:

describe('province',function(){

it('shortfall',function(){

constasia=newProvince(sampleProvinceData());

assert.equal(asia.shortfall,5);

});

});

oran“expect”style:

describe('province',function(){

it('shortfall',function(){

constasia=newProvince(sampleProvinceData());

expect(asia.shortfall).equal(5);

});

});

Iusuallyprefertheassertstyle,butatthemomentImostlyusetheexpectstylewhileworkinginJavaScript.

Differentenvironmentsprovidedifferentwaystoruntests.WhenI’mprogramminginJava,IuseanIDEthatgivesmeagraphicaltestrunner.Itsprogressbarisgreenaslongasallthetestspass,andturnsredshouldanyofthemfail.Mycolleaguesoftenusethephrases“greenbar”and“redbar”to

describethestateoftests.Imightsay,“Neverrefactoronaredbar,”meaningyoushouldn’tberefactoringifyourtestsuitehasafailingtest.Or,Imightsay,“Reverttogreen”tosayyoushouldundorecentchangesandgobacktothelaststatewhereyouhadall-passingtestsuite(usuallybygoingbacktoarecentversion-controlcheckpoint).

Graphicaltestrunnersarenice,butnotessential.IusuallyhavemytestssettorunfromasinglekeyinEmacs,andobservethetextfeedbackinmycompilationwindow.ThekeypointisthatIcanquicklyseeifmytestsareallOK.

AddAnotherTest

NowI’llcontinueaddingmoretests.ThestyleIfollowistolookatallthethingstheclassshoulddoandtesteachoneofthemforanyconditionsthatmightcausetheclasstofail.Thisisnotthesameastestingeverypublicmethod,whichiswhatsomeprogrammersadvocate.Testingshouldberisk-driven;remember,I’mtryingtofindbugs,noworinthefuture.ThereforeIdon’ttestaccessorsthatjustreadandwriteafield:TheyaresosimplethatI’mnotlikelytofindabugthere.

Thisisimportantbecausetryingtowritetoomanytestsusuallyleadstonotwritingenough.IgetmanybenefitsfromtestingevenifIdoonlyalittletesting.MyfocusistotesttheareasthatI’mmostworriedaboutgoingwrong.ThatwayIgetthemostbenefitformytestingeffort.

Itisbettertowriteandrunincompleteteststhannottoruncompletetests.

SoI’llstartbyhittingtheothermainoutputforthiscode—theprofitcalculation.Again,I’lljustdoabasictestforprofitonmyinitialfixture.

describe('province',function(){

it('shortfall',function(){

constasia=newProvince(sampleProvinceData());

expect(asia.shortfall).equal(5);

});

it('profit',function(){

constasia=newProvince(sampleProvinceData());

expect(asia.profit).equal(230);

});

});

Thatshowsthefinalresult,butthewayIgotitwasbyfirstsettingtheexpected

valuetoaplaceholder,thenreplacingitwithwhatevertheprogramproduced(230).Icouldhavecalculateditbyhandmyself,butsincethecodeissupposedtobeworkingcorrectly,I’lljusttrustitfornow.OnceIhavethatnewtestworkingcorrectly,Ibreakitbyalteringtheprofitcalculationwithaspurious*2.Isatisfymyselfthatthetestfailsasitshould,thenrevertmyinjectedfault.Thispattern—writewithaplaceholderfortheexpectedvalue,replacetheplaceholderwiththecode’sactualvalue,injectafault,revertthefault—isacommononeIusewhenaddingteststoexistingcode.

Thereissomeduplicationbetweenthesetests—bothofthemsetupthefixturewiththesamefirstline.JustasI’msuspiciousofduplicatedcodeinregularcode,I’msuspiciousofitintestcode,sowilllooktoremoveitbyfactoringtoacommonplace.Oneoptionistoraisetheconstanttotheouterscope.

describe('province',function(){

constasia=newProvince(sampleProvinceData());//DON'TDOTHIS

it('shortfall',function(){

expect(asia.shortfall).equal(5);

});

it('profit',function(){

expect(asia.profit).equal(230);

});

});

Butasthecommentindicates,Ineverdothis.Itwillworkforthemoment,butitintroducesapetridishthat’sprimedforoneofthenastiestbugsintesting—asharedfixturewhichcausesteststointeract.TheconstkeywordinJavaScriptonlymeansthereferencetoasiaisconstant,notthecontentofthatobject.Shouldafuturetestchangethatcommonobject,I’llendupwithintermittenttestfailuresduetotestsinteractingthroughthesharedfixture,yieldingdifferentresultsdependingonwhatorderthetestsarerunin.That’sanon-determinismintheteststhatcanleadtolonganddifficultdebuggingatbest,andacollapseofconfidenceinthetestsatworst.Instead,Iprefertodothis:

describe('province',function(){

letasia;

beforeEach(function(){

asia=newProvince(sampleProvinceData());

});

it('shortfall',function(){

expect(asia.shortfall).equal(5);

});

it('profit',function(){

expect(asia.profit).equal(230);

});

});

ThebeforeEachclauseisrunbeforeeachtestruns,clearingoutasiaandsettingittoafreshvalueeachtime.ThiswayIbuildafreshfixturebeforeeachtestisrun,whichkeepsthetestsisolatedandpreventsthenon-determinismthatcausessomuchtrouble.

WhenIgivethisadvice,somepeopleareconcernedthatbuildingafreshfixtureeverytimewillslowdownthetests.Mostofthetime,itwon’tbenoticeable.Ifitisaproblem,I’dconsiderasharedfixture,butthenIwillneedtobereallycarefulthatnotesteverchangesit.IcanalsouseasharedfixtureifI’msureitistrulyimmutable.Butmyreflexistouseafreshfixturebecausethedebuggingcostofmakingamistakewithasharedfixturehasbitmetooofteninthepast.

GivenIrunthesetupcodeinbeforeEachwitheverytest,whynotleavethesetupcodeinsidetheindividualitblocks?Ilikemyteststoalloperateonacommonbitoffixture,soIcanbecomefamiliarwiththatstandardfixtureandseethevariouscharacteristicstotestonit.ThepresenceofthebeforeEachblocksignalstothereaderthatI’musingastandardfixture.Youcanthenlookatallthetestswithinthescopeofthatdescribeblockandknowtheyalltakethesamebasedataasastartingpoint.

ModifyingtheFixture

Sofar,thetestsI’vewrittenshowhowIprobethepropertiesofthefixtureonceI’veloadedit.Butinuse,thatfixturewillberegularlyupdatedbytheuserastheychangevalues.

Mostoftheupdatesaresimplesetters,andIdon’tusuallybothertotestthoseasthere’slittlechancetheywillbethesourceofabug.ButthereissomecomplicatedbehavioraroundProducer’sproductionsetter,soIthinkthat’sworthatest.

describe(’province’…

it('changeproduction',function(){

asia.producers[0].production=20;

expect(asia.shortfall).equal(-6);

expect(asia.profit).equal(292);

});

Thisisacommonpattern.Itaketheinitialstandardfixturethat’ssetupbythebeforeEachblock,Iexercisethatfixtureforthetest,thenIverifythefixturehasdonewhatIthinkitshouldhavedone.Ifyoureadmuchabouttesting,you’llhearthesephasesdescribedvariouslyassetup-exercise-verify,given-when-then,orarrange-act-assert.Sometimesyou’llseeallthestepspresentwithinthetestitself,inothercasesthecommonearlyphasescanbepushedoutintostandardsetuproutinessuchasbeforeEach.

(Thereisanimplicitfourthphasethat’susuallynotmentioned:teardown.Teardownremovesthefixturebetweentestssothatdifferenttestsdon’tinteractwitheachother.BydoingallmysetupinbeforeEach,Iallowthetestframeworktoimplicitlyteardownmyfixturebetweentests,soIcantaketheteardownphaseforgranted.Mostwritersontestsglossoverteardown—reasonablyso,sincemostofthetimeweignoreit.Butoccasionally,itcanbeimportanttohaveanexplicitteardownoperation,particularlyifwehaveafixturethatwehavetosharebetweentestsbecauseit’sslowtocreate.)

Inthistest,I’mverifyingtwodifferentcharacteristicsinasingleitclause.Asageneralrule,it’swisetohaveonlyasingleverifystatementineachitclause.Thisisbecausethetestwillfailonthefirstverificationfailure—whichcanoftenhideusefulinformationwhenyou’refiguringoutwhyatestisbroken.Inthiscase,IfeelthetwoarecloselyenoughconnectedthatI’mhappytohavetheminthesametest.ShouldIwishtoseparatethemintoseparateitclauses,Icandothatlater.

ProbingtheBoundaries

Sofarmytestshavefocusedonregularusage,oftenreferredtoas“happypath”conditionswhereeverythingisgoingOKandthingsareusedasexpected.Butit’salsogoodtothrowtestsattheboundariesoftheseconditions—toseewhathappenswhenthingsmightgowrong.

WheneverIhaveacollectionofsomething,suchasproducersinthisexample,Iliketoseewhathappenswhenit’sempty.

describe('noproducers',function(){

letnoProducers;

beforeEach(function(){

constdata={

name:"Noproudcers",

producers:[],

demand:30,

price:20

};

noProducers=newProvince(data);

});

it('shortfall',function(){

expect(noProducers.shortfall).equal(30);

});

it('profit',function(){

expect(noProducers.profit).equal(0);

});

Withnumbers,zerosaregoodthingstoprobe.

describe(’province’…

it('zerodemand',function(){

asia.demand=0;

expect(asia.shortfall).equal(-25);

expect(asia.profit).equal(0);

});

Asarenegatives

describe(’province’…

it('negativedemand',function(){

asia.demand=-1;

expect(asia.shortfall).equal(-26);

expect(asia.profit).equal(-10);

});

Atthispoint,Imaystarttowonderifanegativedemandresultinginanegativeprofitreallymakesanysenseforthedomain.Shouldn’ttheminimumdemandbezero?Inwhichcase,perhaps,thesettershouldreactdifferentlytoanegativeargument—raisinganerrororsettingthevaluetozeroanyway.Thesearegoodquestionstoask,andwritingtestslikethishelpsmethinkabouthowthecodeoughttoreacttoboundarycases.

Thinkoftheboundaryconditionsunderwhichthingsmightgowrongandconcentrateyourteststhere.

ThesetterstakeastringfromthefieldsintheUI,whichareconstrainedtoonlyacceptnumbers—buttheycanstillbeblank,soIshouldhaveteststhatensurethecoderespondstotheblanksthewayIwantitto.

describe(’province’…

it('emptystringdemand',function(){

asia.demand="";

expect(asia.shortfall).NaN;

expect(asia.profit).NaN;

});

NoticehowI’mplayingthepartofanenemytomycode.I’mactivelythinkingabouthowIcanbreakit.Ifindthatstateofmindtobebothproductiveandfun.Itindulgesthemean-spiritedpartofmypsyche.

Thisoneisinteresting:

describe('stringforproducers',function(){

it('',function(){

constdata={

name:"Stringproducers",

producers:"",

demand:30,

price:20

};

constprov=newProvince(data);

expect(prov.shortfall).equal(0);

});

Thisdoesn’tproduceasimplefailurereportingthattheshortfallisn’t0.Here’stheconsoleoutput:

!

9passing(74ms)

1failing

1)stringforproducers:

TypeError:doc.producers.forEachisnotafunction

atnewProvince(src/main.js:22:19)

atContext.<anonymous>(src/tester.js:86:18)

Mochatreatsthisasafailure—butmanytestingframeworksdistinguishbetweenthissituation,whichtheycallanerror,andaregularfailure.Afailureindicatesa

verifystepwheretheactualvalueisoutsidetheboundsexpectedbytheverifystatement.Butthiserrorisadifferentanimal—it’sanexceptionraisedduringanearlierphase(inthiscase,thesetup).Thislookslikeanexceptionthattheauthorsofthecodehadn’tanticipated,sowegetanerrorsadlyfamiliartoJavaScriptprogrammers(“…isnotafunction”).

Howshouldthecoderespondtosuchacase?Oneapproachistoaddsomehandlingthatwouldgiveabettererrorresponse—eitherraisingamoremeaningfulerrormessage,orjustsettingproducerstoanemptyarray(withperhapsalogmessage).Buttheremayalsobevalidreasonstoleaveitasitis.Perhapstheinputobjectisproducedbyatrustedsource—suchasanotherpartofthesamecodebase.Puttinginlotsofvalidationchecksbetweenmodulesinthesamecodebasecanresultinduplicatechecksthatcausemoretroublethantheyareworth,especiallyiftheyduplicatevalidationdoneelsewhere.Butifthatinputobjectiscominginfromanexternalsource,suchasaJSON-encodedrequest,thenvalidationchecksareneeded,andshouldbetested.Ineithercase,writingtestslikethisraisesthesekindsofquestions.

IfI’mwritingtestslikethisbeforerefactoring,Iwouldprobablydiscardthistest.Refactoringshouldpreserveobservablebehavior;anerrorlikethisisoutsidetheboundsofobservable,soIneednotbeconcernedifmyrefactoringchangesthecode’sresponsetothiscondition.

Ifthiserrorcouldleadtobaddatarunningaroundtheprogram,causingafailurethatwillbehardtodebug,ImightuseIntroduceAssertion(299)tofailfast.Idon’taddteststocatchsuchassertionfailures,astheyarethemselvesaformoftest.

Don’tletthefearthattestingcan’tcatchallbugsstopyoufromwritingteststhatcatchmostbugs.

Whendoyoustop?I’msureyouhaveheardmanytimesthatyoucannotprovethataprogramhasnobugsbytesting.That’strue,butitdoesnotaffecttheabilityoftestingtospeedupprogramming.I’veseenvariousproposedrulestoensureyouhavetestedeverycombinationofeverything.It’sworthtakingalookatthese—butdon’tletthemgettoyou.Thereisalawofdiminishingreturnsintesting,andthereisthedangerthatbytryingtowritetoomanytestsyoubecomediscouragedandendupnotwritingany.Youshouldconcentrateonwheretheriskis.Lookatthecodeandseewhereitbecomescomplex.Lookatafunction

andconsiderthelikelyareasoferror.Yourtestswillnotfindeverybug,butasyourefactor,youwillunderstandtheprogrambetterandthusfindmorebugs.AlthoughIalwaysstartrefactoringwithatestsuite,IinvariablyaddtoitasIgoalong.

MuchMoreThanThis

That’sasfarasI’mgoingtogowiththischapter—afterall,thisisabookonrefactoring,notontesting.Buttestingisanimportanttopic,bothbecauseit’sanecessaryfoundationforrefactoringandbecauseit’savaluabletoolinitsownright.WhileI’vebeenhappytoseethegrowthofrefactoringasaprogrammingpracticesinceIwrotethisbook,I’vebeenevenhappiertoseethechangeinattitudestotesting.Previouslyseenastheresponsibilityofaseparate(andinferior)group,testingisnowincreasinglyafirst-classconcernofanydecentsoftwaredeveloper.Architecturesoftenare,rightly,judgedontheirtestability.

ThekindsoftestsI’veshownhereareunittests,designedtooperateonasmallareaofthecodeandrunfast.Theyarethebackboneofself-testingcode;mosttestsinsuchasystemareunittests.Thereareotherkindsofteststoo,focusingonintegrationbetweencomponents,exercisingmultiplelevelsofthesoftwaretogether,lookingforperformanceissues,etc.(Andevenmorevariedthanthetypesoftestsaretheargumentspeoplegetintoabouthowtoclassifytests.)

Likemostaspectsofprogramming,testingisaniterativeactivity.Unlessyouareeitherveryskilledorverylucky,youwon’tgetyourtestsrightthefirsttime.IfindI’mconstantlyworkingonthetestsuite—justasmuchasIworkonthemaincode.Naturally,thismeansaddingnewtestsasIaddnewfeatures,butitalsoinvolveslookingattheexistingtests.Aretheyclearenough?DoIneedtorefactorthemsoIcanmoreeasilyunderstandwhattheyaredoing?HaveIgottherighttests?Animportanthabittogetintoistorespondtoabugbyfirstwritingatestthatclearlyrevealsthebug.OnlyafterIhavethetestdoIfixthebug.Byhavingthetest,Iknowthebugwillstaydead.Ialsothinkaboutthatbuganditstest:Doesitgivemecluestoothergapsinthetestsuite?

Whenyougetabugreport,startbywritingaunittestthatexposesthebug.

Acommonquestionis,“Howmuchtestingisenough?”There’snogoodmeasurementforthis.Somepeopleadvocateusingtestcoverage[bib-test-coverage]asameasure,buttestcoverageanalysisisonlygoodforidentifying

untestedareasofthecode,notforassessingthequalityofatestsuite.

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

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

Chapter5IntroducingtheCatalogTherestofthisbookisacatalogofrefactorings.ThiscatalogstartedfrommypersonalnotesthatImadetoremindmyselfhowtodorefactoringsinasafeandefficientway.Sincethen,I’verefinedthecatalog,andthere’smoreofitthatcomesfromdeliberateexplorationofsomerefactoringmoves.It’sstillsomethingIusewhenIdoarefactoringIhaven’tdoneinawhile.

FormatoftheRefactorings

AsIdescribetherefactoringsinthisandotherchapters,Iuseastandardformat.Eachrefactoringhasfiveparts,asfollows:

Ibeginwithaname.Thenameisimportanttobuildingavocabularyofrefactorings.ThisisthenameIuseelsewhereinthebook.Refactoringsoftengobydifferentnamesnow,soIalsolistanyaliasesthatseemtobecommon.

Ifollowthenamewithashortsketchoftherefactoring.Thishelpsyoufindarefactoringmorequickly.

Themotivationdescribeswhytherefactoringshouldbedoneanddescribescircumstancesinwhichitshouldn’tbedone.

Themechanicsareaconcise,step-by-stepdescriptionofhowtocarryouttherefactoring.

Theexamplesshowaverysimpleuseoftherefactoringtoillustratehowitworks.

Thesketchshowsacodeexampleofthetransformationoftherefactoring.It’snotmeanttoexplainwhattherefactoringis,letalonehowtodoit,butitshouldremindyouwhattherefactoringisifyou’vecomeacrossitbefore.Ifnot,you’llprobablyneedtoworkthroughtheexampletogetabetteridea.Ialsoincludeasmallgraphic;again,Idon’tintendittobeexplanatory—it’smoreofagraphicmemory-jogger.

ThemechanicscomefrommyownnotestorememberhowtodotherefactoringwhenIhaven’tdoneitforawhile.Assuch,theyaresomewhatterse,usuallywithoutexplanationsofwhythestepsaredonethatway.Igiveamoreexpansiveexplanationintheexample.Thisway,themechanicsareshortnotesyoucanrefertoeasilywhenyouknowtherefactoringbutneedtolookupthesteps(atleastthisishowIusethem).You’llprobablyneedtoreadtheexampleswhenyoufirstdotherefactoring.

I’vewrittenthemechanicsinsuchawaythateachstepofeachrefactoringisassmallaspossible.Iemphasizethesafewayofdoingtherefactoring—whichistotakeverysmallstepsandtestaftereveryone.Atwork,Iusuallytakelargerstepsthansomeofthebabystepsdescribed,butifIrunintoabug,Ibackoutthelaststepandtakethesmallersteps.Thestepsincludeanumberofreferencestospecialcases.Thestepsthusalsofunctionasachecklist;Ioftenforgetthesethingsmyself.

AlthoughI(withfewexceptions)onlylistonesetofmechanics,theyaren’ttheonlywaytocarryouttherefactoring.Iselectedthemechanicsinthebookbecausetheyworkprettywellmostofthetime.It’slikelyyou’llvarythemasyougetmorepracticeinrefactoring,andthat’sfine.Justrememberthatthekeyistotakesmallsteps—andthetrickierthesituation,thesmallerthesteps.

Theexamplesareofthelaughablysimpletextbookkind.Myaimwiththeexamplesistohelpexplainthebasicrefactoringwithminimaldistractions,soIhopeyou’llforgivethesimplicity.(Theyarecertainlynotexamplesofgoodbusinessmodeling.)I’msureyou’llbeabletoapplythemtoyourrathermorecomplexsituations.Someverysimplerefactoringsdon’thaveexamplesbecauseIdidn’tthinkanexamplewouldaddmuch.

Inparticular,rememberthattheexamplesareincludedonlytoillustratetheonerefactoringunderdiscussion.Inmostcases,therearestillproblemswiththecodeattheend—butfixingtheseproblemsrequiresotherrefactorings.Inafewcasesinwhichrefactoringsoftengotogether,Icarryexamplesfromonerefactoringtoanother.Inmostcases,Ileavethecodeasitisafterthesinglerefactoring.Idothistomakeeachrefactoringself-contained,becausetheprimaryroleofthecatalogistobeareference.

Iuseboldfacecodetohighlightchangedcodewhereitmaybedifficulttospotamongcodethathasnotbeenchanged.Idonotuseboldfacetypeforall

changedcode,becausetoomuchdefeatsthepurpose.

TheChoiceofRefactorings

Thisisbynomeansacompletecatalogofrefactorings.Itis,Ihope,acollectionofthosemostusefultohavethemwrittendown.By“mostuseful”Imeanthosethatarebothcommonlyusedandworthwhiletonameanddescribe.Ifindsomethingworthwhiletodescribeforacombinationofreasons:Somehaveinterestingmechanicswhichhelpgeneralrefactoringskills,somehaveastrongeffectonimprovingthedesignofcode.

SomerefactoringsaremissingbecausetheyaresosmallandstraightforwardthatIdon’tfeeltheyareworthwritingup.AnexampleinthefirsteditionwasSlideStatements(221)—whichIusefrequentlybutdidn’trecognizeassomethingIshouldincludeinthecatalog(obviously,Ichangedmymindforthisedition).Thesemaywellgetaddedtothebookovertime,dependingonhowmuchenergyIdevotetonewrefactoringsinthefuture.

Anothercategoryisrefactoringsthatlogicallyexist,buteitheraren’tusedmuchbymeorshowasimplesimilaritytootherrefactorings.Everyrefactoringinthisbookhasalogicalinverserefactoring,butIdidn’twriteallofthemupbecauseIdon’tfindmanyinversesinteresting.EncapsulateVariable(132)isacommonandpowerfulrefactoringbutitsinverseissomethingIhardlyeverdo(anditiseasytoperformanyway)soIdidn’tthinkweneedacatalogentryforit.

Chapter6AFirstSetofRefactoringsI’mstartingthecatalogwithasetofrefactoringsthatIconsiderthemostusefultolearnfirst.

ProbablythemostcommonrefactoringIdoisextractingcodeintoafunction(ExtractFunction(106))oravariable(ExtractVariable(119)).Sincerefactoringisallaboutchange,it’snosurprisethatIalsofrequentlyusetheinversesofthosetwo(InlineFunction(115)andInlineVariable(123)).

Extractionisallaboutgivingnames,andIoftenneedtochangethenamesasIlearn.ChangeFunctionDeclaration(124)changesnamesoffunctions;Ialsousethatrefactoringtoaddorremoveafunction’sarguments.Forvariables,IuseRenameVariable(137),whichreliesonEncapsulateVariable(132).Whenchangingfunctionarguments,IoftenfinditusefultocombineacommonclumpofargumentsintoasingleobjectwithIntroduceParameterObject(140).

Formingandnamingfunctionsareessentiallow-levelrefactorings—but,oncecreated,it’snecessarytogroupfunctionsintohigher-levelmodules.IuseCombineFunctionsintoClass(144)togroupfunctions,togetherwiththedatatheyoperateon,intoaclass.AnotherpathItakeistocombinethemintoatransform(CombineFunctionsintoTransform(149)),whichisparticularlyhandywithread-onlydata.Atastepfurtherinscale,IcanoftenformthesemodulesintodistinctprocessingphasesusingSplitPhase(154).

ExtractFunction

formerly:ExtractMethod

inverseof:InlineFunction(115)

Motivation

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

Duringmycareer,I’veheardmanyargumentsaboutwhentoenclosecodeinitsownfunction.Someoftheseguidelineswerebasedonlength:Functionsshouldbenolargerthanfitonascreen.Somewerebasedonreuse:Anycodeusedmorethanonceshouldbeputinitsownfunction,butcodeonlyusedonceshouldbeleftinline.Theargumentthatmakesmostsensetome,however,istheseparationbetweenintentionandimplementation.Ifyouhavetospendeffortlookingatafragmentofcodeandfiguringoutwhatit’sdoing,thenyoushouldextractitintoafunctionandnamethefunctionafterthe“what.”Then,whenyoureaditagain,thepurposeofthefunctionleapsrightoutatyou,andmostofthetimeyouwon’tneedtocareabouthowthefunctionfulfillsitspurpose(whichisthebodyofthefunction).

OnceIacceptedthisprinciple,Idevelopedahabitofwritingverysmallfunctions—typically,onlyafewlineslong.Tome,anyfunctionwithmorethanhalf-a-dozenlinesofcodestartstosmell,andit’snotunusualformetohavefunctionsthatareasinglelineofcode.Thefactthatsizeisn’timportantwasbroughthometomebyanexamplethatKentBeckshowedmefromtheoriginalSmalltalksystem.Smalltalkinthosedaysranonblack-and-whitesystems.Ifyouwantedtohighlightsometextorgraphics,youwouldreversethevideo.Smalltalk’sgraphicsclasshadamethodforthiscalledhighlight,whoseimplementationwasjustacalltothemethodreverse.Thenameofthemethodwaslongerthanitsimplementation—butthatdidn’tmatterbecausetherewasabigdistancebetweentheintentionofthecodeanditsimplementation.

Somepeopleareconcernedaboutshortfunctionsbecausetheyworryabouttheperformancecostofafunctioncall.WhenIwasyoung,thatwasoccasionallyafactor,butthat’sveryrarenow.Optimizingcompilersoftenworkbetterwithshorterfunctionswhichcanbecachedmoreeasily.Asalways,followthegeneralguidelinesonperformanceoptimization.

Smallfunctionslikethisonlyworkifthenamesaregood,soyouneedtopaygoodattentiontonaming.Thistakespractice—butonceyougetgoodatit,thisapproachcanmakecoderemarkablyself-documenting.

Often,Iseefragmentsofcodeinalargerfunctionthatstartwithacommenttosaywhattheydo.ThecommentisoftenagoodhintforthenameofthefunctionwhenIextractthatfragment.

Mechanics

Createanewfunction,andnameitaftertheintentofthefunction(nameitbywhatitdoes,notbyhowitdoesit).

IfthecodeIwanttoextractisverysimple,suchasasinglefunctioncall,Istillextractitifthenameofthenewfunctionwillrevealtheintentofthecodeinabetterway.IfIcan’tcomeupwithamoremeaningfulname,that’sasignthatIshouldn’textractthecode.However,Idon’thavetocomeupwiththebestnamerightaway;sometimesagoodnameonlyappearsasIworkwiththeextraction.It’sOKtoextractafunction,trytoworkwithit,realizeitisn’thelping,andtheninlineitbackagain.AslongasI’velearnedsomething,mytimewasn’twasted.

Ifthelanguagesupportsnestedfunctions,nesttheextractedfunctioninsidethesourcefunction.Thatwillreducetheamountofout-of-scopevariablestodealwithafterthenextcoupleofsteps.IcanalwaysuseMoveFunction(196)later.

Copytheextractedcodefromthesourcefunctionintothenewtargetfunction.

Scantheextractedcodeforreferencestoanyvariablesthatarelocalinscopetothesourcefunctionandwillnotbeinscopefortheextractedfunction.Passthemasparameters.

IfIextractintoanestedfunctionofthesourcefunction,Idon’trunintotheseproblems.

Usually,thesearelocalvariablesandparameterstothefunction.Themostgeneralapproachistopassallsuchparametersinasarguments.Thereareusuallynodifficultiesforvariablesthatareusedbutnotassignedto.

Ifavariableisonlyusedinsidetheextractedcodebutisdeclaredoutside,movethedeclarationintotheextractedcode.

Anyvariablesthatareassignedtoneedmorecareiftheyarepassedbyvalue.Ifthere’sonlyoneofthem,Itrytotreattheextractedcodeasaqueryandassigntheresulttothevariableconcerned.

Sometimes,Ifindthattoomanylocalvariablesarebeingassignedbytheextractedcode.It’sbettertoabandontheextractionatthispoint.Whenthishappens,IconsiderotherrefactoringssuchasSplitVariable(240)orReplaceTempwithQuery(176)tosimplifyvariableusageandrevisittheextractionlater.

Compileafterallvariablesaredealtwith

Onceallthevariablesaredealtwith,itcanbeusefultocompileifthelanguageenvironmentdoescompile-timechecks.Often,thiswillhelpfindanyvariablesthathaven’tbeendealtwithproperly.

Replacetheextractedcodeinthesourcefunctionwithacalltothetargetfunction.

Test.

Lookforothercodethat’sthesameorsimilartothecodejustextracted,andconsiderusingReplaceInlineCodewithFunctionCall(220)tocallthenewfunction.

Somerefactoringtoolssupportthisdirectly.Otherwise,itcanbeworthdoingsomequicksearchestoseeifduplicatecodeexistselsewhere.

Example:NoVariablesOutofScope

Inthesimplestcase,ExtractFunctionistriviallyeasy.

functionprintOwing(invoice){

letoutstanding=0;

console.log("***********************");

console.log("****CustomerOwes****");

console.log("***********************");

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

//recordduedate

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

//printdetails

console.log(`name:${invoice.customer}`);

console.log(`amount:${outstanding}`);

console.log(`due:${invoice.dueDate.toLocaleDateString()}`);

}

YoumaybewonderingwhattheClock.todayisabout.ItisaClockWrapper(https://martinfowler.com/bliki/ClockWrapper.html)—anobjectthatwrapscallstothesystemclock.IavoidputtingdirectcallstothingslikeDate.now()inmycode,becauseitleadstonon-deterministictestsandmakesitdifficulttoreproduceerrorconditionswhendiagnosingfailures.

It’seasytoextractthecodethatprintsthebanner.Ijustcut,paste,andputinacall:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

//recordduedate

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

//printdetails

console.log(`name:${invoice.customer}`);

console.log(`amount:${outstanding}`);

console.log(`due:${invoice.dueDate.toLocaleDateString()}`);

}

functionprintBanner(){

console.log("***********************");

console.log("****CustomerOwes****");

console.log("***********************");

}

Similarly,Icantaketheprintingofdetailsandextractthattoo:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

//recordduedate

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

printDetails();

functionprintDetails(){

console.log(`name:${invoice.customer}`);

console.log(`amount:${outstanding}`);

console.log(`due:${invoice.dueDate.toLocaleDateString()}`);

}

ThismakesExtractFunctionseemlikeatriviallyeasyrefactoring.Butinmanysituations,itturnsouttoberathermoretricky.

Inthecaseabove,IdefinedprintDetailssoitwasnestedinsideprintOwing.ThatwayitwasabletoaccessallthevariablesdefinedinprintOwing.Butthat’snotanoptiontomeifI’mprogramminginalanguagethatdoesn’tallownestedfunctions.ThenI’mfaced,essentially,withtheproblemofextractingthefunctiontothetoplevel,whichmeansIhavetopayattentiontoanyvariablesthatexistonlyinthescopeofthesourcefunction.Thesearetheargumentstotheoriginalfunctionandthetemporaryvariablesdefinedinthefunction.

Example:UsingLocalVariables

Theeasiestcasewithlocalvariablesiswhentheyareusedbutnotreassigned.Inthiscase,Icanjustpasstheminasparameters.SoifIhavethefollowingfunction:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

//recordduedate

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

//printdetails

console.log(`name:${invoice.customer}`);

console.log(`amount:${outstanding}`);

console.log(`due:${invoice.dueDate.toLocaleDateString()}`);

}

Icanextracttheprintingofdetailspassingtwoparameters:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

//recordduedate

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

printDetails(invoice,outstanding);

}

functionprintDetails(invoice,outstanding){

console.log(`name:${invoice.customer}`);

console.log(`amount:${outstanding}`);

console.log(`due:${invoice.dueDate.toLocaleDateString()}`);

}

Thesameistrueifthelocalvariableisastructure(suchasanarray,record,orobject)andImodifythatstructure.So,Icansimilarlyextractthesettingoftheduedate:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

functionrecordDueDate(invoice){

consttoday=Clock.today;

invoice.dueDate=newDate(today.getFullYear(),today.getMonth(),today.getDate()+30);

}

Example:ReassigningaLocalVariable

It’stheassignmenttolocalvariablesthatbecomescomplicated.Inthiscase,we’reonlytalkingabouttemps.IfIseeanassignmenttoaparameter,IimmediatelyuseSplitVariable(240),whichturnsitintoatemp.

Fortempsthatareassignedto,therearetwocases.Thesimplercaseiswherethevariableisatemporaryvariableusedonlywithintheextractedcode.Whenthathappens,thevariablejustexistswithintheextractedcode.Sometimes,particularlywhenvariablesareinitializedatsomedistancebeforetheyareused,it’shandytouseSlideStatements(221)togetallthevariablemanipulationtogether.

Themoreawkwardcaseiswherethevariableisusedoutsidetheextractedfunction.Inthatcase,Ineedtoreturnthenewvalue.Icanillustratethiswiththefollowingfamiliar-lookingfunction:

functionprintOwing(invoice){

letoutstanding=0;

printBanner();

//calculateoutstanding

for(constoofinvoice.orders){

outstanding+=o.amount;

}

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

I’veshownthepreviousrefactoringsallinonestep,sincetheywerestraightforward,butthistimeI’lltakeitonestepatatimefromthemechanics.

First,I’llslidethedeclarationnexttoitsuse.

functionprintOwing(invoice){

printBanner();

//calculateoutstanding

letoutstanding=0;

for(constoofinvoice.orders){

outstanding+=o.amount;

}

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

IthencopythecodeIwanttoextractintoatargetfunction.

functionprintOwing(invoice){

printBanner();

//calculateoutstanding

letoutstanding=0;

for(constoofinvoice.orders){

outstanding+=o.amount;

}

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

functioncalculateOutstanding(invoice){

letoutstanding=0;

for(constoofinvoice.orders){

outstanding+=o.amount;

}

returnoutstanding;

}

SinceImovedthedeclarationofoutstandingintotheextractedcode,Idon’tneedtopassitinasaparameter.Theoutstandingvariableistheonlyonereassignedintheextractedcode,soIcanreturnit.

MyJavaScriptenvironmentdoesn’tyieldanyvaluebycompiling—indeedlessthanI’mgettingfromthesyntaxanalysisinmyeditor—sothere’snosteptodohere.Mynextthingtodoistoreplacetheoriginalcodewithacalltothenewfunction.SinceI’mreturningthevalue,Ineedtostoreitintheoriginalvariable.

functionprintOwing(invoice){

printBanner();

letoutstanding=calculateOutstanding(invoice);

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

functioncalculateOutstanding(invoice){

letoutstanding=0;

for(constoofinvoice.orders){

outstanding+=o.amount;

}

returnoutstanding;

}

BeforeIconsidermyselfdone,Irenamethereturnvaluetofollowmyusualcodingstyle.

functionprintOwing(invoice){

printBanner();

constoutstanding=calculateOutstanding(invoice);

recordDueDate(invoice);

printDetails(invoice,outstanding);

}

functioncalculateOutstanding(invoice){

letresult=0;

for(constoofinvoice.orders){

result+=o.amount;

}

returnresult;

}

Ialsotaketheopportunitytochangetheoriginaloutstandingintoaconst.

Atthispointyoumaybewondering,“Whathappensifmorethanonevariableneedstobereturned?”

Here,Ihaveseveraloptions.UsuallyIprefertopickdifferentcodetoextract.Ilikeafunctiontoreturnonevalue,soIwouldtrytoarrangeformultiplefunctionsforthedifferentvalues.IfIreallyneedtoextractwithmultiplevalues,Icanformarecordandreturnthat—butusuallyIfinditbettertoreworkthetemporaryvariablesinstead.HereIlikeusingReplaceTempwithQuery(176)andSplitVariable(240).

ThisraisesaninterestingquestionwhenI’mextractingfunctionsthatIexpecttothenmovetoanothercontext,suchastoplevel.Iprefersmallsteps,somyinstinctistoextractintoanestedfunctionfirst,thenmovethatnestedfunctiontoitsnewcontext.ButthetrickypartofthisisdealingwithvariablesandIdon’texposethatdifficultyuntilIdothemove.ThisarguesthateventhoughIcanextractintoanestedfunction,itmakessensetoextracttoatleastthesiblinglevelofthesourcefunctionfirst,soIcanimmediatelytelliftheextractedcodemakessense.

InlineFunction

formerly:InlineMethod

inverseof:ExtractFunction(106)

Motivation

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

IalsouseInlineFunctioniswhenIhaveagroupoffunctionsthatseembadlyfactored.Icaninlinethemallintoonebigfunctionandthenre-extractthefunctionsthewayIprefer.

IcommonlyuseInlineFunctionwhenIseecodethat’susingtoomuch

indirection—whenitseemsthateveryfunctiondoessimpledelegationtoanotherfunction,andIgetlostinallthedelegation.Someofthisindirectionmaybeworthwhile,butnotallofit.Byinlining,Icanflushouttheusefulonesandeliminatetherest.

Mechanics

Checkthatthisisn’tapolymorphicmethod.

Ifthisisamethodinaclass,andhassubclassesthatoverrideit,thenIcan’tinlineit.

Findallthecallersofthefunction.

Replaceeachcallwiththefunction’sbody.

Testaftereachreplacement.

Theentireinliningdoesn’thavetobedoneallatonce.Ifsomepartsoftheinlinearetricky,theycanbedonegraduallyasopportunitypermits.

Removethefunctiondefinition.

Writtenthisway,InlineFunctionissimple.Ingeneral,itisn’t.Icouldwritepagesonhowtohandlerecursion,multiplereturnpoints,inliningamethodintoanotherobjectwhenyoudon’thaveaccessors,andthelike.ThereasonIdon’tisthatifyouencounterthesecomplexities,youshouldn’tdothisrefactoring.

Example

Inthesimplestcase,thisrefactoringissoeasyit’strivial.Istartwith

functionrating(aDriver){

returnmoreThanFiveLateDeliveries(aDriver)?2:1;

}

functionmoreThanFiveLateDeliveries(aDriver){

returnaDriver.numberOfLateDeliveries>5;

}

Icanjusttakethereturnexpressionofthecalledfunctionandpasteitintothe

callertoreplacethecall.

functionrating(aDriver){

returnaDriver.numberOfLateDeliveries>5?2:1;

}

Butitcanbealittlemoreinvolvedthanthat,requiringmetodomoreworktofitthecodeintoitsnewhome.ConsiderthecasewhereIstartwiththisslightvariationontheearlierinitialcode.

functionrating(aDriver){

returnmoreThanFiveLateDeliveries(aDriver)?2:1;

}

functionmoreThanFiveLateDeliveries(dvr){

returndvr.numberOfLateDeliveries>5;

}

Almostthesame,butnowthedeclaredargumentonmoreThanFiveLateDeliveriesisdifferenttothenameofthepassed-inargument.SoIhavetofitthecodealittlewhenIdotheinline.

functionrating(aDriver){

returnaDriver.numberOfLateDeliveries>5?2:1;

}

Itcanbeevenmoreinvolvedthanthis.Considerthiscode:

functionreportLines(aCustomer){

constlines=[];

gatherCustomerData(lines,aCustomer);

returnlines;

}

functiongatherCustomerData(out,aCustomer){

out.push(["name",aCustomer.name]);

out.push(["location",aCustomer.location]);

}

InlininggatherCustomerDataintoreportLinesisn’tasimplecutandpaste.It’snottoocomplicated,andmosttimesIwouldstilldothisinonego,withabitoffitting.Buttobecautious,itmaymakesensetomoveonelineatatime.SoI’dstartwithusingMoveStatementstoCallers(215)onthefirstline(I’ddoitthesimplewaywithacut,paste,andfit).

functionreportLines(aCustomer){

constlines=[];

lines.push(["name",aCustomer.name]);

gatherCustomerData(lines,aCustomer);

returnlines;

}

functiongatherCustomerData(out,aCustomer){

out.push(["name",aCustomer.name]);

out.push(["location",aCustomer.location]);

}

IthencontinuewiththeotherlinesuntilI’mdone.

functionreportLines(aCustomer){

constlines=[];

lines.push(["name",aCustomer.name]);

lines.push(["location",aCustomer.location]);

returnlines;

}

Thepointhereistoalwaysbereadytotakesmallersteps.Mostofthetime,withthesmallfunctionsInormallywrite,IcandoInlineFunctioninonego,evenifthereisabitofrefittingtodo.ButifIrunintocomplications,Igoonelineatatime.Evenwithoneline,thingscangetabitawkward;then,I’llusethemoreelaboratemechanicsforMoveStatementstoCallers(215)tobreakthingsdownevenmore.Andif,feelingconfident,Idosomethingthequickwayandthetestsbreak,Iprefertorevertbacktomylastgreencodeandrepeattherefactoringwithsmallerstepsandatouchofchagrin.

ExtractVariable

formerly:IntroduceExplainingVariable

inverseof:InlineVariable(123)

Motivation

Expressionscanbecomeverycomplexandhardtoread.Insuchsituations,localvariablesmayhelpbreaktheexpressiondownintosomethingmoremanageable.Inparticular,theygivemeanabilitytonameapartofamorecomplexpieceoflogic.Thisallowsmetobetterunderstandthepurposeofwhat’shappening.

Suchvariablesarealsohandyfordebugging,sincetheyprovideaneasyhookforadebuggerorprintstatementtocapture.

IfI’mconsideringExtractVariable,itmeansIwanttoaddanametoanexpressioninmycode.OnceI’vedecidedIwanttodothat,Ialsothinkaboutthecontextofthatname.Ifit’sonlymeaningfulwithinthefunctionI’mworkingon,thenExtractVariableisagoodchoice—butifitmakessenseinabroadercontext,I’llconsidermakingthenameavailableinthatbroadercontext,usuallyasafunction.Ifthenameisavailablemorewidely,thenothercodecanusethatexpressionwithouthavingtorepeattheexpression,leadingtolessduplicationandabetterstatementofmyintent.

Thedownsideofpromotingthenametoabroadercontextisextraeffort.Ifit’ssignificantlymoreeffort,I’mlikelytoleaveittilllaterwhenIcanuseReplaceTempwithQuery(176).Butifit’seasy,Iliketodoitnowsothenameisimmediatelyavailableinthecode.Asagoodexampleofthis,ifI’mworkinginaclass,thenExtractFunction(106)isveryeasytodo.

Mechanics

Ensurethattheexpressionyouwanttoextractdoesnothaveside-effects.

Declareanimmutablevariable.Setittoacopyoftheexpressionyouwanttoname.

Replacetheoriginalexpressionwiththenewvariable.

Test.

Iftheexpressionappearsmorethanonce,replaceeachoccurrencewiththevariable,testingaftereachreplacement.

Example

Istartwithasimplecalculation

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

returnorder.quantity*order.itemPrice-

Math.max(0,order.quantity-500)*order.itemPrice*0.05+

Math.min(order.quantity*order.itemPrice*0.1,100);

}

Simpleasitmaybe,Icanmakeitstilleasiertofollow.First,Irecognizethatthebasepriceisthemultipleofthequantityandtheitemprice.

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

returnorder.quantity*order.itemPrice-

Math.max(0,order.quantity-500)*order.itemPrice*0.05+

Math.min(order.quantity*order.itemPrice*0.1,100);

}

Oncethatunderstandingisinmyhead,Iputitinthecodebycreatingand

namingavariableforit.

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

constbasePrice=order.quantity*order.itemPrice;

returnorder.quantity*order.itemPrice-

Math.max(0,order.quantity-500)*order.itemPrice*0.05+

Math.min(order.quantity*order.itemPrice*0.1,100);

}

Ofcourse,justdeclaringandinitializingavariabledoesn’tdoanything;Ialsohavetouseit,soIreplacetheexpressionthatIusedasitssource.

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

constbasePrice=order.quantity*order.itemPrice;

returnbasePrice-

Math.max(0,order.quantity-500)*order.itemPrice*0.05+

Math.min(order.quantity*order.itemPrice*0.1,100);

}

Thatsameexpressionisusedlateron,soIcanreplaceitwiththevariabletheretoo.

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

constbasePrice=order.quantity*order.itemPrice;

returnbasePrice-

Math.max(0,order.quantity-500)*order.itemPrice*0.05+

Math.min(basePrice*0.1,100);

}

Thenextlineisthequantitydiscount,soIcanextractthattoo.

functionprice(order){

//priceisbaseprice-quantitydiscount+shipping

constbasePrice=order.quantity*order.itemPrice;

constquantityDiscount=Math.max(0,order.quantity-500)*order.itemPrice*0.05;

returnbasePrice-

quantityDiscount+

Math.min(basePrice*0.1,100);

}

Finally,Ifinishwiththeshipping.AsIdothat,Icanremovethecomment,too,becauseitnolongersaysanythingthecodedoesn’tsay.

functionprice(order){

constbasePrice=order.quantity*order.itemPrice;

constquantityDiscount=Math.max(0,order.quantity-500)*order.itemPrice*0.05;

constshipping=Math.min(basePrice*0.1,100);

returnbasePrice-quantityDiscount+shipping;

}

Example:withaClass

Here’sthesamecode,butthistimeinthecontextofaclass

classOrder{

constructor(aRecord){

this._data=aRecord;

}

getquantity(){returnthis._data.quantity;}

getitemPrice(){returnthis._data.itemPrice;}

getprice(){

returnthis.quantity*this.itemPrice-

Math.max(0,this.quantity-500)*this.itemPrice*0.05+

Math.min(this.quantity*this.itemPrice*0.1,100);

}

}

Inthiscase,Iwanttoextractthesamenames,butIrealizethatthenamesapplytotheOrderasawhole,notjustthecalculationoftheprice.Sincetheyapplytothewholeorder,I’minclinedtoextractthenamesasmethodsratherthanvariables.

classOrder{

constructor(aRecord){

this._data=aRecord;

}

getquantity(){returnthis._data.quantity;}

getitemPrice(){returnthis._data.itemPrice;}

getprice(){

returnthis.basePrice-this.quantityDiscount+this.shipping;

}

getbasePrice(){returnthis.quantity*this.itemPrice;}

getquantityDiscount(){returnMath.max(0,this.quantity-500)*this.itemPrice*0.05;}

getshipping(){returnMath.min(this.basePrice*0.1,100);}

}

Thisisoneofthegreatbenefitsofobjects—theygiveyouareasonableamount

ofcontextforlogictoshareotherbitsoflogicanddata.Forsomethingassimpleasthis,itdoesn’tmattersomuch,butwithalargerclassitbecomesveryusefultocalloutcommonhunksofbehaviorastheirownabstractionswiththeirownnamestorefertothemwheneverI’mworkingwiththeobject.

InlineVariable

formerly:InlineTemp

inverseof:ExtractVariable(119)

Motivation

Variablesprovidenamesforexpressionswithinafunction,andassuchtheyareusuallyaGoodThing.Butsometimes,thenamedoesn’treallycommunicatemorethantheexpressionitself.Atothertimes,youmayfindthatavariablegetsinthewayofrefactoringtheneighboringcode.Inthesecases,itcanbeusefultoinlinethevariable.

Mechanics

Checkthattheright-handsideoftheassignmentisfreeofsideeffects.

Ifthevariableisn’talreadydeclaredimmutable,dosoandtest.

Thischecksthatit’sonlyassignedtoonce.

Findthefirstreferencetothevariableandreplaceitwiththeright-handsideoftheassignment.

Test.

Repeatreplacingreferencestothevariableuntilyou’vereplacedallofthem.

Removethedeclarationandassignmentofthevariable.

Test.

ChangeFunctionDeclaration

aka:RenameFunction

formerly:RenameMethod

formerly:AddParameter

formerly:RemoveParameter

aka:ChangeSignature

Motivation

Functionsrepresenttheprimarywaywebreakaprogramdownintoparts.Functiondeclarationsrepresenthowthesepartsfittogether—effectively,theyrepresentthejointsinoursoftwaresystems.And,aswithanyconstruction,muchdependsonthosejoints.Goodjointsallowmetoaddnewpartsthesystemeasily,butbadonesareaconstantsourceofdifficulty,makingithardertofigureoutwhatthesoftwaredoesandhowtomodifyitasmyneedschange.Fortunately,software,beingsoft,allowsmetochangethesejoints,providingIdoitcarefully.

Themostimportantelementofsuchajointisthenameofthefunction.AgoodnameallowsmetounderstandwhatthefunctiondoeswhenIseeitcalled,withoutseeingthecodethatdefinesitsimplementation.However,comingupwithgoodnamesishard,andIrarelygetmynamesrightthefirsttime.WhenIfindanamethat’sconfusedme,I’mtemptedtoleaveit—afterall,it’sonlyaname.ThisistheworkoftheevildemonObfuscatis;forthesakeofmyprogram’ssoulImustneverlistentohim.IfIseeafunctionwiththewrongname,itisimperativethatIchangeitassoonasIunderstandwhatabetternamecouldbe.Thatway,thenexttimeI’mlookingatthiscode,Idon’thavetofigureoutagainwhat’sgoingon.(Often,agoodwaytoimproveanameistowriteacommenttodescribethefunction’spurpose,thenturnthatcommentintoaname.)

Similarlogicappliestoafunction’sparameters.Theparametersofafunctiondictatehowafunctionfitsinwiththerestofitsworld.ParameterssetthecontextinwhichIcanuseafunction.IfIhaveafunctiontoformataperson’stelephonenumber,andthatfunctiontakesapersonasitsargument,thenIcan’tuseittoformatacompany’stelephonenumber.IfIreplacethepersonparameterwiththetelephonenumberitself,thentheformattingcodeismorewidelyuseful.

Apartfromincreasingafunction’srangeofapplicability,Icanalsoremovesomecoupling,changingwhatmodulesneedtoconnecttoothers.Telephoneformattinglogicmaysitinamodulethathasnoknowledgeaboutpeople.ReducinghowmuchmodulesneedtoknowabouteachotherhelpsreducehowmuchIneedtoputintomybrainwhenIchangesomething—andmybrainisn’tasbigasitusedtobe(thatdoesn’tsayanythingaboutthesizeofitscontainer,

though).

Choosingtherightparametersisn’tsomethingthatadherestosimplerules.Imayhaveasimplefunctionfordeterminingifapaymentisoverdue,bylookingatifit’solderthan30days.Shouldtheparametertothisfunctionbethepaymentobject,ortheduedateofthepayment?Usingthepaymentcouplesthefunctiontotheinterfaceofthepaymentobject.ButifIusethepayment,Icaneasilyaccessotherpropertiesofthepayment,shouldthelogicevolve,withouthavingtochangeeverybitofcodethatcallsthisfunction—essentially,increasingtheencapsulationofthefunction.

Theonlyrightanswertothispuzzleisthatthereisnorightanswer,especiallyovertime.SoIfindit’sessentialtobefamiliarwithChangeFunctionDeclarationsothecodecanevolvewithmyunderstandingofwhatthebestjointsinthecodeneedtobe.

Usually,IonlyusethemainnameofarefactoringwhenIrefertoitfromelsewhereinthisbook.However,sincerenamingissuchasignificantusecaseforChangeFunctionDeclaration,ifI’mjustrenamingsomething,I’llrefertothisrefactoringasChangeFunctionDeclaration(124)tomakeitclearerwhatI’mdoing.WhetherI’mmerelyrenamingormanipulatingtheparameters,Iusethesamemechanics.

Mechanics

Inmostoftherefactoringsinthisbook,Ipresentonlyasinglesetofmechanics.Thisisn’tbecausethereisonlyonesetthatwilldothejobbutbecause,usually,onesetofmechanicswillworkreasonablywellformostcases.ChangeFunctionDeclaration,however,isanexception.Thesimplemechanicsareofteneffective,butthereareplentyofcaseswhenamoregradualmigrationmakesmoresense.So,withthisrefactoring,IlookatthechangeandaskmyselfifIthinkIcanchangethedeclarationandallitscallerseasilyinonego.Ifso,Ifollowthesimplemechanics.Themigration-stylemechanicsallowmetochangethecallersmoregradually—whichisimportantifIhavelotsofthem,theyareawkwardtogetto,thefunctionisapolymorphicmethod,orIhaveamorecomplicatedchangetothedeclaration.

SimpleMechanics

Ifyou’reremovingaparameter,ensureitisn’treferencedinthebodyofthefunction.

Changethemethoddeclarationtothedesireddeclaration.

Findallreferencestotheoldmethoddeclaration,updatethemtothenewone.

Test.

It’softenbesttoseparatechanges,soifyouwanttobothchangethenameandaddaparameter,dotheseasseparatesteps.(Inanycase,ifyourunintotrouble,revertandusethemigrationmechanicsinstead.)

MigrationMechanics

Ifnecessary,refactorthebodyofthefunctiontomakeiteasytodothefollowingextractionstep.

UseExtractFunction(106)onthefunctionbodytocreatethenewfunction.

Ifthenewfunctionwillhavethesamenameastheoldone,givethenewfunctionatemporarynamethat’seasytosearchfor.

Iftheextractedfunctionneedsadditionalparameters,usethesimplemechanicstoaddthem.

Test.

ApplyInlineFunction(115)totheoldfunction.

Ifyouusedatemporaryname,useChangeFunctionDeclaration(124)againtorestoreittotheoriginalname.

Test.

Ifyou’rechangingamethodonaclasswithpolymorphism,you’llneedtoaddindirectionforeachbinding.Ifthemethodispolymorphicwithinasingleclasshierarchy,youonlyneedtheforwardingmethodonthesuperclass.Ifthepolymorphismhasnosuperclasslink,thenyou’llneedforwardingmethodsoneachimplementationclass.

IfyouarerefactoringapublishedAPI,youcanpausetherefactoringonceyou’vecreatedthenewfunction.Duringthispause,deprecatetheoriginalfunctionandwaitforclientstochangetothenewfunction.Theoriginalfunctiondeclarationcanberemovedwhen(andif)you’reconfidentalltheclientsoftheoldfunctionhavemigratedtothenewone.

Example:RenamingaFunction(SimpleMechanics)

Considerthisfunctionwithanoverlyabbrevedname.

functioncircum(radius){

return2*Math.PI*radius;

}

Iwanttochangethattosomethingmoresensible.Ibeginbychangingthedeclaration:

functioncircumference(radius){

return2*Math.PI*radius;

}

Ithenfindallthecallersofcircumandchangethenametocircumference.

Differentlanguageenvironmentshaveanimpactonhoweasyitistofindallthereferencestotheoldfunction.StatictypingandagoodIDEprovidethebestexperience,usuallyallowingmetorenamefunctionsautomaticallywithlittlechanceoferror.Withoutstatictyping,thiscanbemoreinvolved;evengoodsearchingtoolswillthenhavealotoffalsepositives.

Iusethesameapproachforaddingorremovingparameters:findallthecallers,changethedeclaration,andchangethecallers.It’softenbettertodotheseasseparatesteps—so,ifI’mbothrenamingthefunctionandaddingaparameter,Ifirstdotherename,test,thenaddtheparameter,andtestagain.

AdisadvantageofthissimplewayofdoingtherefactoringisthatIhavetodoallthecallersandthedeclaration(orallofthem,ifpolymorphic)atonce.Ifthereareonlyafewofthem,orifIhavedecentautomatedrefactoringtools,thisisreasonable.Butifthere’salot,itcangettricky.Anotherproblemiswhenthenamesaren’tunique—e.g.IwanttorenametheachangeAddressmethodonapersonclassbutthesamemethod,whichIdon’twanttochange,existsonaninsuranceagreementclass.Themorecomplexthechangeis,thelessIwanttodo

itinonegolikethis.Whenthiskindofproblemarises,Iusethemigrationmechanicsinstead.Similarly,ifIusesimplemechanicsandsomethinggoeswrong,I’llrevertthecodetothelastknowngoodstateandtryagainusingmigrationmechanics.

Example:RenamingaFunction(MigrationMechanics)

Again,Ihavethisfunctionwithitsoverlyabbrevedname.

functioncircum(radius){

return2*Math.PI*radius;

}

Todothisrefactoringwithmigrationmechanics,IbeginbyapplyingExtractFunction(106)totheentirefunctionbody.

functioncircum(radius){

returncircumference(radius);

}

functioncircumference(radius){

return2*Math.PI*radius;

}

Itestthat,thenapplyInlineFunction(115)totheoldfunctions.Ifindallthecallsoftheoldfunctionandreplaceeachonewithacallofthenewone.Icantestaftereachchange,whichallowsmetodothemoneatatime.OnceI’vegotthemall,Iremovetheoldfunction.

Withmostrefactorings,I’mchangingcodethatIcanmodify,butthisrefactoringcanbehandywithapublishedAPI—thatis,oneusedbycodethatI’munabletochangemyself.Icanpausetherefactoringaftercreatingcircumferenceand,ifpossible,markcircumasdeprecated.Iwillthenwaitforcallerstochangetousecircumference;oncetheydo,Icandeletecircum.EvenifI’mneverabletoreachthehappypointofdeletingcircum,atleastIhaveabetternamefornewcode.

Example:AddingaParameter

Insomesoftware,tomanagealibraryofbooks,Ihaveabookclasswhichhastheabilitytotakeareservationforacustomer.

classBook…

addReservation(customer){

this._reservations.push(customer);

}

Ineedtosupportapriorityqueueforreservations.Thus,IneedanextraparameteronaddReservationtoindicatewhetherthereservationshouldgointheusualqueueorthehigh-priorityqueue.IfIcaneasilyfindandchangeallthecallers,thenIcanjustgoaheadwiththechange—butifnot,Icanusethemigrationapproach,whichI’llshowhere.

IbeginbyusingExtractFunction(106)onthebodyofaddReservationtocreatethenewfunction.AlthoughitwilleventuallybecalledaddReservation,thenewandoldfunctionscan’tcoexistwiththesamename.SoIuseatemporarynamethatwillbeeasytosearchforlater.

classBook…

addReservation(customer){

this.zz_addReservation(customer);

}

zz_addReservation(customer){

this._reservations.push(customer);

}

Ithenaddtheparametertothenewdeclarationanditscall(ineffect,usingthesimplemechanics).

classBook…

addReservation(customer){

this.zz_addReservation(customer,false);

}

zz_addReservation(customer,isPriority){

this._reservations.push(customer);

}

WhenIuseJavaScript,beforeIchangeanyofthecallers,IliketoapplyIntroduceAssertion(299)tocheckthenewparameterisusedbythecaller.

classBook…

zz_addReservation(customer,isPriority){

assert(isPriority===true||isPriority===false);

this._reservations.push(customer);

}

Now,whenIchangethecallers,ifImakeamistakeandleaveoffthenewparameter,thisassertionwillhelpmecatchthemistake.AndIknowfromlongexperiencetherearefewmoremistake-proneprogrammersthanmyself.

Now,IcanstartchangingthecallersbyusingInlineFunction(115)ontheoriginalfunction.Thisallowsmetochangeonecalleratatime.

Ithenrenamethenewfunctionbacktotheoriginal.Usually,thesimplemechanicsworkfineforthis,butIcanalsousethemigrationapproachifIneedto.

Example:ChangingaParametertoOneofItsProperties

Theexamplessofararesimplechangesofanameandaddinganewparameter,butwiththemigrationmechanics,thisrefactoringcanhandlemorecomplicatedcasesquiteneatly.Here’sanexamplethatisabitmoreinvolved.

IhaveafunctionwhichdeterminesifacustomerisbasedinNewEngland.

functioninNewEngland(aCustomer){

return["MA","CT","ME","VT","NH","RI"].includes(aCustomer.address.state);

}

Hereisoneofitscallers

caller…

constnewEnglanders=someCustomers.filter(c=>inNewEngland(c));

inNewEnglandonlyusesthecustomer’shomestatetodetermineifit’sinNewEngland.I’dprefertorefactorinNewEnglandsothatittakesastatecodeasaparameter,makingitusableinmorecontextsbyremovingthedependencyonthecustomer.

WithChangeFunctionDeclaration,myusualfirstmoveistoapplyExtractFunction(106),butinthiscaseIcanmakeiteasierbyfirstrefactoringthe

functionbodyalittle.IuseExtractVariable(119)onmydesirednewparameter.

functioninNewEngland(aCustomer){

conststateCode=aCustomer.address.state;

return["MA","CT","ME","VT","NH","RI"].includes(stateCode);

}

NowIuseExtractFunction(106)tocreatethatnewfunction.

functioninNewEngland(aCustomer){

conststateCode=aCustomer.address.state;

returnxxNEWinNewEngland(stateCode);

}

functionxxNEWinNewEngland(stateCode){

return["MA","CT","ME","VT","NH","RI"].includes(stateCode);

}

Igivethefunctionanamethat’seasytoautomaticallyreplacetoturnintotheoriginalnamelater.(YoucantellIdon’thaveastandardforthesetemporarynames.)

IapplyInlineVariable(123)ontheinputparameterintheoriginalfunction.

functioninNewEngland(aCustomer){

returnxxNEWinNewEngland(aCustomer.address.state);

}

IuseInlineFunction(115)tofoldtheoldfunctionintoitscallers,effectivelyreplacingthecalltotheoldfunctionwithacalltothenewone.Icandotheseoneatatime.

caller…

constnewEnglanders=someCustomers.filter(c=>xxNEWinNewEngland(c.address.state))

OnceI’veinlinedtheoldfunctionintoeverycaller,IuseChangeFunctionDeclarationagaintochangethenameofthenewfunctiontothatoftheoriginal.

caller…

constnewEnglanders=someCustomers.filter(c=>inNewEngland(c.address.state));

toplevel…

functioninNewEngland(stateCode){

return["MA","CT","ME","VT","NH","RI"].includes(stateCode);

}

Automatedrefactoringtoolsmakethemigrationmechanicsbothlessusefulandmoreeffective.Theymakeitlessusefulbecausetheyhandleevencomplicatedrenamesandparameterchangessafer,soIdon’thavetousethemigrationapproachasoftenasIdowithoutthatsupport.However,incaseslikethisexample,wherethetoolscan’tdothewholerefactoring,theystillmakeitmucheasierasthekeymovesofextractandinlinecanbedonemorequicklyandsafelywiththetool.

EncapsulateVariable

formerly:Self-EncapsulateField

formerly:EncapsulateField

Motivation

Refactoringisallaboutmanipulatingtheelementsofourprograms.Dataismoreawkwardtomanipulatethanfunctions.Sinceusingafunctionusuallymeanscallingit,Icaneasilyrenameormoveafunctionwhilekeepingtheoldfunction

intactasaforwardingfunction(somyoldcodecallstheoldfunction,whichcallsthenewfunction).I’llusuallynotkeepthisforwardingfunctionaroundforlong,butitdoessimplifytherefactoring.

DataismoreawkwardbecauseIcan’tdothat.IfImovedataaround,Ihavetochangeallthereferencestothedatainasinglecycletokeepthecodeworking.Fordatawithaverysmallscopeofaccess,suchasatemporaryvariableinasmallfunction,thisisn’taproblem.Butasthescopegrows,sodoesthedifficulty,whichiswhyglobaldataissuchapain.

SoifIwanttomovewidely-accesseddata,oftenthebestapproachistofirstencapsulateitbyroutingallitsaccessthroughfunctions.Thatway,Iturnthedifficulttaskofreorganizingdataintothesimplertaskofreorganizingfunctions.

Encapsulatingdataisvaluableforotherthingstoo.Itprovidesaclearpointtomonitorchangesanduseofthedata;Icaneasilyaddvalidationorconsequentiallogicontheupdates.Itismyhabittomakeallmutabledataencapsulatedlikethisandonlyaccessedthroughfunctionsifitsscopeisgreaterthanasinglefunction.Thegreaterthescopeofthedata,themoreimportantitistoencapsulate.MyapproachwithlegacycodeisthatwheneverIneedtochangeoraddanewreferencetosuchavariable,Ishouldtaketheopportunitytoencapsulateit.ThatwayIpreventtheincreaseofcouplingtocommonlyuseddata.

Thisprincipleiswhytheobject-orientedapproachputssomuchemphasisonkeepinganobject’sdataprivate.WheneverIseeapublicfield,IconsiderusingEncapsulateVariable(inthatcaseoftencalledEncapsulateVariable(132))toreduceitsvisibility.Somegofurtherandarguethateveninternalreferencestofieldswithinaclassshouldgothroughaccessorfunctions—anapproachknownasself-encapsulation.Onthewhole,Ifindself-encapsulationexcessive—ifaclassissobigthatIneedtoself-encapsulateitsfields,itneedstobebrokenupanyway.Butself-encapsulatingafieldisausefulstepbeforesplittingaclass.

Keepingdataencapsulatedismuchlessimportantforimmutabledata.Whenthedatadoesn’tchange,Idon’tneedaplacetoputinvalidationorotherlogichooksbeforeupdates.Icanalsofreelycopythedataratherthanmoveit—soIdon’thavetochangereferencesfromoldlocations,nordoIworryaboutsectionsofcodegettingstaledata.Immutabilityisapowerfulpreservative.

Mechanics

Createencapsulatingfunctionstoaccessandupdatethevariable.

Runstaticchecks.

Foreachreferencetothevariable,replacewithacalltotheappropriateencapsulatingfunction.Testaftereachreplacement.

Restrictthevisibilityofthevariable.

Sometimesit’snotpossibletopreventaccesstothevariable.Ifso,itmaybeusefultodetectanyremainingreferencesbyrenamingthevariableandtesting.

Test.

Ifthevalueofthevariableisarecord,considerEncapsulateRecord(160).

Example

Considersomeusefuldataheldinaglobalvariable.

letdefaultOwner={firstName:"Martin",lastName:"Fowler"};

Likeanydata,it’sreferencedwithcodelikethis:

spaceship.owner=defaultOwner;

Andupdatedlikethis:

defaultOwner={firstName:"Rebecca",lastName:"Parsons"};

Todoabasicencapsulationonthis,Istartbydefiningfunctionstoreadandwritethedata.

functiongetDefaultOwner(){returndefaultOwner;}

functionsetDefaultOwner(arg){defaultOwner=arg;}

IthenstartworkingonreferencestodefaultOwner.WhenIseeareference,Ireplaceitwithacalltothegettingfunction.

spaceship.owner=getDefaultOwner();

WhenIseeanassignment,Ireplaceitwiththesettingfunction.

setDefaultOwner({firstName:"Rebecca",lastName:"Parsons"});

Itestaftereachreplacement.

OnceI’mdonewithallthereferences,Irestrictthevisibilityofthevariable.Thisbothchecksthattherearen’tanyreferencesthatI’vemissed,andensuresthatfuturechangestothecodewon’taccessthevariabledirectly.IcandothatinJavaScriptbymovingboththevariableandtheaccessormethodstotheirownfileandonlyexportingtheaccessormethods.

defaultOwner.js…

letdefaultOwner={firstName:"Martin",lastName:"Fowler"};

exportfunctiongetDefaultOwner(){returndefaultOwner;}

exportfunctionsetDefaultOwner(arg){defaultOwner=arg;}

IfI’minasituationwhereIcannotrestricttheaccesstoavariable,itmaybeusefultorenamethevariableandretest.Thatwon’tpreventfuturedirectaccess,butnamingthevariablesomethingmeaningfulandawkwardsuchas__privateOnly_defaultOwnermayhelp.

Idon’tliketheuseofgetprefixesongetters,soI’llrenametoremoveit.

defaultOwner.js…

letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};

exportfunctiongetdefaultOwner(){returndefaultOwnerData;}

exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}

AcommonconventioninJavaScriptistonameagettingfunctionandsettingfunctionthesameanddifferentiatethemduethepresenceofanargument.IcallthispracticeOverloadedGetterSetter[bib-overloaded-getter-setter]andstronglydislikeit.So,eventhoughIdon’tlikethegetprefix,Iwillkeepthesetprefix.

EncapsulatingtheValue

ThebasicrefactoringI’veoutlinedhereencapsulatesareferencetosomedatastructure,allowingmetocontrolitsaccessandre-assignment.Butitdoesn’t

controlchangestothatstructure.

constowner1=defaultOwner();

assert.equal("Fowler",owner1.lastName,"whenset");

constowner2=defaultOwner();

owner2.lastName="Parsons";

assert.equal("Parsons",owner1.lastName,"afterchangeowner2");//isthisok?

Thebasicrefactoringencapsulatesthereferencetothedataitem.Inmanycases,thisisallIwanttodoforthemoment.ButIoftenwanttotaketheencapsulationdeepertocontrolnotjustchangestothevariablebutalsotoitscontents.

Forthis,Ihaveacoupleofoptions.Thesimplestoneistopreventanychangestothevalue.Myfavoritewaytohandlethisisbymodifyingthegettingfunctiontoreturnacopyofthedata.

defaultOwner.js…

letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};

exportfunctiondefaultOwner(){returnObject.assign({},defaultOwnerData);}

exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}

Iusethisapproachparticularlyoftenwithlists.IfIreturnacopyofthedata,anyclientsusingitcanchangeit,butthatchangeisn’treflectedintheshareddata.Ihavetobecarefulwithusingcopies,however:Somecodemayexpecttochangeshareddata.Ifthat’sthecase,I’mrelyingonmyteststodetectaproblem.Analternativeistopreventchanges—andagoodwayofdoingthatisEncapsulateRecord(160).

letdefaultOwnerData={firstName:"Martin",lastName:"Fowler"};

exportfunctiondefaultOwner(){returnnewPerson(defaultOwnerData);}

exportfunctionsetDefaultOwner(arg){defaultOwnerData=arg;}

classPerson{

constructor(data){

this._lastName=data.lastName;

this._firstName=data.firstName

}

getlastName(){returnthis._lastName;}

getfirstName(){returnthis._firstName;}

//andsoonforotherproperties

Now,anyattempttoreassignthepropertiesofthedefaultownerwillcauseanerror.Differentlanguageshavedifferenttechniquestodetectorpreventchanges

likethis,sodependingonthelanguageI’dconsiderotheroptions.

Detectingandpreventingchangeslikethisisoftenworthwhileasatemporarymeasure.Icaneitherremovethechanges,orprovidesuitablemutatingfunctions.Then,oncetheyarealldealtwith,Icanmodifythegettingmethodtoreturnacopy.

SofarI’vetalkedaboutcopyingongettingdata,butitmaybeworthwhiletomakeacopyinthesettertoo.ThatwilldependonwherethedatacomesfromandwhetherIneedtomaintainalinktoreflectanychangesinthatoriginaldata.IfIdon’tneedsuchalink,acopypreventsaccidentsduetochangesonthatsourcedata.Takingacopymaybesuperfluousmostofthetime,butcopiesinthesecasesusuallyhaveanegligibleeffectonperformance;ontheotherhand,ifIdon’tdothem,thereisariskofalonganddifficultboutofdebugginginthefuture.

Rememberthatthecopyingabove,andtheclasswrapper,bothonlyworkoneleveldeepintherecordstructure.Goingdeeperrequiresmorelevelsofcopiesorobjectwrapping.

Asyoucansee,encapsulatingdataisvaluable,butoftennotstraightforward.Exactlywhattoencapsulate—andhowtodoit—dependsonthewaythedataisbeingusedandthechangesIhaveinmind.Butthemorewidelyit’sused,themoreit’sworthmyattentiontoencapsulateproperly.

RenameVariable

Motivation

Namingthingswellistheheartofclearprogramming.VariablescandoalottoexplainwhatI’mupto—ifInamethemwell.ButIfrequentlygetmynameswrong—sometimesbecauseI’mnotthinkingcarefullyenough,sometimesbecausemyunderstandingoftheproblemimprovesasIlearnmore,andsometimesbecausetheprogram’spurposechangesasmyusers’needschange.

Evenmorethanmostprogramelements,theimportanceofanamedependsonhowwidelyit’sused.Avariableusedinaone-linelambdaexpressionisusuallyeasytofollow—Ioftenuseasingleletterinthatcasesincethevariable’spurposeisclearfromitscontext.Parametersforshortfunctionscanoftenbeterseforthesamereason,althoughinadynamicallytypedlanguagelikeJavaScript,Idoliketoputthetypeintothename(henceparameternameslikeaCustomer).

Persistentfieldsthatlastbeyondasinglefunctioninvocationrequiremorecarefulnaming.ThisiswhereI’mlikelytoputmostofmyattention.

Mechanics

Ifthevariableisusedwidely,considerEncapsulateVariable(132).

Findallreferencestothevariable,andchangeeveryone.

Iftherearereferencesfromanothercodebase,thevariableisapublishedvariable,andyoucannotdothisrefactoring.

Ifthevariabledoesnotchange,youcancopyittoonewiththenewname,thenchangegradually,testingaftereachchange.

Test.

Example

Thesimplestcaseforrenamingavariableiswhenit’slocaltoasinglefunction:atemporargument.It’stootrivialforevenanexample:Ijustfindeachreferenceandchangeit.AfterI’mdone,ItesttoensureIdidn’tmessup.

Problemsoccurwhenthevariablehasawiderscopethanjustasinglefunction.Theremaybealotofreferencesalloverthecodebase.

lettpHd="untitled";

Somereferencesaccessthevariable:

result+=`<h1>${tpHd}</h1>`;

Othersupdateit:

tpHd=obj['articleTitle'];

MyusualresponsetothisisapplyEncapsulateVariable(132).

result+=`<h1>${title()}</h1>`;

setTitle(obj['articleTitle']);

functiontitle(){returntpHd;}

functionsetTitle(arg){tpHd=arg;}

Atthispoint,Icanrenamethevariable.

let_title="untitled";

functiontitle(){return_title;}

functionsetTitle(arg){_title=arg;}

Atthispoint,Icouldcontinuebyinliningthewrappingfunctionssoallcallersareusingthevariabledirectly.ButI’drarelywanttodothis.IfthevariableisusedwidelyenoughthatIfeeltheneedtoencapsulateitinordertochangeitsname,it’sworthkeepingitencapsulatedbehindfunctionsforthefuture.

IncaseswhereIwasgoingtoinline,I’dcallthegettingfunctiongetTitleandnotuseanunderscoreforthevariablenamewhenIrenameit.

RenamingaConstant

IfI’mrenamingaconstant(orsomethingthatactslikeaconstanttoclients)Icanavoidencapsulation,andstilldotherenamegradually,bycopying.Iftheoriginaldeclarationlookslikethis:

constcpyNm="AcmeGooseberries";

Icanbegintherenamingbymakingacopy:

constcompanyName="AcmeGooseberries";

constcpyNm=companyName;

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

Thisworksforconstantsaswellasforvariablesthatareread-onlytoclients(suchasanexportedvariableinJavaScript).

IntroduceParameterObject

Motivation

Ioftenseegroupsofdataitemsthatregularlytraveltogether,appearinginfunctionafterfunction.Suchagroupisadataclump,andIliketoreplaceitwithasingledatastructure.

Groupingdataintoastructureisvaluablebecauseitmakesexplicittherelationshipbetweenthedataitems.Itreducesthesizeofparameterlistsforanyfunctionthatusesthenewstructure.Ithelpsconsistencysinceallfunctionsthatusethestructurewillusethesamenamestogetatitselements.

Buttherealpowerofthisrefactoringishowitenablesdeeperchangestothecode.WhenIidentifythesenewstructures,Icanreorientthebehavioroftheprogramtousethesestructures.Iwillcreatefunctionsthatcapturethecommonbehavioroverthisdata—eitherasasetofcommonfunctionsorasaclassthatcombinesthedatastructurewiththesefunctions.Thisprocesscanchangetheconceptualpictureofthecode,raisingthesestructuresasnewabstractionsthatcangreatlysimplifymyunderstandingofthedomain.Whenthisworks,itcanhavesurprisinglypowerfuleffects—butnoneofthisispossibleunlessIuseIntroduceParameterObjecttobegintheprocess.

Mechanics

Ifthereisn’tasuitablestructurealready,createone.

Iprefertouseaclass,asthatmakesiteasiertogroupbehaviorlateron.Iusuallyliketoensurethesestructuresarevalueobjects[bib-value-object].

Test.

UseChangeFunctionDeclaration(124)toaddaparameterforthenewstructure.

Test.

Adjusteachcallertopassinthecorrectinstanceofthenewstructure.Testaftereachone.

Foreachelementofthenewstructure,replacetheuseoftheoriginalparameterwiththeelementofthestructure.Removetheparameter.Test.

Example

I’llbeginwithsomecodethatlooksatasetoftemperaturereadingsanddetermineswhetheranyofthemfalloutsideofanoperatingrange.Here’swhat

thedatalookslikeforthereadings:

conststation={name:"ZB1",

readings:[

{temp:47,time:"2016-11-1009:10"},

{temp:53,time:"2016-11-1009:20"},

{temp:58,time:"2016-11-1009:30"},

{temp:53,time:"2016-11-1009:40"},

{temp:51,time:"2016-11-1009:50"},

]

};

Ihaveafunctiontofindthereadingsthatareoutsideatemperaturerange.

functionreadingsOutsideRange(station,min,max){

returnstation.readings

.filter(r=>r.temp<min||r.temp>max);

}

Itmightbecalledfromsomecodelikethis:

caller

alerts=readingsOutsideRange(station,

operatingPlan.temperatureFloor,

operatingPlan.temperatureCeiling);

NoticehowthecallingcodepullsthetwodataitemsasapairfromanotherobjectandpassesthepairintoreadingsOutsideRange.TheoperatingplanusesdifferentnamestoindicatethestartandendoftherangecomparedtoreadingsOutsideRange.Arangelikethisisacommoncasewheretwoseparatedataitemsarebettercombinedintoasingleobject.I’llbeginbydeclaringaclassforthecombineddata.

classNumberRange{

constructor(min,max){

this._data={min:min,max:max};

}

getmin(){returnthis._data.min;}

getmax(){returnthis._data.max;}

}

Ideclareaclass,ratherthanjustusingabasicJavaScriptobject,becauseIusuallyfindthisrefactoringtobeafirststeptomovingbehaviorintothenewlycreatedobject.Sinceaclassmakessenseforthis,Igorightaheadanduseone

directly.Ialsodon’tprovideanyupdatemethodsforthenewclass,asI’llprobablymakethisaValueObject(https://martinfowler.com/bliki/ValueObject.html).MosttimesIdothisrefactoring,Icreatevalueobjects.

IthenuseChangeFunctionDeclaration(124)toaddthenewobjectasaparametertoreadingsOutsideRange.

functionreadingsOutsideRange(station,min,max,range){

returnstation.readings

.filter(r=>r.temp<min||r.temp>max);

}

InJavaScript,Icanleavethecallerasis,butinotherlanguagesI’dhavetoaddanullforthenewparameterwhichwouldlooksomethinglikethis:

caller

alerts=readingsOutsideRange(station,

operatingPlan.temperatureFloor,

operatingPlan.temperatureCeiling,

null);

AtthispointIhaven’tchangedanybehavior,andtestsshouldstillpass.Ithengotoeachcallerandadjustittopassinthecorrectdaterange.

caller

constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);

alerts=readingsOutsideRange(station,

operatingPlan.temperatureFloor,

operatingPlan.temperatureCeiling,

range);

Istillhaven’talteredanybehavioryet,astheparameterisn’tused.Alltestsshouldstillwork.

NowIcanstartreplacingtheusageoftheparameters.I’llstartwiththemaximum.

functionreadingsOutsideRange(station,min,max,range){

returnstation.readings

.filter(r=>r.temp<min||r.temp>range.max);

}

caller

constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);

alerts=readingsOutsideRange(station,

operatingPlan.temperatureFloor,

operatingPlan.temperatureCeiling,

range);

Icantestatthispoint,thenremovetheotherparameter.

functionreadingsOutsideRange(station,min,range){

returnstation.readings

.filter(r=>r.temp<range.min||r.temp>range.max);

}

caller

constrange=newNumberRange(operatingPlan.temperatureFloor,operatingPlan.temperatureCeiling);

alerts=readingsOutsideRange(station,

operatingPlan.temperatureFloor,

range);

Thatcompletesthisrefactoring.However,replacingaclumpofparameterswitharealobjectisjustthesetupforthereallygoodstuff.ThegreatbenefitsofmakingaclasslikethisisthatIcanthenmovebehaviorintothenewclass.Inthiscase,I’daddamethodforrangethattestsifavaluefallswithintherange.

functionreadingsOutsideRange(station,range){

returnstation.readings

.filter(r=>!range.contains(r.temp));

}

classNumberRange…

contains(arg){return(arg>=this.min&&arg<=this.max);}

Thisisafirststeptocreatingarange[bib-range-pattern]thatcantakeonalotofusefulbehavior.OnceI’veidentifiedtheneedforarangeinmycode,IcanbeconstantlyonthelookoutforothercaseswhereIseeamax/minpairofnumbersandreplacethemwitharange.(Oneimmediatepossibilityistheoperatingplan,replacingtemperatureFloorandtemperatureCeilingwithatemperatureRange.)AsIlookathowthesepairsareused,Icanmovemoreusefulbehaviorintotherangeclass,simplifyingitsusageacrossthecodebase.OneofthefirstthingsImayaddisavalue-basedequalitymethodtomakeita

truevalueobject.

CombineFunctionsintoClass

Motivation

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

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

Inadditiontoorganizingalreadyformedfunctions,thisrefactoringalsoprovides

agoodopportunitytoidentifyotherbitsofcomputationandrefactorthemintomethodsonthenewclass.

AnotherwayoforganizingfunctionstogetherisCombineFunctionsintoTransform(149).Whichonetousedependsmoreonthebroadercontextoftheprogram.Onesignificantadvantageofusingaclassisthatitallowsclientstomutatethecoredataoftheobject,andthederivationsremainconsistent.

Aswellasaclass,functionslikethiscanalsobecombinedintoanestedfunction.UsuallyIpreferaclasstoanestedfunction,asitcanbedifficulttotestfunctionsnestedwithinanother.ClassesarealsonecessarywhenthereismorethanonefunctioninthegroupthatIwanttoexposetocollaborators.

Languagesthatdon’thaveclassesasafirst-classelement,butdohavefirst-classfunctions,oftenusetheFunctionAsObject(https://martinfowler.com/bliki/FunctionAsObject.html)toprovidethiscapability.

Mechanics

ApplyEncapsulateRecord(160)tothecommondatarecordthatthefunctionsshare.

Ifthedatathatiscommonbetweenthefunctionsisn’talreadygroupedintoarecordstructure,useIntroduceParameterObject(140)tocreatearecordtogroupittogether.

TakeeachfunctionthatusesthecommonrecordanduseMoveFunction(196)tomoveitintothenewclass.

Anyargumentstothefunctioncallthatarememberscanberemovedfromtheargumentlist.

EachbitoflogicthatmanipulatesthedatacanbeextractedwithExtractFunction(106)andthenmovedintothenewclass.

Example

IgrewupinEngland,acountryrenownedforitsloveofTea.(Personally,I

don’tlikemostteatheyserveinEngland,buthavesinceacquiredatasteforChineseandJapaneseteas.)Somyauthor’sfantasyconjuresupastateutilityforprovidingteatothepopulation.Everymonththeyreadtheteameters,togetarecordlikethis:

reading={customer:"ivan",quantity:10,month:5,year:2017};

Ilookthroughthecodethatprocessestheserecords,andIseelotsofplaceswheresimilarcalculationsaredoneonthedata.SoIfindaspotthatcalculatesthebasecharge:

client1…

constaReading=acquireReading();

constbaseCharge=baseRate(aReading.month,aReading.year)*aReading.quantity;

BeingEngland,everythingessentialmustbetaxed,soitiswithtea.Buttherulesallowatleastanessentiallevelofteatobefreeoftaxation.

client2…

constaReading=acquireReading();

constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);

consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));

I’msurethat,likeme,younoticedthattheformulaforthebasechargeisduplicatedbetweenthesetwofragments.Ifyou’relikeme,you’realreadyreachingforExtractFunction(106).Interestingly,itseemsourworkhasbeendoneforuselsewhere.

client3…

constaReading=acquireReading();

constbasicChargeAmount=calculateBaseCharge(aReading);

functioncalculateBaseCharge(aReading){

returnbaseRate(aReading.month,aReading.year)*aReading.quantity;

}

Giventhis,Ihaveanaturalimpulsetochangethetwoearlierbitsofclientcodetousethisfunction.Butthetroublewithtop-levelfunctionslikethisisthattheyareofteneasytomiss.I’dratherchangethecodetogivethefunctionacloserconnectiontothedataitprocesses.Agoodwaytodothisistoturnthedatainto

aclass.

Toturntherecordintoaclass,IuseEncapsulateRecord(160).

classReading{

constructor(data){

this._customer=data.customer;

this._quantity=data.quantity;

this._month=data.month;

this._year=data.year;

}

getcustomer(){returnthis._customer;}

getquantity(){returnthis._quantity;}

getmonth(){returnthis._month;}

getyear(){returnthis._year;}

}

Tomovethebehavior,I’llstartwiththefunctionIalreadyhave:calculateBaseCharge.Tousethenewclass,IneedtoapplyittothedataassoonasI’veacquiredit.

client3…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

constbasicChargeAmount=calculateBaseCharge(aReading);

IthenuseMoveFunction(196)tomovecalculateBaseChargeintothenewclass.

classReading…

getcalculateBaseCharge(){

returnbaseRate(this.month,this.year)*this.quantity;

}

client3…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

constbasicChargeAmount=aReading.calculateBaseCharge;

WhileI’matit,IuseChangeFunctionDeclaration(124)tomakeitsomethingmoretomyliking.

getbaseCharge(){

returnbaseRate(this.month,this.year)*this.quantity;

}

client3…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

constbasicChargeAmount=aReading.baseCharge;

Withthisnaming,theclientofthereadingclasscan’ttellwhetherthebasechargeisafieldoraderivedvalue.ThisisaGoodThing—theUniformAccessPrinciple[bib-uniform-access].

Inowalterthefirstclienttocallthemethodratherthanrepeatthecalculation.

client1…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

constbaseCharge=aReading.baseCharge;

There’sastrongchanceI’lluseInlineVariable(123)onthebaseChargevariablebeforethedayisout.Butmorerelevanttothisrefactoringistheclientthatcalculatesthetaxableamount.Myfirststephereistousethenewbasechargeproperty.

client2…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

consttaxableCharge=Math.max(0,aReading.baseCharge-taxThreshold(aReading.year));

IuseExtractFunction(106)onthecalculationforthetaxablecharge.

functiontaxableChargeFn(aReading){

returnMath.max(0,aReading.baseCharge-taxThreshold(aReading.year));

}

client3…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

consttaxableCharge=taxableChargeFn(aReading);

ThenIapplyMoveFunction(196).

classReading…

gettaxableCharge(){

returnMath.max(0,this.baseCharge-taxThreshold(this.year));

}

client3…

constrawReading=acquireReading();

constaReading=newReading(rawReading);

consttaxableCharge=aReading.taxableCharge;

Sinceallthederiveddataiscalculatedondemand,IhavenoproblemshouldIneedtoupdatethestoreddata.Ingeneral,Ipreferimmutabledata,butmanycircumstancesforceustoworkwithmutabledata(suchasJavaScript,alanguageecosystemthatwasn’tdesignedwithimmutabilityinmind).Whenthereisareasonablechancethedatawillbeupdatedsomewhereintheprogram,thenaclassisveryhelpful.

CombineFunctionsintoTransform

Motivation

Softwareofteninvolvesfeedingdataintoprogramsthatcalculatevariousderivedinformationfromit.Thesederivedvaluesmaybeneededinseveralplaces,andthosecalculationsareoftenrepeatedwhereverthederiveddataisused.Iprefertobringallofthesederivationstogether,soIhaveaconsistentplacetofindandupdatethemandavoidanyduplicatelogic.

Onewaytodothisistouseadatatransformationfunctionthattakesthesourcedataasinputandcalculatesallthederivations,puttingeachderivedvalueasafieldintheoutputdata.Then,toexaminethederivations,allIneeddoislookatthetransformfunction.

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

code.Usingatransformstoresderiveddatainthenewrecord,soifthesourcedatachanges,Iwillrunintoinconsistencies.

OneofthereasonsIliketodocombinefunctionsistoavoidduplicationofthederivationlogic.IcandothatjustbyusingExtractFunction(106)onthelogic,butit’softendifficulttofindthefunctionsunlesstheyarekeptclosetothedatastructurestheyoperateon.Usingatransform(oraclass)makesiteasytofindandusethem.

Mechanics

Createatransformationfunctionthatthattakestherecordtobetransformedandreturnsthesamevalues.

Thiswillusuallyinvolveadeepcopyoftherecord.Itisoftenworthwhiletowriteatesttoensurethetransformdoesnotaltertheoriginalrecord.

Picksomelogicandmoveitsbodyintothetransformtocreateanewfieldintherecord.Changetheclientcodetoaccessthenewfield.

Ifthelogiciscomplex,useExtractFunction(106)first.

Test.

Repeatfortheotherrelevantfunctions.

Example

WhereIgrewup,teaisanimportantpartoflife—somuchthatIcanimagineaspecialutilitythatprovidesteatothepopulacethat’sregulatedlikeautility.Everymonth,theutilitygetsareadingofhowmuchteaacustomerhasacquired.

reading={customer:"ivan",quantity:10,month:5,year:2017};

Codeinvariousplacescalculatesvariousconsequencesofthisteausage.Onesuchcalculationisthebasemonetaryamountthat’susedtocalculatethechargeforthecustomer.

client1…

constaReading=acquireReading();

constbaseCharge=baseRate(aReading.month,aReading.year)*aReading.quantity;

Anotheristheamountthatshouldbetaxed—whichislessthanthebaseamountsincethegovernmentwiselyconsidersthateverycitizenshouldgetsometeatax-free.

client2…

constaReading=acquireReading();

constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);

consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));

Lookingthroughthiscode,Iseethesecalculationsrepeatedinseveralplaces.Suchduplicationisaskingfortroublewhentheyneedtochange(andI’dbetit’s“when”not“if”).IcandealwiththisrepetitionbyusingExtractFunction(106)onthesecalculations,butsuchfunctionsoftenendupscatteredaroundtheprogrammakingithardforfuturedeveloperstorealizetheyarethere.Indeed,lookingaroundIdiscoversuchafunction,usedinanotherareaofthecode.

client3…

constaReading=acquireReading();

constbasicChargeAmount=calculateBaseCharge(aReading);

functioncalculateBaseCharge(aReading){

returnbaseRate(aReading.month,aReading.year)*aReading.quantity;

}

Onewayofdealingwiththisistomoveallofthesederivationsintoatransformationstepthattakestherawreadingandemitsareadingenrichedwithallthecommonderivedresults.

Ibeginbycreatingatransformationfunctionthatmerelycopiestheinputobject.

functionenrichReading(original){

constresult=_.cloneDeep(original);

returnresult;

}

I’musingthecloneDeepfromlodashtocreateadeepcopy.

WhenI’mapplyingatransformationthatproducesessentiallythesamethingbut

withadditionalinformation,Iliketonameitusing“enrich”.IfitwereproducingsomethingIfeltwasdifferent,Iwouldnameitusing“transform”.

IthenpickoneofthecalculationsIwanttochange.First,Ienrichthereadingituseswiththecurrentonethatdoesnothingyet.

client3…

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

constbasicChargeAmount=calculateBaseCharge(aReading);

IuseMoveFunction(196)oncalculateBaseChargetomoveitintotheenrichmentcalculation.

functionenrichReading(original){

constresult=_.cloneDeep(original);

result.baseCharge=calculateBaseCharge(result);

returnresult;

}

Withinthetransformationfunction,I’mhappytomutatearesultobject,insteadofcopyingeachtime.Ilikeimmutability,butmostcommonlanguagesmakeitdifficulttoworkwith.I’mpreparedtogothroughtheextraefforttosupportitatboundaries,butwillmutatewithinsmallerscopes.Ialsopickmynames(usingaReadingastheaccumulatingvariable)tomakeiteasiertomovethecodeintothetransformerfunction.

Ichangetheclientthatusesthatfunctiontousetheenrichedfieldinstead.

client3…

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

constbasicChargeAmount=aReading.baseCharge;

OnceI’vemovedallcallstocalculateBaseCharge,IcannestitinsideenrichReading.Thatwouldmakeitclearthatclientsthatneedthecalculatedbasechargeshouldusetheenrichedrecord.

Onetraptobewareofhere.WhenIwriteenrichReadinglikethis,toreturntheenrichedreading,I’mimplyingthattheoriginalreadingrecordisn’tchanged.So

it’swiseformetoaddatest.

it('checkreadingunchanged',function(){

constbaseReading={customer:"ivan",quantity:15,month:5,year:2017};

constoracle=_.cloneDeep(baseReading);

enrichReading(baseReading);

assert.deepEqual(baseReading,oracle);

});

Icanthenchangeclient1toalsousethesamefield.

client1…

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

constbaseCharge=aReading.baseCharge;

ThereisagoodchanceIcanthenuseInlineVariable(123)onbaseChargetoo.

NowIturntothetaxableamountcalculation.Myfirststepistoaddinthetransformationfunction.

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

constbase=(baseRate(aReading.month,aReading.year)*aReading.quantity);

consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));

Icanimmediatelyreplacethecalculationofthebasechargewiththenewfield.Ifthecalculationwascomplex,IcouldExtractFunction(106)first,buthereit’ssimpleenoughtodoinonestep.

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

constbase=aReading.baseCharge;

consttaxableCharge=Math.max(0,base-taxThreshold(aReading.year));

OnceI’vetestedthatthatworks,IapplyInlineVariable(123).

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

consttaxableCharge=Math.max(0,aReading.baseCharge-taxThreshold(aReading.year));

Andmovethatcomputationintothetransformer.

functionenrichReading(original){

constresult=_.cloneDeep(original);

result.baseCharge=calculateBaseCharge(result);

result.taxableCharge=Math.max(0,result.baseCharge-taxThreshold(result.year));

returnresult;

}

Imodifytheoriginalcodetousethenewfield.

constrawReading=acquireReading();

constaReading=enrichReading(rawReading);

consttaxableCharge=aReading.taxableCharge;

OnceI’vetestedthat,it’slikelyIwouldbeabletouseInlineVariable(123)ontaxableCharge.

Onebigproblemwithanenrichedreadinglikethisis:Whathappensshouldaclientchangeadatavalue?Changing,say,thequantityfieldwouldresultindatathat’sinconsistent.ToavoidthisinJavaScript,mybestoptionistouseCombineFunctionsintoClass(144)instead.IfI’minalanguagewithimmutabledatastructures,Idon’thavethisproblem,soitsmorecommontoseetransformsinthoselanguages.Buteveninlanguageswithoutimmutability,Icanusetransformsifthedataappearsinaread-onlycontext,suchasderivingdatatodisplayonawebpage.

SplitPhase

Motivation

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

Oneoftheneatestwaystodoasplitlikethisistodividethebehaviorintotwosequentialphases.Agoodexampleofthisiswhenyouhavesomeprocessingwhoseinputsdon’treflectthemodelyouneedtocarryoutthelogic.Beforeyou

begin,youcanmassagetheinputintoaconvenientformforyourmainprocessing.Or,youcantakethelogicyouneedtodoandbreakitdownintosequentialsteps,whereeachstepissignificantlydifferentinwhatitdoes.

Themostobviousexampleofthisisacompiler.It’sabasictaskistotakesometext(codeinaprogramminglanguage)andturnitintosomeexecutableform(e.g.objectcodeforaspecifichardware).Overtime,we’vefoundthiscanbeusefullysplitintoachainofphases:tokenizingthetext,parsingthetokensintoasyntaxtree,thenvariousstepsoftransformingthesyntaxtree(e.g.foroptimization),andfinallygeneratingtheobjectcode.EachstephasalimitedscopeandIcanthinkofonestepwithoutunderstandingthedetailsofothers.

Splittingphaseslikethisiscommoninlargesoftware;thevariousphasesinacompilercaneachcontainmanyfunctionsandclasses.ButIcancarryoutthebasicsplit-phaserefactoringonanyfragmentofcode—wheneverIseeanopportunitytousefullyseparatethecodeintodifferentphases.Thebestclueiswhendifferentstagesofthefragmentusedifferentsetsofdataandfunctions.ByturningthemintoseparatemodulesIcanmakethisdifferenceexplicit,revealingthedifferenceinthecode.

Mechanics

Extractthesecondphasecodeintoitsownfunction.

Test.

Introduceanintermediatedatastructureasanadditionalargumenttotheextractedfunction.

Test.

Examineeachparameteroftheextractedsecondphase.Ifitisusedbyfirstphase,moveittotheintermediatedatastructure.Testaftereachmove.

Sometimes,aparametershouldnotbeusedbythesecondphase.Inthiscase,extracttheresultsofeachusageoftheparameterintoafieldoftheintermediatedatastructureanduseMoveStatementstoCallers(215)onthelinethatpopulatesit.

ApplyExtractFunction(106)onthefirst-phasecode,returningtheintermediatedatastructure.

It’salsoreasonabletoextractthefirstphaseintoatransformerobject.

Example

I’llstartwithcodetopriceanorderforsomevagueandunimportantkindofgoods:

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constshippingPerCase=(basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=quantity*shippingPerCase;

constprice=basePrice-discount+shippingCost;

returnprice;

}

Althoughthisistheusualkindoftrivialexample,thereisasenseoftwophasesgoingonhere.Thefirstcoupleoflinesofcodeusetheproductinformationtocalculatetheproduct-orientedpriceoftheorder,whilethelatercodeusesshippinginformationtodeterminetheshippingcost.IfIhavechangescomingupthatcomplicatethepricingandshippingcalculations,buttheyworkrelativelyindependently,thensplittingthiscodeintotwophasesisvaluable.

IbeginbyapplyingExtractFunction(106)totheshippingcalculation.

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constprice=applyShipping(basePrice,shippingMethod,quantity,discount);

returnprice;

}

functionapplyShipping(basePrice,shippingMethod,quantity,discount){

constshippingPerCase=(basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=quantity*shippingPerCase;

constprice=basePrice-discount+shippingCost;

returnprice;

}

Ipassinallthedatathatthissecondphaseneedsasindividualparameters.Inamorerealisticcase,therecanbealotofthese,butIdon’tworryaboutitasI’llwhittlethemdownlater.

Next,Iintroducetheintermediatedatastructurethatwillcommunicatebetweenthetwophases.

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constpriceData={};

constprice=applyShipping(priceData,basePrice,shippingMethod,quantity,discount);

returnprice;

}

functionapplyShipping(priceData,basePrice,shippingMethod,quantity,discount){

constshippingPerCase=(basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=quantity*shippingPerCase;

constprice=basePrice-discount+shippingCost;

returnprice;

}

Now,IlookatthevariousparameterstoapplyShipping.ThefirstoneisbasePricewhichiscreatedbythefirst-phasecode.SoImovethisintotheintermediatedatastructure,removingitfromtheparameterlist.

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constpriceData={basePrice:basePrice};

constprice=applyShipping(priceData,basePrice,shippingMethod,quantity,discount);

returnprice;

}

functionapplyShipping(priceData,basePrice,shippingMethod,quantity,discount){

constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=quantity*shippingPerCase;

constprice=priceData.basePrice-discount+shippingCost;

returnprice;

}

ThenextparameterinthelistisshippingMethod.ThisoneIleaveasis,sinceitisn’tusedbythefirst-phasecode.

Afterthis,Ihavequantity.Thisisusedbythefirstphasebutnotcreatedbyit,soIcouldactuallyleavethisintheparameterlist.Myusualpreference,however,istomoveasmuchasIcantotheintermediatedatastructure.

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constpriceData={basePrice:basePrice,quantity:quantity};

constprice=applyShipping(priceData,shippingMethod,quantity,discount);

returnprice;

}

functionapplyShipping(priceData,shippingMethod,quantity,discount){

constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=priceData.quantity*shippingPerCase;

constprice=priceData.basePrice-discount+shippingCost;

returnprice;

}

Idothesamewithdiscount.

functionpriceOrder(product,quantity,shippingMethod){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

constpriceData={basePrice:basePrice,quantity:quantity,discount:discount};

constprice=applyShipping(priceData,shippingMethod,discount);

returnprice;

}

functionapplyShipping(priceData,shippingMethod,discount){

constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=priceData.quantity*shippingPerCase;

constprice=priceData.basePrice-priceData.discount+shippingCost;

returnprice;

}

OnceI’vegonethroughallthefunctionparameters,Ihavetheintermediatedatastructurefullyformed.SoIcanextractthefirst-phasecodeintoitsownfunction,returningthisdata.

functionpriceOrder(product,quantity,shippingMethod){

constpriceData=calculatePricingData(product,quantity);

constprice=applyShipping(priceData,shippingMethod);

returnprice;

}

functioncalculatePricingData(product,quantity){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

return{basePrice:basePrice,quantity:quantity,discount:discount};

}

functionapplyShipping(priceData,shippingMethod){

constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=priceData.quantity*shippingPerCase;

constprice=priceData.basePrice-priceData.discount+shippingCost;

returnprice;

}

Ican’tresisttidyingoutthosefinalconstants.

functionpriceOrder(product,quantity,shippingMethod){

constpriceData=calculatePricingData(product,quantity);

returnapplyShipping(priceData,shippingMethod);

}

functioncalculatePricingData(product,quantity){

constbasePrice=product.basePrice*quantity;

constdiscount=Math.max(quantity-product.discountThreshold,0)

*product.basePrice*product.discountRate;

return{basePrice:basePrice,quantity:quantity,discount:discount};

}

functionapplyShipping(priceData,shippingMethod){

constshippingPerCase=(priceData.basePrice>shippingMethod.discountThreshold)

?shippingMethod.discountedFee:shippingMethod.feePerCase;

constshippingCost=priceData.quantity*shippingPerCase;

returnpriceData.basePrice-priceData.discount+shippingCost;

}

Chapter7EncapsulationPerhapsthemostimportantcriteriatobeusedindecomposingmodulesistoidentifysecretsthatmodulesshouldhidefromtherestofthesystem[bib-parnas].Datastructuresarethemostcommonsecrets,andIcanhidedatastructuresbyencapsulatingthemwithEncapsulateRecord(160)andEncapsulateCollection(168).EvenprimitivedatavaluescanbeencapsulatedwithReplacePrimitivewithObject(172)—themagnitudeofsecond-orderbenefitsfromdoingthisoftensurprisespeople.Temporaryvariablesoftengetinthewayofrefactoring—Ihavetoensuretheyarecalcuatedintherightorderandtheirvaluesareavailabletootherpartsofthecodethatneedthem.UsingReplaceTempwithQuery(176)isagreathelphere,particularlywhensplittingupanoverlylongfunction.

Classesweredesignedforinformationhiding.Inthepreviouschapter,IdescribedawaytoformthemwithCombineFunctionsintoClass(144).Thecommonextract/inlineoperationsalsoapplytoclasseswithExtractClass(180)andInlineClass(184).

Aswellashidingtheinternalsofclasses,it’softenusefultohideconnectionsbetweenclasses,whichIcandowithHideDelegate(187).Buttoomuchhidingleadstobloatedinterfaces,soIalsoneeditsreverse:RemoveMiddleMan(190).

Classesandmodulesarethelargestformsofencapsulation,butfunctionsalsoencapsulatetheirimplementation.Sometimes,Imayneedtomakeawholesalechangetoanalgorithm,whichIcandobywrappingitinafunctionwithExtractFunction(106)andapplyingSubstituteAlgorithm(193).

EncapsulateRecord

formerly:ReplaceRecordwithDataClass

Motivation

Recordstructuresareacommonfeatureinprogramminglanguages.Theyprovideanintuitivewaytogrouprelateddatatogether,allowingmetopassmeaningfulunitsofdataratherthanlooseclumps.Butsimplerecordstructureshavedisadvantages.Themostannoyingoneisthattheyforcemetoclearlyseparatewhatisstoredintherecordfromcalculatedvalues.Considerthenotionofaninclusiverangeofintegers.Icanstorethisas{start:1,end:5}or{start:1,length:5}(oreven{end:5,length:5},ifIwanttoflauntmycontrariness).Butwhatevermystore,Iwanttoknowwhatthestart,end,andlengthare.

ThisiswhyIoftenfavorobjectsoverrecordsformutabledata.Withobjects,Icanhidewhatisstoredandprovidemethodsforallthreevalues.Theuseroftheobjectdoesn’tneedtoknoworcarewhichisstoredandwhichiscalculated.This

encapsulationalsohelpswithrenaming:Icanrenamethefieldwhileprovidingmethodsforboththenewandtheoldnames,graduallyupdatingcallersuntiltheyarealldone.

IjustsaidIfavorobjectsformutabledata.IfIhaveanimmutablevalue,Icanjusthaveallthreevaluesinmyrecord,usinganenrichmentstepifnecessary.Similarly,it’seasytocopythefieldwhenrenaming.

Icanhavetwokindsofrecordstructures:thosewhereIdeclarethelegalfieldnamesandthosethatallowmetousewhateverIlike.Thelatterareoftenimplementedthroughalibraryclasscalledsomethinglikehash,map,hashmap,dictionary,orassociativearray.Manylanguagesprovideconvenientsyntaxforcreatinghashmaps,whichmakesthemusefulinmanyprogrammingsituations.Thedownsideofusingthemistheyarearen’texplicitabouttheirfields.TheonlywayIcantelliftheyusestart/endorstart/lengthisbylookingatwheretheyarecreatedandused.Thisisn’taproblemiftheyareonlyusedinasmallsectionofaprogram,butthewidertheirscopeofusage,thegreaterproblemIgetfromtheirimplicitstructure.Icouldrefactorsuchimplicitrecordsintoexplicitones—butifIneedtodothat,I’drathermakethemclassesinstead.

It’scommontopassnestedstructuresoflistsandhashmapswhichareoftenserializedintoformatslikeJSONorXML.Suchstructurescanbeencapsulatedtoo,whichhelpsiftheirformatschangelateronorifI’mconcernedaboutupdatestothedatathatarehardtokeeptrackof.

Mechanics

UseEncapsulateVariable(132)onthevariableholdingtherecord.

Givethefunctionsthatencapsulatetherecordnamesthatareeasilysearchable.

Replacethecontentofthevariablewithasimpleclassthatwrapstherecord.Defineanaccessorinsidethisclassthatreturnstherawrecord.Modifythefunctionsthatencapsulatethevariabletousethisaccessor.

Test.

Providenewfunctionsthatreturntheobjectratherthantherawrecord.

Foreachuseroftherecord,replaceitsuseofafunctionthatreturnstherecordwithafunctionthatreturnstheobject.Useanaccessorontheobjecttogetatthefielddata,creatingthataccessorifneeded.Testaftereachchange.

Ifit’sacomplexrecord,suchasonewithanestedstructure,focusonclientsthatupdatethedatafirst.Considerreturningacopyorread-onlyproxyofthedataforclientsthatonlyreadthedata.

Removetheclass’srawdataaccessorandtheeasilysearchablefunctionsthatreturnedtherawrecord.

Test.

Ifthefieldsoftherecordarethemselvesstructures,considerusingEncapsulateRecordandEncapsulateCollection(168)recursively.

Example

I’llstartwithaconstantthatiswidelyusedacrossaprogram.

constorganization={name:"AcmeGooseberries",country:"GB"};

ThisisaJavaScriptobjectwhichisbeingusedasarecordstructurebyvariouspartsoftheprogram,withaccesseslikethis:

result+=`<h1>${organization.name}</h1>`;

and

organization.name=newName;

ThefirststepisasimpleEncapsulateVariable(132)

functiongetRawDataOfOrganization(){returnorganization;}

examplereader…

result+=`<h1>${getRawDataOfOrganization().name}</h1>`;

examplewriter…

getRawDataOfOrganization().name=newName;

It’snotquiteastandardEncapsulateVariable(132),sinceIgavethegetteranamedeliberatelychosentobebothuglyandeasytosearchfor.ThisisbecauseIintenditslifetobeshort.

Encapsulatingarecordmeansgoingdeeperthanjustthevariableitself;Iwanttocontrolhowit’smanipulated.Icandothisbyreplacingtherecordwithaclass.

classOrganization…

classOrganization{

constructor(data){

this._data=data;

}

}

toplevel

constorganization=newOrganization({name:"AcmeGooseberries",country:"GB"});

functiongetRawDataOfOrganization(){returnorganization._data;}

functiongetOrganization(){returnorganization;}

NowthatIhaveanobjectinplace,Istartlookingattheusersoftherecord.Anyonethatupdatestherecordgetsreplacedwithasetter.

classOrganization…

setname(aString){this._data.name=aString;}

client…

getOrganization().name=newName;

Similarly,Ireplaceanyreaderswiththeappropriategetter.

classOrganization…

getname(){returnthis._data.name;}

client…

result+=`<h1>${getOrganization().name}</h1>`;

AfterI’vedonethat,Icanfollowthroughonmythreattogivetheugly-sounding

functionashortlife.

functiongetRawDataOfOrganization(){returnorganization._data;}

functiongetOrganization(){returnorganization;}

I’dalsobeinclinedtofoldthe_datafielddirectlyintotheobject.

classOrganization{

constructor(data){

this._name=data.name;

this._country=data.country;

}

getname(){returnthis._name;}

setname(aString){this._name=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

Thishastheadvantageofbreakingthelinktotheinputdatarecord.Thismightbeusefulifareferencetoitrunsaround,whichwouldbreakencapsulation.ShouldInotfoldthedataintoindividualfields,Iwouldbewisetocopy_data

whenIassignit.

Example:EncapsulatingaNestedRecord

Theaboveexamplelooksatashallowrecord,butwhatdoIdowithdatathatisdeeplynested,e.g.comingfromaJSONdocument?Thecorerefactoringstepsstillapply,andIhavetobeequallycarefulwithupdates,butIdogetsomeoptionsaroundreads.

Asanexample,hereissomeslightlymorenesteddata:acollectionofcustomers,keptinahashmapindexedbytheircustomerID.

"1920":{

name:"martin",

id:"1920",

usages:{

"2016":{

"1":50,

"2":55,

//remainingmonthsoftheyear

},

"2015":{

"1":70,

"2":63,

//remainingmonthsoftheyear

}

}

},

"38673":{

name:"neal",

id:"38673",

//morecustomersinasimilarform

Withmorenesteddata,readsandwritescanbediggingintothedatastructure.

sampleupdate…

customerData[customerID].usages[year][month]=amount;

sampleread…

functioncompareUsage(customerID,laterYear,month){

constlater=customerData[customerID].usages[laterYear][month];

constearlier=customerData[customerID].usages[laterYear-1][month];

return{laterAmount:later,change:later-earlier};

}

Toencapsulatethisdata,IalsostartwithEncapsulateVariable(132).

functiongetRawDataOfCustomers(){returncustomerData;}

functionsetRawDataOfCustomers(arg){customerData=arg;}

sampleupdate…

getRawDataOfCustomers()[customerID].usages[year][month]=amount;

sampleread…

functioncompareUsage(customerID,laterYear,month){

constlater=getRawDataOfCustomers()[customerID].usages[laterYear][month];

constearlier=getRawDataOfCustomers()[customerID].usages[laterYear-1][month];

return{laterAmount:later,change:later-earlier};

}

Ithenmakeaclassfortheoveralldatastructure.

classCustomerData{

constructor(data){

this._data=data;

}

}

toplevel…

functiongetCustomerData(){returncustomerData;}

functiongetRawDataOfCustomers(){returncustomerData._data;}

functionsetRawDataOfCustomers(arg){customerData=newCustomerData

Themostimportantareatodealwithistheupdates.So,whileIlookatallthecallersofgetRawDataOfCustomers,I’mfocusedonthosewherethedataischanged.Toremindyou,here’stheupdateagain.

sampleupdate…

getRawDataOfCustomers()[customerID].usages[year][month]=amount;

Thegeneralmechanicsnowsaytoreturnthefullcustomeranduseanaccessor,creatingoneifneeded.Idon’thaveasetteronthecustomerforthisupdate,andthisonedigsintothestructure.So,tomakeone,IbeginbyusingExtractFunction(106)onthecodethatdigsintothedatastructure.

sampleupdate…

setUsage(customerID,year,month,amount);

toplevel…

functionsetUsage(customerID,year,month,amount){

getRawDataOfCustomers()[customerID].usages[year][month]=amount;

}

IthenuseMoveFunction(196)tomoveitintothenewcustomerdataclass.

sampleupdate…

getCustomerData().setUsage(customerID,year,month,amount);

classCustomerData…

setUsage(customerID,year,month,amount){

this._data[customerID].usages[year][month]=amount;

}

Whenworkingwithabigdatastructure,Iliketoconcentrateontheupdates.Gettingthemvisibleandgatheredinasingleplaceisthemostimportantpartoftheencapsulation.

Atsomepoint,IwillthinkI’vegotthemall—buthowcanIbesure?There’sacoupleofwaystocheck.OneistomodifygetRawDataOfCustomerstoreturnadeepcopyofthedata;ifmytestcoverageisgood,oneofthetestsshouldbreakifImissedamodification.

toplevel…

functiongetCustomerData(){returncustomerData;}

functiongetRawDataOfCustomers(){returncustomerData.rawData;}

functionsetRawDataOfCustomers(arg){customerData=newCustomerData(arg);}

classCustomerData…

getrawData(){

return_.cloneDeep(this._data);

}

I’musingthelodashlibrarytomakeadeepcopy.

Anotherapproachistoreturnaread-onlyproxyforthedatastructure.Suchaproxycouldraiseanexceptioniftheclientcodetriestomodifytheunderlyingobject.Somelanguagesmakethiseasy,butit’sapaininJavaScript,soI’llleaveitasanexerciseforthereader.Icouldalsotakeacopyandrecursivelyfreezeittodetectanymodifications.

Dealingwiththeupdatesisvaluable,butwhataboutthereaders?Herethereareafewoptions.

ThefirstoptionistodothesamethingasIdidforthesetters.Extractallthereadsintotheirownfunctionsandmovethemintothecustomerdataclass.

classCustomerData…

usage(customerID,year,month){

returnthis._data[customerID].usages[year][month];

}

toplevel…

functioncompareUsage(customerID,laterYear,month){

constlater=getCustomerData().usage(customerID,laterYear,month)

constearlier=getCustomerData().usage(customerID,laterYear-1,month)

return{laterAmount:later,change:later-earlier};

}

ThegreatthingaboutthisapproachisthatitgivescustomerDataanexplicitAPIthatcapturesalltheusesmadeofit.Icanlookattheclassandseealltheirusesofthedata.Butthiscanbealotofcodeforlotsofspecialcases.Modernlanguagesprovidegoodaffordancesfordiggingintoalist-and-hash[bib-list-and-hash]datastructure,soit’susefultogiveclientsjustsuchadatastructuretoworkwith.

Iftheclientwantsadatastructure,Icanjusthandouttheactualdata.Buttheproblemwiththisisthatthere’snowaytopreventclientsfrommodifyingthedatadirectly,whichbreaksthewholepointofencapsulatingalltheupdatesinsidefunctions.Consequently,thesimplestthingtodoistoprovideacopyoftheunderlyingdata,usingtherawDatamethodIwroteearlier.

classCustomerData…

getrawData(){

return_.cloneDeep(this._data);

}

toplevel…

functioncompareUsage(customerID,laterYear,month){

constlater=getCustomerData().rawData[customerID].usages[laterYear][month];

constearlier=getCustomerData().rawData[customerID].usages[laterYear-1][month];

return{laterAmount:later,change:later-earlier};

}

Butalthoughit’ssimple,therearedownsides.Themostobviousproblemisthecostofcopyingalargedatastructure,whichmayturnouttobeaperformanceproblem.Aswithanythinglikethis,however,theperformancecostmightbeacceptable—IwouldwanttomeasureitsimpactbeforeIstarttoworryaboutit.Theremayalsobeconfusionifclientsexpectmodifyingthecopieddatatomodifytheoriginal.Inthosecases,aread-onlyproxyorfreezingthecopieddatamightprovideahelpfulerrorshouldtheydothis.

Anotheroptionismorework,butoffersthemostcontrol:ApplyEncapsulateRecordrecursively.Withthis,Iturnthecustomerrecordintoitsownclass,

applyEncapsulateCollection(168)totheusages,andcreateausageclass.Icanthenenforcecontrolofupdatesbyusingaccessors,perhapsapplyingChangeReferencetoValue(252)ontheusageobjects.Butthiscanbealotofeffortforalargedatastructure—andnotreallyneededifIdon’taccessthatmuchofthedatastructure.Sometimes,ajudiciousmixofgettersandnewclassesmaywork,usingagettertodigdeepintothestructurebutreturninganobjectthatwrapsthestructureratherthantheunencapsulateddata.Iwroteaboutthiskindofthingoninanarticle:RefactoringCodetoLoadaDocument[bib-refact-doc].

EncapsulateCollection

Motivation

Ilikeencapsulatinganymutabledatainmyprograms.Thismakesiteasiertoseewhenandhowdatastructuresaremodified,whichthenmakesiteasiertochangethosedatastructureswhenIneedto.Encapsulationisoftenencouraged,particularlybyobject-orienteddevelopers,butacommonmistakeoccurswhenworkingwithcollections.Accesstoacollectionvariablemaybeencapsulated,butifthegetterreturnsthecollectionitself,thenthatcollection’smembershipcanbealteredwithouttheenclosingclassbeingabletointervene.

Toavoidthis,Iprovidecollectionmodifiermethods—usuallyaddandremove—ontheclassitself.Thisway,changestothecollectiongothroughtheowningclass,givingmetheopportunitytomodifysuchchangesastheprogramevolves.

Ifftheteamhasthehabittonottomodifycollectionsoutsidetheoriginalmodule,justprovidingthesemethodsmaybeenough.However,it’susuallyunwisetorelyonsuchhabits;amistakeherecanleadtobugsthataredifficulttotrackdownlater.Abetterapproachistoensurethatthegetterforthecollectiondoesnotreturntherawcollection,sothatclientscannotaccidentallychangeit.

Onewaytopreventmodificationoftheunderlyingcollectionisbyneverreturningacollectionvalue.Inthisapproach,anyuseofacollectionfieldisdonewithspecificmethodsontheowningclass,replacingaCustomer.orders.sizewithaCustomer.numberOfOrders.Idon’tagreewiththisapproach.Modernlanguageshaverichcollectionclasseswithstandardizedinterfaces,whichcanbecombinedinusefulwayssuchasCollectionPipelines(https://martinfowler.com/bliki/Collection-Pipeline.html).Puttinginspecialmethodstohandlethiskindoffunctionalityaddsalotofextracodeandcripplestheeasycomposabilityofcollectionoperations.

Anotherwayistoallowsomeformofread-onlyaccesstoacollection.Java,forexample,makesiteasytoreturnaread-onlyproxytothecollection.Suchaproxyforwardsallreadstotheunderlyingcollection,butblocksallwrites—inJava’scase,throwinganexception.Asimilarrouteisusedbylibrariesthatbasetheircollectioncompositiononsomekindofiteratororenumerableobject—providingthatiteratorcannotmodifytheunderlyingcollection.

Probablythemostcommonapproachistoprovideagettingmethodforthecollection,butmakeitreturnacopyoftheunderlyingcollection.Thatway,anymodificationstothecopydon’taffecttheencapsulatedcollection.Thismightcausesomeconfusionifprogrammersexpectthereturnedcollectiontomodifythesourcefield—butinmanycodebases,programmersareusedtocollectiongettersprovidingcopies.Ifthecollectionishuge,thismaybeaperformanceissue—butmostlistsaren’tallthatbig,sothegeneralrulesforperformanceshouldapply.(RefactoringandPerformance,p.62)

Anotherdifferencebetweenusingaproxyandacopyisthatamodificationofthesourcedatawillbevisibleintheproxybutnotinacopy.Thisisn’tanissuemostofthetime,becauselistsaccessedinthiswayareusuallyonlyheldfora

shorttime.

What’simportanthereisconsistencywithinacodebase.Useonlyonemechanismsoeveryonecangetusedtohowitbehavesandexpectitwhencallinganycollectionaccessorfunction.

Mechanics

ApplyEncapsulateVariable(132)ifthereferencetothecollectionisn’talreadyencapsulated.

Addfunctionstoaddandremoveelementsfromthecollection.

Ifthereisasetterforthecollection,useRemoveSettingMethod(329)ifpossible.Ifnot,makeittakeacopyoftheprovidedcollection.

Runstaticchecks.

Findallreferencestothecollection.Ifanyonecallsmodifiersonthecollection,changethemtousethenewadd/removefunctions.Testaftereachchange.

Modifythegetterforthecollectiontoreturnaprotectedviewonit,usingaread-onlyproxyoracopy.

Test.

Example

Istartwithapersonclassthathasafieldforalistofcourses.

classPerson…

constructor(name){

this._name=name;

this._courses=[];

}

getname(){returnthis._name;}

getcourses(){returnthis._courses;}

setcourses(aList){this._courses=aList;}

classCourse…

constructor(name,isAdvanced){

this._name=name;

this._isAdvanced=isAdvanced;

}

getname(){returnthis._name;}

getisAdvanced(){returnthis._isAdvanced;}

Clientsusethecoursecollectiontogatherinformationoncourses.

numAdvancedCourses=aPerson.courses

.filter(c=>c.isAdvanced)

.length

;

Anaivedeveloperwouldsaythisclasshasproperdataencapsulation:Afterall,eachfieldisprotectedbyaccessormethods.ButIwouldarguethatthelistofcoursesisn’tproperlyencapsulated.Certainly,anyoneupdatingthecoursesasasinglevaluehaspropercontrolthroughthesetter:

clientcode…

constbasicCourseNames=readBasicCourseNames(filename);

aPerson.courses=basicCourseNames.map(name=>newCourse(name,false));

Butclientsmightfinditeasiertoupdatethecourselistdirectly.

clientcode…

for(constnameofreadBasicCourseNames(filename)){

aPerson.courses.push(newCourse(name,false));

}

Thisviolatesencapsulatingbecausethepersonclasshasnoabilitytotakecontrolwhenthelistisupdatedinthisway.Whilethereferencetothefieldisencapsulated,thecontentofthefieldisnot.

I’llbegincreatingproperencapsulationbyaddingmethodstothepersonclassthatallowaclienttoaddandremoveindividualcourses.

classPerson…

addCourse(aCourse){

this._courses.push(aCourse);

}

removeCourse(aCourse,fnIfAbsent=()=>{thrownewRangeError();}){

constindex=this._courses.indexOf(aCourse);

if(index===-1)fnIfAbsent();

elsethis._courses.splice(index,1);

}

Witharemoval,Ihavetodecidewhattodoifaclientaskstoremoveanelementthatisn’tinthecollection.Icaneithershrug,orraiseanerror.Withthiscode,Idefaulttoraisinganerror,butgivethecallersanopportunitytodosomethingelseiftheywish.

Ithenchangeanycodethatcallsmodifiersdirectlyonthecollectiontousenewmethods.

clientcode…

for(constnameofreadBasicCourseNames(filename)){

aPerson.addCourse(newCourse(name,false));

}

Withindividualaddandremovemethods,thereisusuallynoneedforsetCourses,inwhichcaseI’lluseRemoveSettingMethod(329)onit.ShouldtheAPIneedasettingmethodforsomereason,Iensureitputsacopyofthecollectioninthefield.

classPerson…

setcourses(aList){this._courses=aList.slice();}

Allthisenablestheclientstousetherightkindofmodifiermethods,butIprefertoensurenobodymodifiesthelistwithoutusingthem.Icandothisbyprovidingacopy.

classPerson…

getcourses(){returnthis._courses.slice();}

Ingeneral,IfinditwisetobemoderatelyparanoidaboutcollectionsandI’drathercopythemunnecessarilythandebugerrorsduetounexpectedmodifications.Modificationsaren’talwaysobvious;forexample,sortinganarrayinJavaScriptmodifiestheoriginal,whilemanylanguagesdefaulttomakingacopyforanoperationthatchangesacollection.Anyclassthat’s

responsibleformanagingacollectionshouldalwaysgiveoutcopies—butIalsogetintothehabitofmakingacopyifIdosomethingthat’sliabletochangeacollection.

ReplacePrimitivewithObject

formerly:ReplaceDataValuewithObject

formerly:ReplaceTypeCodewithClass

Motivation

Often,inearlystagesofdevelopmentyoumakedecisionsaboutrepresentingsimplefactsassimpledataitems,suchasnumbersorstrings.Asdevelopmentproceeds,thosesimpleitemsaren’tsosimpleanymore.Atelephonenumbermayberepresentedasastringforawhile,butlateritwillneedspecialbehaviorforformatting,extractingtheareacode,andthelike.Thiskindoflogiccanquicklyendupbeingduplicatedaroundthecodebase,increasingtheeffortwheneveritneedstobeused.

AssoonasIrealizeIwanttodosomethingotherthansimpleprinting,Iliketocreateanewclassforthatbitofdata.Atfirst,suchaclassdoeslittlemorethanwraptheprimitive—butonceIhavethatclass,Ihaveaplacetoputbehavior

specifictoitsneeds.Theselittlevaluesstartveryhumble,butoncenurturedtheycangrowintousefultools.Theymaynotlooklikemuch,butIfindtheireffectsonacodebasecanbesurprisinglylarge.Indeedmanyexperienceddevelopersconsiderthistobeoneofthemostvaluablerefactoringsinthetoolkit—eventhoughitoftenseemscounter-intuitivetoanewprogrammer.

Mechanics

ApplyEncapsulateVariable(132)ifitisn’talready.

Createasimplevalueclassforthedatavalue.Itshouldtaketheexistingvalueinitsconstructorandprovideagetterforthatvalue.

Runstaticchecks.

Changethesettertocreateanewinstanceofthevalueclassandstorethatinthefield,changingthetypeofthefieldifpresent.

Changethegettertoreturntheresultofinvokingthegetterofthenewclass.

Test.

ConsiderusingChangeFunctionDeclaration(124)ontheoriginalaccessorstobetterreflectwhattheydo.

ConsiderclarifyingtheroleofthenewobjectasavalueorreferenceobjectbyapplyingChangeReferencetoValue(252)orChangeValuetoReference(256).

Example

Ibeginwithasimpleorderclassthatreadsitsdatafromasimplerecordstructure.Oneofitspropertiesisapriority,whichitreadsasasimplestring.

classOrder…

constructor(data){

this.priority=data.priority;

//moreinitialization

Someclientcodesusesitlikethis:

client…

highPriorityCount=orders.filter(o=>"high"===o.priority

||"rush"===o.priority)

.length;

WheneverI’mfiddlingwithadatavalue,thefirstthingIdoisuseEncapsulateVariable(132)onit.

classOrder…

getpriority(){returnthis._priority;}

setpriority(aString){this._priority=aString;}

TheconstructorlinethatinitializestheprioritywillnowusethesetterIdefinehere.

Thisself-encapsulatesthefieldsoIcanpreserveitscurrentusewhileImanipulatethedataitself.

Icreateasimplevalueclassforthepriority.Ithasaconstructorforthevalueandaconversionfunctiontoreturnastring.

classPriority{

constructor(value){this._value=value;}

toString(){returnthis._value;}

}

Ipreferusingaconversionfunction(toString)ratherthanagetter(value)here.Forclientsoftheclass,askingforthestringrepresentationshouldfeelmorelikeaconversionthangettingaproperty.

Ithenmodifytheaccessorstousethisnewclass.

classOrder…

getpriority(){returnthis._priority.toString();}

setpriority(aString){this._priority=newPriority(aString);}

NowthatIhaveapriorityclass,Ifindthecurrentgetterontheordertobemisleading.Itdoesn’treturnthepriority—butastringthatdescribesthepriority.MyimmediatemoveistouseChangeFunctionDeclaration(124).

classOrder…

getpriorityString(){returnthis._priority.toString();}

setpriority(aString){this._priority=newPriority(aString);}

client…

highPriorityCount=orders.filter(o=>"high"===o.priorityString

||"rush"===o.priorityString)

.length;

Inthiscase,I’mhappytoretainthenameofthesetter.Thenameoftheargumentcommunicateswhatitexpects.

NowI’mdonewiththeformalrefactoring.ButasIlookatwhousesthepriority,Iconsiderwhethertheyshouldusethepriorityclassthemselves.Asaresult,Iprovideagetteronorderthatprovidesthenewpriorityobjectdirectly.

classOrder…

getpriority(){returnthis._priority;}

getpriorityString(){returnthis._priority.toString();}

setpriority(aString){this._priority=newPriority(aString);}

client…

highPriorityCount=orders.filter(o=>"high"===o.priority.toString()

||"rush"===o.priority.toString())

.length;

Asthepriorityclassbecomesusefulelsewhere,Iwouldallowclientsoftheordertousethesetterwithapriorityinstance,whichIdobyadjustingthepriorityconstructor.

classPriority…

constructor(value){

if(valueinstanceofPriority)returnvalue;

this._value=value;

}

Thepointofallthisisthatnow,mynewpriorityclasscanbeusefulasaplacefornewbehavior—eithernewtothecodeormovedfromelsewhere.Here’ssomesimplecodetoaddvalidationofpriorityvaluesandcomparisonlogic.

classPriority…

constructor(value){

if(valueinstanceofPriority)returnvalue;

if(Priority.legalValues().includes(value))

this._value=value;

else

thrownewError(`<${value}>isinvalidforPriority`);

}

toString(){returnthis._value;}

get_index(){returnPriority.legalValues().findIndex(s=>s===this._value);}

staticlegalValues(){return['low','normal','high','rush'];}

equals(other){returnthis._index===other._index;}

higherThan(other){returnthis._index>other._index;}

lowerThan(other){returnthis._index<other._index;}

AsIdothis,Idecidethatapriorityshouldbeavalueobject,soIprovideanequalsmethodandensurethatitisimmutable.

NowI’veaddedthatbehavior,Icanmaketheclientcodemoremeaningful:

client…

highPriorityCount=orders.filter(o=>o.priority.higherThan(newPriority("normal"))

.length;

ReplaceTempwithQuery

Motivation

Oneuseoftemporaryvariablesistocapturethevalueofsomecodeinordertorefertoitlaterinafunction.Usingatempallowsmetorefertothevaluewhileexplainingitsmeaningandavoidingrepeatingthecodethatcalculatesit.Butwhileusingavariableishandy,itcanoftenbeworthwhiletogoastepfurtheranduseafunctioninstead.

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

Usingfunctionsinsteadofvariablesalsoallowsmetoavoidduplicatingthecalculationlogicinsimilarfunctions.WheneverIseevariablescalculatedinthesamewayindifferentplaces,Ilooktoturnthemintoasinglefunction.

ThisrefactoringworksbestifI’minsideaclass,sincetheclassprovidesasharedcontextforthemethodsI’mextracting.Outsideofaclass,I’mliabletohavetoomanyparametersinatop-levelfunctionwhichnegatesmuchofthebenefitofusingafunction.Nestedfunctioncanavoidthis,buttheylimitmyabilitytosharethelogicbetweenrelatedfunctions.

OnlysometemporaryvariablesaresuitableforReplaceTempwithQuery.Thevariableneedstobecalculatedonceandthenonlybereadafterwards.Inthesimplestcase,thismeansthevariableisassignedtoonce,butit’salsopossibletohaveseveralassignmentsinamorecomplicatedlumpofcode—allofwhichhastobeextractedintothequery.Furthermore,thelogicusedtocalculatethevariablemustyieldthesameresultwhenthevariableisusedlater—whichrulesoutvariablesusedassnapshotswithnameslikeoldAddress.

Mechanics

Checkthatthevariableisdeterminedentirelybeforeit’sused,andthecodethatcalculatesitdoesnotyieldadifferentvaluewheneveritisused.

Ifthevariableisn’tread-only,andcanbemaderead-only,doso.

Test.

Extracttheassignmentofthevariableintoafunction.

Ifthevariableandthefunctioncannotshareaname,useatemporarynameforthefunction.

Ensuretheextractedfunctionisfreeofsideeffects.Ifnot,useSeparateQueryfromModifier(304).

Test.

UseInlineVariable(123)toremovethetemp.

Example

Hereisasimpleclass.

classOrder…

constructor(quantity,item){

this._quantity=quantity;

this._item=item;

}

getprice(){

varbasePrice=this._quantity*this._item.price;

vardiscountFactor=0.98;

if(basePrice>1000)discountFactor-=0.03;

returnbasePrice*discountFactor;

}

}

IwanttoreplacethetempsbasePriceanddiscountFactorwithmethods.

StartingwithbasePrice,Imakeitconstandruntests.ThisisagoodwayofcheckingthatIhaven’tmissedareassignment—unlikelyinsuchashortfunctionbutcommonwhenI’mdealingwithsomethinglarger.

classOrder…

constructor(quantity,item){

this._quantity=quantity;

this._item=item;

}

getprice(){

constbasePrice=this._quantity*this._item.price;

vardiscountFactor=0.98;

if(basePrice>1000)discountFactor-=0.03;

returnbasePrice*discountFactor;

}

}

Ithenextracttheright-hand-sideoftheassignmenttoagettingmethod.

classOrder…

getprice(){

constbasePrice=this.basePrice;

vardiscountFactor=0.98;

if(basePrice>1000)discountFactor-=0.03;

returnbasePrice*discountFactor;

}

getbasePrice(){

returnthis._quantity*this._item.price;

}

Itest,andapplyInlineVariable(123)

classOrder…

getprice(){

constbasePrice=this.basePrice;

vardiscountFactor=0.98;

if(this.basePrice>1000)discountFactor-=0.03;

returnthis.basePrice*discountFactor;

}

IthenrepeatthestepswithdiscountFactor,firstusingExtractFunction(106).

classOrder…

getprice(){

constdiscountFactor=this.discountFactor;

returnthis.basePrice*discountFactor;

}

getdiscountFactor(){

vardiscountFactor=0.98;

if(this.basePrice>1000)discountFactor-=0.03;

returndiscountFactor;

}

InthiscaseIneedmyextractedfunctiontocontainbothassignmentstodiscountFactor.Icanalsosettheoriginalvariabletobeconst.

Then,Iinline:

getprice(){

returnthis.basePrice*this.discountFactor;

}

ExtractClass

Motivation

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

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

Onesignthatoftencropsuplaterindevelopmentisthewaytheclassissub-typed.Youmayfindthatsubtypingaffectsonlyafewfeaturesorthatsomefeaturesneedtobesubtypedonewayandotherfeaturesadifferentway.

Mechanics

Decidehowtosplittheresponsibilitiesoftheclass.

Createanewchildclasstoexpressthesplit-offresponsibilities.

Iftheresponsibilitiesoftheoriginalparentclassnolongermatchitsname,renametheparent.

Createaninstanceofthechildclasswhenconstructingtheparentandaddalinkfromparenttochild.

UseMoveField(205)oneachfieldyouwishtomove.Testaftereachmove.

UseMoveFunction(196)tomovemethodstothenewchild.Startwithlower-levelmethods(thosebeingcalledratherthancalling).Testaftereachmove.

Reviewtheinterfacesofbothclasses,removeunneededmethods,changenamestobetterfitthenewcircumstances.

Decidewhethertoexposethenewchild.Ifso,considerapplyingChangeReferencetoValue(252)tothechildclass.

Example

Istartwithasimplepersonclass:

classPerson…

getname(){returnthis._name;}

setname(arg){this._name=arg;}

gettelephoneNumber(){return`(${this.officeAreaCode})${this.officeNumber}`;}

getofficeAreaCode(){returnthis._officeAreaCode;}

setofficeAreaCode(arg){this._officeAreaCode=arg;}

getofficeNumber(){returnthis._officeNumber;}

setofficeNumber(arg){this._officeNumber=arg;}

Here.Icanseparatethetelephonenumberbehaviorintoitsownclass.Istartbydefininganemptytelephonenumberclass:

classTelephoneNumber{

}

Thatwaseasy!Next,Icreateaninstanceoftelephonenumberwhenconstructingtheperson:

classPerson…

constructor(){

this._telephoneNumber=newTelephoneNumber();

}

classTelephoneNumber…

getofficeAreaCode(){returnthis._officeAreaCode;}

setofficeAreaCode(arg){this._officeAreaCode=arg;}

IthenuseMoveField(205)ononeofthefields.

classPerson…

getofficeAreaCode(){returnthis._telephoneNumber.officeAreaCode;}

setofficeAreaCode(arg){this._telephoneNumber.officeAreaCode=arg;}

Itest,thenmovethenextfield.

classTelephoneNumber…

getofficeNumber(){returnthis._officeNumber;}

setofficeNumber(arg){this._officeNumber=arg;}

classPerson…

getofficeNumber(){returnthis._telephoneNumber.officeNumber;}

setofficeNumber(arg){this._telephoneNumber.officeNumber=arg;}

Testagain,thenmovethetelephonenumbermethod.

classTelephoneNumber…

gettelephoneNumber(){return`(${this.officeAreaCode})${this.officeNumber}`;}

classPerson…

gettelephoneNumber(){returnthis._telephoneNumber.telephoneNumber;

NowIshouldtidythingsup.Having“office”aspartofthetelephonenumbercodemakesnosense,soIrenamethem.

classTelephoneNumber…

getareaCode(){returnthis._areaCode;}

setareaCode(arg){this._areaCode=arg;}

getnumber(){returnthis._number;}

setnumber(arg){this._number=arg;}

classPerson…

getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}

setofficeAreaCode(arg){this._telephoneNumber.areaCode=arg;}

getofficeNumber(){returnthis._telephoneNumber.number;}

setofficeNumber(arg){this._telephoneNumber.number=arg;}

Thetelephonenumbermethodonthetelephonenumberclassalsodoesn’tmakemuchsense,soIapplyChangeFunctionDeclaration(124).

classTelephoneNumber…

toString(){return`(${this.areaCode})${this.number}`;}

classPerson…

gettelephoneNumber(){returnthis._telephoneNumber.toString();}

Telephonenumbersaregenerallyuseful,soIthinkI’llexposethenewobjecttoclients.Icanreplacethose“office”methodswithaccessorsforthetelephonenumber.Butthisway,thetelephonenumberwillworkbetterasaValueObject(https://martinfowler.com/bliki/ValueObject.html),soIwouldapplyChangeReferencetoValue(252)first(thatrefactoring’sexampleshowshowI’ddothatforthetelephonenumber).

InlineClass

Motivation

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

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

Mechanics

Inthetargetclass,createfunctionsforallthepublicfunctionsofthesourceclass.Thesefunctionsshouldjustdelegatetothesourceclass.

Changeallreferencestosourceclassmethodssotheyusethetargetclass’sdelegatorsinstead.Testaftereachchange.

Moveallthefunctionsanddatafromthesourceclassintothetarget,testingaftereachmove,untilthesourceclassisempty.

Deletethesourceclassandholdashort,simplefuneralservice.

Example

Here’saclassthatholdsacoupleofpiecesoftrackinginformationforashipment.

classTrackingInformation{

getshippingCompany(){returnthis._shippingCompany;}

setshippingCompany(arg){this._shippingCompany=arg;}

gettrackingNumber(){returnthis._trackingNumber;}

settrackingNumber(arg){this._trackingNumber=arg;}

getdisplay(){

return`${this.shippingCompany}:${this.trackingNumber}`;

}

}

It’susedaspartofashipmentclass.

classShipment…

gettrackingInfo(){

returnthis._trackingInformation.display;

}

gettrackingInformation(){returnthis._trackingInformation;}

settrackingInformation(aTrackingInformation){

this._trackingInformation=aTrackingInformation;

}

Whilethisclassmayhavebeenworthwhileinthepast,Inolongerfeelit’spullingitsweight,soIwanttoinlineitintoShipment.

IstartbylookingatplacesthatareinvokingthemethodsofTrackingInformation.

caller…

aShipment.trackingInformation.shippingCompany=request.vendor;

I’mgoingtomoveallsuchfunctionstoShipment,butIdoitslightlydifferentlytohowIusuallydoMoveFunction(196).Inthiscase,Istartbyputtingadelegatingmethodintotheshipment,andadjustingtheclienttocallthat.

classShipment…

setshippingCompany(arg){this._trackingInformation.shippingCompany=arg;}

caller…

aShipment.trackingInformation.shippingCompany=request.vendor;

Idothisforalltheelementsoftrackinginformationthatareusedbyclients.OnceI’vedonethat,Icanmovealltheelementsofthetrackinginformationoverintotheshipmentclass.

IstartbyapplyingInlineFunction(115)tothedisplaymethod.

classShipment…

gettrackingInfo(){

return`${this.shippingCompany}:${this.trackingNumber}`;

}

Imovetheshippingcompanyfield.

getshippingCompany(){returnthis._trackingInformation._shippingCompany;}

setshippingCompany(arg){this._trackingInformation._shippingCompany=arg;}

Idon’tusethefullmechanicsforMoveField(205)sinceinthiscaseIonlyreferenceshippingCompanyfromShipmentwhichisthetargetofthemove.Ithusdon’tneedthestepsthatputareferencefromthesourcetothetarget.

Icontinueuntileverythingismovedover.OnceI’vedonethat,Icandeletethetrackinginformationclass.

classShipment…

gettrackingInfo(){

return`${this.shippingCompany}:${this.trackingNumber}`;

}

getshippingCompany(){returnthis._shippingCompany;}

setshippingCompany(arg){this._shippingCompany=arg;}

gettrackingNumber(){returnthis._trackingNumber;}

settrackingNumber(arg){this._trackingNumber=arg;}

HideDelegate

inverseof:RemoveMiddleMan(190)

Motivation

Oneofthekeys—ifnotthekey—togoodmodulardesignisencapsulation.Encapsulationmeansthatmodulesneedtoknowlessaboutotherpartsofthesystem.Then,whenthingschange,fewermodulesneedtobetoldaboutthechange—whichmakesthechangeeasiertomake.

Whenwearefirsttaughtaboutobjectorientation,wearetoldthatencapsulationmeanshidingourfields.Aswebecomemoresophisticated,werealizethereismorethatwecanencapsulate.

IfIhavesomeclientcodethatcallsamethoddefinedonanobjectinafieldofaserverobject,theclientneedstoknowaboutthisdelegateobject.Ifthedelegatechangesitsinterface,changespropagatetoalltheclientsoftheserverthatusethedelegate.Icanremovethisdependencybyplacingasimpledelegatingmethodontheserverthathidesthedelegate.ThenanychangesImaketothedelegatepropagateonlytotheserverandnottotheclients.

Mechanics

Foreachmethodonthedelegate,createasimpledelegatingmethodontheserver.

Adjusttheclienttocalltheserver.Testaftereachchange.

Ifnoclientneedstoaccessthedelegateanymore,removetheserver’saccessorforthedelegate.

Test.

Example

Istartwithapersonandadepartment

classPerson…

constructor(name){

this._name=name;

}

getname(){returnthis._name;}

getdepartment(){returnthis._department;}

setdepartment(arg){this._department=arg;}

classDepartment…

getchargeCode(){returnthis._chargeCode;}

setchargeCode(arg){this._chargeCode=arg;}

getmanager(){returnthis._manager;}

setmanager(arg){this._manager=arg;}

Someclientcodewantstoknowthemanagerofaperson.Todothis,itneedstogetthedepartmentfirst.

clientcode…

manager=aPerson.department.manager;

Thisrevealstotheclienthowthedepartmentclassworksandthatthedepartmentisresponsiblefortrackingthemanager.Icanreducethiscouplingbyhidingthedepartmentclassfromtheclient.Idothisbycreatingasimpledelegatingmethodonperson:

classPerson…

getmanager(){returnthis._department.manager;}

Inowneedtochangeallclientsofpersontousethisnewmethod:

clientcode…

manager=aPerson.department.manager;

OnceI’vemadethechangeforallmethodsofdepartmentandforalltheclientsofperson,Icanremovethedepartmentaccessoronperson.

RemoveMiddleMan

inverseof:HideDelegate(187)

Motivation

InthemotivationforHideDelegate(187),Italkedabouttheadvantagesofencapsulatingtheuseofadelegatedobject.Thereisapriceforthis.Everytimetheclientwantstouseanewfeatureofthedelegate,Ihavetoaddasimpledelegatingmethodtotheserver.Afteraddingfeaturesforawhile,Igetirritatedwithallthisforwarding.Theserverclassisjustamiddleman(MiddleMan,p.79),andperhapsit’stimefortheclienttocallthedelegatedirectly.(Thissmelloftenpopsupwhenpeoplegetover-enthusiasticaboutfollowingtheLawofDemeter,whichI’dlikealotmoreifitwerecalledtheOccasionallyUsefulSuggestionofDemeter.)

It’shardtofigureoutwhattherightamountofhidingis.Fortunately,withHideDelegate(187)andRemoveMiddleMan,itdoesn’tmattersomuch.Icanadjustmycodeastimegoeson.Asthesystemchanges,thebasisforhowmuchIhidealsochanges.Agoodencapsulationsixmonthsagomaybeawkwardnow.RefactoringmeansIneverhavetosayI’msorry—Ijustfixit.

Mechanics

Createagetterforthedelegate.

Foreachclientuseofadelegatingmethod,replacethecalltothedelegatingmethodbychainingthroughtheaccessor.Testaftereachreplacement.

Ifallcallstoadelegatingmethodarereplaced,youcandeletethedelegatingmethod.

Withautomatedrefactorings,youcanuseEncapsulateVariable(132)onthedelegatefieldandthenInlineFunction(115)onallthemethodsthatuseit.

Example

Ibeginwithapersonclassthatusesalinkeddepartmentobjecttodetermineamanager.(Ifyou’rereadingthisbooksequentially,thisexamplemaylookeerilyfamiliar.)

clientcode…

manager=aPerson.manager;

classPerson…

getmanager(){returnthis._department.manager;}

classDepartment…

getmanager(){returnthis._manager;}

Thisissimpletouseandencapsulatesthedepartment.However,iflotsofmethodsaredoingthis,Iendupwithtoomanyofthesesimpledelegationsontheperson.That’swhenitisgoodtoremovethemiddleman.First,Imakeanaccessorforthedelegate:

classPerson…

getdepartment(){returnthis._department;}

NowIgotoeachclientatatimeandmodifythemtousethedepartment

directly.

clientcode…

manager=aPerson.department.manager;

OnceI’vedonethiswithalltheclients,IcanremovethemanagermethodfromPerson.IcanrepeatthisprocessforanyothersimpledelegationsonPerson.

Icandoamixturehere.SomedelegationsmaybesocommonthatI’dliketokeepthemtomakeclientcodeeasiertoworkwith.ThereisnoabsolutereasonwhyIshouldeitherhideadelegateorremoveamiddleman—particularcircumstancessuggestwhichapproachtotake,andreasonablepeoplecandifferonwhatworksbest.

IfIhaveautomatedrefactorings,thenthere’sausefulvariationonthesesteps.First,IuseEncapsulateVariable(132)ondepartment.Thischangesthemanagergettertousethepublicdepartmentgetter:

classPerson…

getmanager(){returnthis.department.manager;}

ThechangeisrathertoosubtleinJavaScript,butbyremovingtheunderscorefromdepartmentI’musingthenewgetterratherthanaccessingthefielddirectly.

ThenIapplyInlineFunction(115)onthemanagermethodtoreplaceallthecallersatonce.

SubstituteAlgorithm

Motivation

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

Sometimes,whenIwanttochangethealgorithmtoworkslightlydifferently,it’seasiertostartbyreplacingitwithsomethingthatwouldmakemychangemorestraightforwardtomake.

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

Mechanics

Arrangethecodetobereplacedsothatitfillsacompletefunction.

Preparetestsusingthisfunctiononly,tocaptureitsbehavior.

Prepareyouralternativealgorithm.

Runstaticchecks.

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

Chapter8MovingFeaturesSofar,therefactoringshavebeenaboutcreating,removing,andrenamingprogramelements.Anotherimportantpartofrefactoringismovingelementsbetweencontexts.IuseMoveFunction(196)tomovefunctionsbetweenclassesandothermodules.Fieldscanmovetoo,withMoveField(205).

Ialsomoveindividualstatementsaround.IuseMoveStatementsintoFunction(211)andMoveStatementstoCallers(215)tomovetheminoroutoffunctions,aswellasSlideStatements(221)tomovethemwithinafunction.Sometimes,IcantakesomestatementsthatmatchanexisitingfunctionanduseReplaceInlineCodewithFunctionCall(220)toremovetheduplication.

TworefactoringsIoftendowithloopsareSplitLoop(226),toensurealoopdoesonlyonething,andReplaceLoopwithPipeline(230)togetridofaloopentirely.

Andthenthere’sthefavoriterefactoringofmanyafineprogrammer:RemoveDeadCode(236).Nothingisassatisfyingasapplyingthedigitalflamethrowertosuperfluousstatements.

MoveFunction

formerly:MoveMethod

Motivation

Theheartofagoodsoftwaredesignisitsmodularity—whichismyabilitytomakemostmodificationstoaprogramwhileonlyhavingtounderstandasmallpartofit.Togetthismodularity,Ineedtoensurethatrelatedsoftwareelementsaregroupedtogetherandthelinksbetweenthemareeasytofindandunderstand.Butmyunderstandingofhowtodothisisn’tstatic—asIbetterunderstandwhatI’mdoing,Ilearnhowtobestgrouptogethersoftwareelements.Toreflectthatgrowingunderstanding,Ineedtomoveelementsaround.

Allfunctionsliveinsomecontext;itmaybeglobal,butusuallyit’ssomeformofamodule.Inanobject-orientedprogram,thecoremodularcontextisaclass.Nestingafunctionwithinanothercreatesanothercommoncontext.Differentlanguagesprovidevariedformsofmodularity,eachcreatingacontextforafunctiontolivein.

Oneofthemoststraightforwardreasonstomoveafunctioniswhenitreferenceselementsinothercontextsmorethantheoneitcurrentlyresidesin.Movingittogetherwiththoseelementsoftenimprovesencapsulation,allowingotherpartsofthesoftwaretobelessdependentonthedetailsofthismodule.

Similarly,Imaymoveafunctionbecauseofwhereitscallerslive,orwhereIneedtocallitfrominmynextenhancement.Afunctiondefinedasahelperinsideanotherfunctionmayhavevalueonitsown,soit’sworthmovingittosomewheremoreaccessible.Amethodonaclassmaybeeasierformetouseifshiftedtoanother.

Decidingtomoveafunctionisrarelyaneasydecision.Tohelpmedecide,Iexaminethecurrentandcandidatecontextsforthatfunction.Ineedtolookatwhatfunctionscallthisone,whatfunctionsarecalledbythemovingfunction,andwhatdatathatfunctionuses.Often,IseethatIneedanewcontextforagroupoffunctionsandcreateonewithCombineFunctionsintoClass(144)orExtractClass(180).Althoughitcanbedifficulttodecidewherethebestplaceforafunctionis,themoredifficultthischoice,oftenthelessitmatters.Ifinditvaluabletotryworkingwithfunctionsinonecontext,knowingI’lllearnhowwelltheyfit,andiftheydon’tfitIcanalwaysmovethemlater.

Mechanics

Examinealltheprogramelementsusedbythechosenfunctioninitscurrentcontext.Considerwhethertheyshouldmovetoo.

IfIfindacalledfunctionthatshouldalsomove,Iusuallymoveitfirst.Thatway,movingaclustersoffunctionsbeginswiththeonethathastheleastdependencyontheothersinthegroup.

Ifahigh-levelfunctionistheonlycallerofsubfunctions,thenyoucaninlinethosefunctionsintothehigh-levelmethod,move,andre-extractatthedestination.

Checkifthechosenfunctionisapolymorphicmethod.

IfI’minanobject-orientedlanguage,Ihavetotakeaccountofsuper-andsubclassdeclarations.

Copythefunctiontothetargetcontext.Adjustittofitinitsnewhome.

Ifthebodyuseselementsinthesourcecontext,Ineedtoeitherpassthoseelementsasparametersorpassareferencetothatsourcecontext.

MovingafunctionoftenmeansIneedtocomeupwithadifferentnamethatworksbetterinthenewcontext.

Performstaticanalysis.

Figureouthowtoreferencethetargetfunctionfromthesourcecontext.

Turnthesourcefunctionintoadelegatingfunction.

Test.

ConsiderInlineFunction(115)onthesourcefunction.

Thesourcefunctioncanstayindefinitelyasadelegatingfunction.Butifitscallerscanjustaseasilyreachthetargetdirectly,thenit’sbettertoremovethemiddleman.

Example:MovingaNestedFunctiontoTopLevel

I’llbeginwithafunctionthatcalculatesthetotaldistanceforaGPStrackrecord.

functiontrackSummary(points){

consttotalTime=calculateTime();

consttotalDistance=calculateDistance();

constpace=totalTime/60/totalDistance;

return{

time:totalTime,

distance:totalDistance,

pace:pace

};

functioncalculateDistance(){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

}

functiondistance(p1,p2){…}

functionradians(degrees){…}

functioncalculateTime(){…}

}

I’dliketomovecalculateDistancetothetoplevelsoIcancalculatedistancesfortrackswithoutalltheotherpartsofthesummary.

Ibeginbycopyingthefunctiontothetoplevel.

functiontrackSummary(points){

consttotalTime=calculateTime();

consttotalDistance=calculateDistance();

constpace=totalTime/60/totalDistance;

return{

time:totalTime,

distance:totalDistance,

pace:pace

};

functioncalculateDistance(){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

}

functiondistance(p1,p2){…}

functionradians(degrees){…}

functioncalculateTime(){…}

}

functiontop_calculateDistance(){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

}

WhenIcopyafunctionlikethis,IliketochangethenamesoIcandistinguishthembothinthecodeandinmyhead.Idon’twanttothinkaboutwhattherightnameshouldberightnow,soIcreateatemporaryname.

Theprogramstillworks,butmystaticanalysisisrightlyratherupset.Thenewfunctionhastwoundefinedsymbols:distanceandpoints.Thenaturalwaytodealwithpointsistopassitinasaparameter.

functiontop_calculateDistance(points){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

}

Icoulddothesamewithdistance,butperhapsitmakessensetomoveittogetherwithcalculateDistance.Here’stherelevantcode:

functiontrackSummary…

functiondistance(p1,p2){

//haversineformulaseehttp://www.movable-type.co.uk/scripts/latlong.html

constEARTH_RADIUS=3959;//inmiles

constdLat=radians(p2.lat)-radians(p1.lat);

constdLon=radians(p2.lon)-radians(p1.lon);

consta=Math.pow(Math.sin(dLat/2),2)

+Math.cos(radians(p2.lat))

*Math.cos(radians(p1.lat))

*Math.pow(Math.sin(dLon/2),2);

constc=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));

returnEARTH_RADIUS*c;

}

functionradians(degrees){

returndegrees*Math.PI/180;

}

Icanseethatdistanceonlyusesradiansandradiansdoesn’tuseanythinginsideitscurrentcontext.Soratherthanpassthefunctions,Imightaswellmovethemtoo.IcanmakeasmallstepinthisdirectionbymovingthemfromtheircurrentcontexttonesttheminsidethenestedcalculateDistance.

functiontrackSummary(points){

consttotalTime=calculateTime();

consttotalDistance=calculateDistance();

constpace=totalTime/60/totalDistance;

return{

time:totalTime,

distance:totalDistance,

pace:pace

};

functioncalculateDistance(){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

functiondistance(p1,p2){…}

functionradians(degrees){…}

}

Bydoingthis,Icanusebothstaticanalysisandtestingtotellmeifthereareanycomplications.Inthiscasealliswell,soIcancopythemovertotop_calculateDistance.

functiontop_calculateDistance(points){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

functiondistance(p1,p2){…}

functionradians(degrees){…}

}

Again,thecopydoesn’tchangehowtheprogramruns,butdoesgivemeanopportunityformorestaticanalysis.HadInotspottedthatdistancecallsradians,thelinterwouldhavecaughtitatthisstep.

NowthatIhavepreparedthetable,it’stimeforthemajorchange—thebodyoftheoriginalcalculateDistancewillnowcalltop_calculateDistance:

functiontrackSummary(points){

consttotalTime=calculateTime();

consttotalDistance=calculateDistance();

constpace=totalTime/60/totalDistance;

return{

time:totalTime,

distance:totalDistance,

pace:pace

};

functioncalculateDistance(){

returntop_calculateDistance(points);

}

Thisisthecrucialtimetorunteststofullytestthatthemovedfunctionhasbeddeddowninitsnewhome.

Withthatdone,it’slikeunpackingtheboxesaftermovinghouse.Thefirstthingistodecidewhethertokeeptheoriginalfunctionthat’sjustdelegatingornot.Inthiscase,therearefewcallersand,asusualwithnestedfunctions,theyarehighlylocalized.SoI’mhappytogetridofit.

functiontrackSummary(points){

consttotalTime=calculateTime();

consttotalDistance=top_calculateDistance(points);

constpace=totalTime/60/totalDistance;

return{

time:totalTime,

distance:totalDistance,

pace:pace

};

NowisalsoagoodtimetothinkaboutwhatIwantthenametobe.Sincethetop-levelfunctionhasthehighestvisibility,I’dlikeittohavethebestname.totalDistanceseemslikeagoodchoice.Ican’tusethatimmediatelysinceitwillbeshadowedbythevariableinsidetrackSummary—butIdon’tseeanyreasontokeepthatanyway,soIuseInlineVariable(123)onit.

functiontrackSummary(points){

consttotalTime=calculateTime();

constpace=totalTime/60/totalDistance(points);

return{

time:totalTime,

distance:totalDistance(points),

pace:pace

};

functiontotalDistance(points){

letresult=0;

for(leti=1;i<points.length;i++){

result+=distance(points[i-1],points[i]);

}

returnresult;

IfI’dhadtheneedtokeepthevariable,I’dhaverenamedittosomethingliketotalDistanceCacheordistance.

Sincethefunctionsfordistanceandradiansdon’tdependonanythinginsidetotalDistance,Iprefertomovethemtotopleveltoo,puttingallfourfunctionsatthetoplevel.

functiontrackSummary(points){…}

functiontotalDistance(points){…}

functiondistance(p1,p2){…}

functionradians(degrees){…}

SomepeoplewouldprefertokeepdistanceandradiansinsidetotalDistanceinordertorestricttheirvisibility.Insomelanguagesthatmaybeaconsideration,butwithES2015,JavaScripthasanexcellentmodulemechanismthat’sthebesttoolforcontrollingfunctionvisibility.Ingeneral,I’mwaryofnestedfunctions—theytooeasilysetuphiddendatainterrelationshipsthatcangethardtofollow.

Example:MovingBetweenClasses

ToillustratethisvarietyofMoveFunction,I’llstarthere:

classAccount…

getbankCharge(){

letresult=4.5;

if(this._daysOverdrawn>0)result+=this.overdraftCharge;

returnresult;

}

getoverdraftCharge(){

if(this.type.isPremium){

constbaseCharge=10;

if(this.daysOverdrawn<=7)

returnbaseCharge;

else

returnbaseCharge+(this.daysOverdrawn-7)*0.85;

}

else

returnthis.daysOverdrawn*1.75;

}

Cominguparechangesthatleadtodifferenttypesofaccounthavingadifferentalgorithmsfordeterminingthecharge.ThusitseemsnaturaltomoveoverdraftChargetotheaccounttypeclass.

ThefirststepistolookatthefeaturesthattheoverdraftChargemethodusesandconsiderwhetheritisworthmovingabatchofmethodstogether.InthiscaseIneedthedaysOverdrawnmethodtoremainontheaccountclass,becausethatwillvarywithindividualaccounts.

Next,Icopythemethodbodyovertotheaccounttypeandgetittofit.

classAccountType…

overdraftCharge(daysOverdrawn){

if(this.isPremium){

constbaseCharge=10;

if(daysOverdrawn<=7)

returnbaseCharge;

else

returnbaseCharge+(daysOverdrawn-7)*0.85;

}

else

returndaysOverdrawn*1.75;

}

Inordertogetthemethodtofitinitsnewlocation,Ineedtodealwithtwocalltargetsthatchangetheirscope.isPremiumisnowasimplecallonthis.WithdaysOverdrawnIhavetodecide—doIpassthevalueordoIpasstheaccount?Forthemoment,IjustpassthesimplevaluebutImaywellchangethisinthefutureifIrequiremorethanjustthedaysoverdrawnfromtheaccount—especiallyifwhatIwantfromtheaccountvarieswiththeaccounttype.

Next,Ireplacetheoriginalmethodbodywithadelegatingcall.

classAccount…

getbankCharge(){

letresult=4.5;

if(this._daysOverdrawn>0)result+=this.overdraftCharge;

returnresult;

}

getoverdraftCharge(){

returnthis.type.overdraftCharge(this.daysOverdrawn);

}

ThencomesthedecisionofwhethertoleavethedelegationinplaceortoinlineoverdraftCharge.Inliningresultsin:

classAccount…

getbankCharge(){

letresult=4.5;

if(this._daysOverdrawn>0)

result+=this.type.overdraftCharge(this.daysOverdrawn);

returnresult;

}

Intheearliersteps,IpasseddaysOverdrawnasaparameter—butifthere’salotofdatafromtheaccounttopass,Imightprefertopasstheaccountitself.

classAccount…

getbankCharge(){

letresult=4.5;

if(this._daysOverdrawn>0)result+=this.overdraftCharge;

returnresult;

}

getoverdraftCharge(){

returnthis.type.overdraftCharge(this);

}

classAccountType…

overdraftCharge(account){

if(this.isPremium){

constbaseCharge=10;

if(account.daysOverdrawn<=7)

returnbaseCharge;

else

returnbaseCharge+(account.daysOverdrawn-7)*0.85;

}

else

returnaccount.daysOverdrawn*1.75;

}

MoveField

Motivation

Programminginvolveswritingalotofcodethatimplementsbehavior—butthestrengthofaprogramisreallyfoundedonitsdatastructures.IfIhaveagoodsetofdatastructuresthatmatchtheproblem,thenmybehaviorcodeissimpleandstraightforward.Butpoordatastructuresleadtolotsofcodewhosejobismerelydealingwiththepoordata.Andit’snotjustmessiercodethat’sharderto

understand;italsomeansthedatastructuresobscurewhattheprogramisdoing.

So,datastructuresareimportant—butlikemostaspectsofprogrammingtheyarehardtogetright.Idomakeaninitialanalysistofigureoutthebestdatastructures,andI’vefoundthatexperienceandtechniqueslikedomain-drivendesignhaveimprovedmyabilitytodothat.Butdespiteallmyskillandexperience,IstillfindthatIfrequentlymakemistakesinthatinitialdesign.Intheprocessofprogramming,Ilearnmoreabouttheproblemdomainandmydatastructures.Adesigndecisionthatisreasonableandcorrectoneweekcanbecomewronginanother.

AssoonasIrealizethatadatastructureisn’tright,it’svitaltochangeit.IfIleavemydatastructureswiththeirblemishes,thoseblemisheswillconfusemythinkingandcomplicatemycodefarintothefuture.

ImayseektomovedatabecauseIfindIalwaysneedtopassafieldfromonerecordwheneverIpassanotherrecordtoafunction.Piecesofdatathatarealwayspassedtofunctionstogetherareusuallybestputinasinglerecordinordertoclarifytheirrelationship.Changeisalsoafactor;ifachangeinonerecordcausesafieldinanotherrecordtochangetoo,that’sasignofafieldinthewrongplace.IfIhavetoupdatethesamefieldinmultiplestructures,that’sasignthatitshouldmovetoanotherplacewhereitonlyneedstobeupdatedonce.

IusuallydoMoveFieldinthecontextofabroadersetofchanges.OnceI’vemovedafield,Ifindthatmanyoftheusersofthefieldarebetteroffaccessingthatdatathroughthetargetobjectratherthantheoriginalsource.Ithenchangethesewithlaterrefactorings.Similarly,ImayfindthatIcan’tdoMoveFieldatthemomentduetothewaythedataisused.Ineedtorefactorsomeusagepatternsfirst,thendothemove.

Inmydescriptionsofar,I’msaying“record,”butallthisistrueofclassesandobjectstoo.Aclassisarecordtypewithattachedfunctions—andtheseneedtobekepthealthyjustasmuchasanyotherdata.Theattachedfunctionsdomakeiteasiertomovedataaround,sincethedataisencapsulatedbehindaccessormethods.Icanmovethedata,changetheaccessors,andclientsoftheaccessorswillstillwork.So,thisisarefactoringthat’seasiertodoifyouhaveclasses,andmydescriptionbelowmakesthatassumption.IfI’musingbarerecordsthatdon’tsupportencapsulation,Icanstillmakeachangelikethis,butitismoretricky.

Mechanics

Ensurethesourcefieldisencapsulated.

Test.

Createafield(andaccessors)inthetarget.

Runstaticchecks.

Ensurethereisareferencefromthesourceobjecttothetargetobject.

Anexistingfieldormethodmaygiveyouthetarget.Ifnot,seeifyoucaneasilycreateamethodthatwilldoso.Failingthat,youmayneedtocreateanewfieldinthesourceobjectthatcanstorethetarget.Thismaybeapermanentchange,butyoucanalsodoittemporarilyuntilyouhavedoneenoughrefactoringinthebroadercontext.

Adjustaccessorstousethetargetfield.

Ifthetargetissharedbetweensourceobjects,considerfirstupdatingthesettertomodifybothtargetandsourcefields,followedbyIntroduceAssertion(299)todetectinconsistentupdates.Onceyoudeterminealliswell,finishchangingtheaccessorstousethetargetfield.

Test.

Removethesourcefield.

Test.

Example

I’mstartingherewiththiscustomerandcontract.

classCustomer…

constructor(name,discountRate){

this._name=name;

this._discountRate=discountRate;

this._contract=newCustomerContract(dateToday());

}

getdiscountRate(){returnthis._discountRate;}

becomePreferred(){

this._discountRate+=0.03;

//othernicethings

}

applyDiscount(amount){

returnamount.subtract(amount.multiply(this._discountRate));

}

classCustomerContract…

constructor(startDate){

this._startDate=startDate;

}

Iwanttomovethediscountratefieldfromthecustomertothecustomercontract.

ThefirstthingIneedtouseisEncapsulateVariable(132)toencapsulateaccesstothediscountratefield.

classCustomer…

constructor(name,discountRate){

this._name=name;

this._setDiscountRate(discountRate);

this._contract=newCustomerContract(dateToday());

}

getdiscountRate(){returnthis._discountRate;}

_setDiscountRate(aNumber){this._discountRate=aNumber;}

becomePreferred(){

this._setDiscountRate(this.discountRate+0.03);

//othernicethings

}

applyDiscount(amount){

returnamount.subtract(amount.multiply(this.discountRate));

}

Iuseamethodtoupdatethediscountrate,ratherthanapropertysetter,asIdon’twanttomakeapublicsetterforthediscountrate.

Iaddafieldandaccessorstothecustomercontract.

classCustomerContract…

constructor(startDate,discountRate){

this._startDate=startDate;

this._discountRate=discountRate;

}

getdiscountRate(){returnthis._discountRate;}

setdiscountRate(arg){this._discountRate=arg;}

Inowmodifytheaccessorsoncustomertousethenewfield.WhenIdidthat,Igotanerror:“Cannotsetproperty‘discountRate’ofundefined”.Thiswasbecause_setDiscountRatewascalledbeforeIcreatedthecontractobjectintheconstructor.Tofixthat,Ifirstrevertedtothepreviousstate,thenusedSlideStatements(221)tomovethe_setDiscountRateaftercreatingthecontract.

classCustomer…

constructor(name,discountRate){

this._name=name;

this._setDiscountRate(discountRate);

this._contract=newCustomerContract(dateToday());

}

Itestedthat,thenchangedtheaccessorsagaintousethecontract.

classCustomer…

getdiscountRate(){returnthis._contract.discountRate;}

_setDiscountRate(aNumber){this._contract.discountRate=aNumber;}

SinceI’musingJavaScript,thereisnodeclaredsourcefield,soIdon’tneedtoremoveanythingfurther.

ChangingaBareRecord

Thisrefactoringisgenerallyeasierwithobjects,sinceencapsulationprovidesanaturalwaytowrapdataaccessinmethods.IfIhavemanyfunctionsaccessingabarerecord,then,whileit’sstillavaluablerefactoring,itisdecidedlymoretricky.

Icancreateaccessorfunctionsandmodifyallthereadsandwritestousethem.Ifthefieldthat’sbeingmovedisimmutable,IcanupdateboththesourceandthetargetfieldswhenIsetitsvalueandgraduallymigratereads.Still,ifpossible,myfirstmovewouldbetouseEncapsulateRecord(160)toturntherecordinto

aclasssoIcanmakethechangemoreeasily.

Example:MovingtoaSharedObject

Now,let’sconsideradifferentcase.Here’sanaccountwithaninterestrate.

classAccount…

constructor(number,type,interestRate){

this._number=number;

this._type=type;

this._interestRate=interestRate;

}

getinterestRate(){returnthis._interestRate;}

classAccountType…

constructor(nameString){

this._name=nameString;

}

Iwanttochangethingssothatanaccount’sinterestrateisdeterminedfromitsaccounttype.

Theaccesstotheinterestrateisalreadynicelyencapsulated,soI’lljustcreatethefieldandanappropriateaccessorontheaccounttype.

classAccountType…

constructor(nameString,interestRate){

this._name=nameString;

this._interestRate=interestRate;

}

getinterestRate(){returnthis._interestRate;}

ButthereisapotentialproblemwhenIupdatetheaccessesfromAccount.Beforethisrefactoring,eachaccounthaditsowninterestrate.Now,Iwantallaccountstosharetheinterestratesoftheiraccounttype.Ifalltheaccountsofthesametypealreadyhavethesameinterestrate,thenthere’snochangeinobservablebehavior,soI’mfinewiththerefactoring.Butifthere’sanaccountwithadifferentinterestrate,it’snolongerarefactoring.Ifmyaccountdataisheldinadatabase,Ishouldcheckthedatabasetoensurethatallmyaccountshavetheratematchingtheirtype.IcanalsoIntroduceAssertion(299)inthe

accountclass.

classAccount…

constructor(number,type,interestRate){

this._number=number;

this._type=type;

assert(interestRate===this._type.interestRate);

this._interestRate=interestRate;

}

getinterestRate(){returnthis._interestRate;}

ImightrunthesystemforawhilewiththisassertioninplacetoseeifIgetanerror.Or,insteadofaddinganassertion,Imightlogtheproblem.OnceI’mconfidentthatI’mnotintroducinganobservablechange,Icanchangetheaccess,removingtheupdatefromtheaccountcompletely.

classAccount…

constructor(number,type){

this._number=number;

this._type=type;

}

getinterestRate(){returnthis._type.interestRate;}

MoveStatementsintoFunction

inverseof:MoveStatementstoCallers(215)

Motivation

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

MoveStatementstoCallers(215).

ImovestatementsintoafunctionwhenIcanbestunderstandthesestatementsaspartofthecalledfunction.Iftheydon’tmakesenseaspartofthecalledfunction,butstillshouldbecalledwithit,I’llsimplyuseExtractFunction(106)onthestatementsandthecalledfunction.That’sessentiallythesameprocessasIdescribebelow,butwithouttheinlineandrenamesteps.It’snotunusualtodothatandthen,afterlaterreflection,carryoutthethosefinalsteps.

Mechanics

Iftherepetitivecodeisn’tadjacenttothecallofthetargetfunction,useSlideStatements(221)togetitadjacent.

Ifthetargetfunctionisonlycalledbythesourcefunction,justcutthecodefromthesource,pasteitintothetarget,test,andignoretherestofthesemechanics.

Ifyouhavemorecallers,useExtractFunction(106)ononeofthecallsitestoextractboththecalltothetargetfunctionandthestatementsyouwishtomoveintoit.Giveitanamethat’stransient,buteasytogrep.

Converteveryothercalltousethenewfunction.Testaftereachconversion.

Whenalltheoriginalcallsusethenewfunction,useInlineFunction(115)toinlinetheoriginalfunctioncompletelyintothenewfunction,removingtheoriginalfunction.

ChangeFunctionDeclaration(124)tochangethenameofthenewfunctiontothesamenameastheoriginalfunction.

Ortoabettername,ifthereisone.

Example

I’llstartwiththiscodetoemitHTMLfordataaboutaphoto.

functionrenderPerson(outStream,person){

constresult=[];

result.push(`<p>${person.name}</p>`);

result.push(renderPhoto(person.photo));

result.push(`<p>title:${person.photo.title}</p>`);

result.push(emitPhotoData(person.photo));

returnresult.join("\n");

}

functionphotoDiv(p){

return[

"<div>",

`<p>title:${p.title}</p>`,

emitPhotoData(p),

"</div>",

].join("\n");

}

functionemitPhotoData(aPhoto){

constresult=[];

result.push(`<p>location:${aPhoto.location}</p>`);

result.push(`<p>date:${aPhoto.date.toDateString()}</p>`);

returnresult.join("\n");

}

ThiscodeshowstwocallstoemitPhotoData,eachprecededbyalineofcodethatissemanticallyequivalent.I’dliketoremovethisduplicationbymovingthetitleprintingintoemitPhotoData.IfIhadjusttheonecaller,Iwouldjustcutandpastethecode,butthemorecallersIhave,themoreI’minclinedtouseasaferprocedure.

IbeginbyusingExtractFunction(106)ononeofthecallers.I’mextractingthestatementsIwanttomoveintoemitPhotoData,togetherwiththecalltoemitPhotoDataitself.

functionphotoDiv(p){

return[

"<div>",

zznew(p),

"</div>",

].join("\n");

}

functionzznew(p){

return[

`<p>title:${p.title}</p>`,

emitPhotoData(p),

].join("\n");

}

IcannowlookattheothercallersofemitPhotoDataand,onebyone,replacethecallsandtheprecedingstatementswithcallstothenewfunction.

functionrenderPerson(outStream,person){

constresult=[];

result.push(`<p>${person.name}</p>`);

result.push(renderPhoto(person.photo));

result.push(zznew(person.photo));

returnresult.join("\n");

}

NowthatI’vedoneallthecallers,IuseInlineFunction(115)onemitPhotoData.

functionzznew(p){

return[

`<p>title:${p.title}</p>`,

`<p>location:${p.location}</p>`,

`<p>date:${p.date.toDateString()}</p>`,

].join("\n");

}

AndfinishwithChangeFunctionDeclaration(124)

functionrenderPerson(outStream,person){

constresult=[];

result.push(`<p>${person.name}</p>`);

result.push(renderPhoto(person.photo));

result.push(emitPhotoData(person.photo));

returnresult.join("\n");

}

functionphotoDiv(aPhoto){

return[

"<div>",

emitPhotoData(aPhoto),

"</div>",

].join("\n");

}

functionemitPhotoData(aPhoto){

return[

`<p>title:${aPhoto.title}</p>`,

`<p>location:${aPhoto.location}</p>`,

`<p>date:${aPhoto.date.toDateString()}</p>`,

].join("\n");

}

IalsomaketheparameternamesfitmyconventionwhileI’matit.

MoveStatementstoCallers

inverseof:MoveStatementsintoFunction(211)

Motivation

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

Onetriggerforthisiswhencommonbehaviorusedinseveralplacesneedstovaryinsomeofitscalls.Now,weneedtomovethevaryingbehavioroutofthefunctiontoitscallers.Inthiscase,I’lluseSlideStatements(221)togetthevaryingbehaviortothebeginningorendofthefunctionandthenMoveStatementstoCallers.Oncethevaryingcodeisinthecaller,Icanchangeitwhennecessary.

MoveStatementstoCallersworkswellforsmallchanges,butsometimestheboundariesbetweencallerandcalleeneedcompletereworking.Inthatcase,mybestmoveistouseInlineFunction(115)andthenslideandextractnewfunctionstoformbetterboundaries.

Mechanics

Insimplecircumstances,whereyouhaveonlyoneortwocallersandasimplefunctiontocallfrom,justcutthefirstlinefromthecalledfunctionandpaste(andperhapsfit)itintothecallers.Testandyou’redone.

Otherwise,applyExtractFunction(106)toallthestatementsthatyoudon’twishtomove;giveitatemporarybuteasilysearchablename.

Ifthefunctionisamethodthatisoverriddenbysubclasses,dotheextractiononallofthemsothattheremainingmethodisidenticalinallclasses.Thenremovethesubclassmethods.

UseInlineFunction(115)ontheoriginalfunction.

ApplyChangeFunctionDeclaration(124)ontheextractedfunctiontorenameittotheoriginalname.

Ortoabettername,ifyoucanthinkofone.

Example

Here’sasimplecase:afunctionwithtwocallers.

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

emitPhotoData(outStream,person.photo);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

emitPhotoData(outStream,p);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

outStream.write(`<p>location:${photo.location}</p>\n`);

}

IneedtomodifythesoftwaresothatlistRecentPhotosrendersthelocationinformationdifferentlywhilerenderPersonstaysthesame.Tomakethischangeeasier,I’lluseMoveStatementstoCallersonthefinalline.

Usually,whenfacedwithsomethingthissimple,I’lljustcutthelastlinefromrenderPersonandpasteitbelowthetwocalls.ButsinceI’mexplainingwhattodoinmoretrickycases,I’llgothroughthemoreelaboratebutsaferprocedure.

MyfirststepistouseExtractFunction(106)onthecodethatwillremaininemitPhotoData.

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

emitPhotoData(outStream,person.photo);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

emitPhotoData(outStream,p);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

zztmp(outStream,photo);

outStream.write(`<p>location:${photo.location}</p>\n`);

}

functionzztmp(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

}

Usually,thenameoftheextractedfunctionisonlytemporary,soIdon’tworryaboutcomingupwithanythingmeaningful.However,itishelpfultousesomethingthat’seasytogrep.Icantestatthispointtoensurethecodeworksoverthefunctioncallboundary.

NowIuseInlineFunction(115),onecallatatime.IstartwithrenderPerson.

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

zztmp(outStream,person.photo);

outStream.write(`<p>location:${person.photo.location}</p>\n`);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

emitPhotoData(outStream,p);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

zztmp(outStream,photo);

outStream.write(`<p>location:${photo.location}</p>\n`);

}

functionzztmp(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

}

Itestagaintoensurethiscallisworkingproperly,thenmoveontothenext.

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

zztmp(outStream,person.photo);

outStream.write(`<p>location:${person.photo.location}</p>\n`);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

zztmp(outStream,p);

outStream.write(`<p>location:${p.location}</p>\n`);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

zztmp(outStream,photo);

outStream.write(`<p>location:${photo.location}</p>\n`);

}

functionzztmp(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

}

ThenIcandeletetheouterfunction,completingInlineFunction(115).

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

zztmp(outStream,person.photo);

outStream.write(`<p>location:${person.photo.location}</p>\n`);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

zztmp(outStream,p);

outStream.write(`<p>location:${p.location}</p>\n`);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

zztmp(outStream,photo);

outStream.write(`<p>location:${photo.location}</p>\n`);

}

functionzztmp(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

}

Ithenrenamezztmpbacktotheoriginalname.

functionrenderPerson(outStream,person){

outStream.write(`<p>${person.name}</p>\n`);

renderPhoto(outStream,person.photo);

emitPhotoData(outStream,person.photo);

outStream.write(`<p>location:${person.photo.location}</p>\n`);

}

functionlistRecentPhotos(outStream,photos){

photos

.filter(p=>p.date>recentDateCutoff())

.forEach(p=>{

outStream.write("<div>\n");

emitPhotoData(outStream,p);

outStream.write(`<p>location:${p.location}</p>\n`);

outStream.write("</div>\n");

});

}

functionemitPhotoData(outStream,photo){

outStream.write(`<p>title:${photo.title}</p>\n`);

outStream.write(`<p>date:${photo.date.toDateString()}</p>\n`);

}

ReplaceInlineCodewithFunctionCall

Motivation

Functionsallowmetopackageupbitsofbehavior.Thisisusefulforunderstanding—anamedfunctioncanexplainthepurposeofthecoderatherthanitsmechanics.It’salsovaluabletoremoveduplication:Insteadofwritingthesamecodetwice,Ijustcallthefunction.Then,shouldIneedtochangethefunction’simplementation,Idon’thavetotrackdownsimilar-lookingcodetoupdateallthechanges.(Imayhavetolookatthecallers,toseeiftheyshouldallusethenewcode,butthat’sbothlesscommonandmucheasier.)

IfIseeinlinecodethat’sdoingthesamethingthatIhaveinanexistingfunction,I’llusuallywanttoreplacethatinlinecodewithafunctioncall.TheexceptionisifIconsiderthesimilaritytobecoincidental—sothat,ifIchangethefunctionbody,Idon’texpectthebehaviorinthisinlinecodetochange.Aguidetothisisthenameofthefunction.AgoodnameshouldmakesenseinplaceofinlinecodeIhave.Ifthenamedoesn’tmakesense,thatmaybebecauseit’sapoorname(inwhichcaseIuseChangeFunctionDeclaration(124)tofixit)orbecausethefunction’spurposeisdifferenttowhatIwantinthiscase—soIshouldn’tcallit.

Ifinditparticularlysatisfyingtodothiswithcallstolibraryfunctions—thatway,Idon’tevenhavetowritethefunctionbody.

Mechanics

Replacetheinlinecodewithacalltotheexistingfunction.

Test.

SlideStatements

formerly:ConsolidateDuplicateConditionalFragments

Motivation

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

Usually,Imoverelatedcodetogetherasapreparatorystepforanother

refactoring,oftenanExtractFunction(106).Puttingrelatedcodeintoaclearlyseparatedfunctionisabetterseparationthanjustmovingasetoflinestogether,butIcan’tdotheExtractFunction(106)unlessthecodeistogetherinthefirstplace.

Mechanics

Identifythetargetpositiontomovefragmentto.Examinestatementsbetweensourceandtargettoseeifthereisinterferenceforthecandidatefragment.Abandonactionifthereisanyinterference.

Afragmentcannotslidebackwardsearlierthananyelementitreferencesisdeclared.

Afragmentcannotslideforwardsbeyondanyelementthatreferencesit.

Afragmentcannotslideoveranystatementthatmodifiesanelementitreferences.

Afragmentthatmodifiesanelementcannotslideoveranyotherelementthatreferencesthemodifiedelement.

Cutfragmentfromthesourceandpasteintothetargetposition.

Test.

Ifthetestfails,trybreakingdowntheslideintosmallersteps.Eitherslideoverlesscodeorreducetheamountofcodeinthefragmentyou’removing.

Example

Whenslidingcodefragments,therearetwodecisionsinvolved:whatslideI’dliketodoandwhetherIcandoit.Thefirstdecisionisverycontext-specific.Onthesimplestlevel,IliketodeclareelementsclosetowhereIusethem,soI’lloftenslideadeclarationdowntoitsusage.ButalmostalwaysIslidesomecodebecauseIwanttodoanotherrefactoring—perhapstogetaclumpofcodetogethertoExtractFunction(106).

OnceIhaveasenseofwhereI’dliketomovesomecode,thenextpartis

decidingifIcandoit.ThisinvolveslookingatthecodeI’mslidingandthecodeI’mslidingover:Dotheyinterferewitheachotherinawaythatwouldchangetheobservablebehavioroftheprogram?

Considerthefollowingfragmentofcode.

1constpricingPlan=retrievePricingPlan();

2constorder=retreiveOrder();

3constbaseCharge=pricingPlan.base;

4letcharge;

5constchargePerUnit=pricingPlan.unit;

6constunits=order.units;

7letdiscount;

8charge=baseCharge+units*chargePerUnit;

9letdiscountableUnits=Math.max(units-pricingPlan.discountThreshold,0);

10discount=discountableUnits*pricingPlan.discountFactor;

11if(order.isRepeat)discount+=20;

12charge=charge-discount;

13chargeOrder(charge);

Thefirstsevenlinesaredeclarations,andit’srelativelyeasytomovethese.Forexample,Imaywanttomoveallthecodedealingwithdiscountstogether,whichwouldinvolvemovingline7(`letdiscount`)toaboveline10(`discount=…`).Sinceadeclarationhasnosideeffectsandreferstonoothervariable,Icansafelymovethisforwardsasfarasthefirstlinethatreferencesdiscountitself.Thisisalsoacommonmove—ifIwanttouseExtractFunction(106)onthediscountlogic,I’llneedtomovethedeclarationdownfirst.

Idosimilaranalysiswithanycodethatdoesn’thaveside-effects.SoIcantakeline2(`constorder=…`)andmoveitdowntoaboveline6(`constunits=…`withouttrouble.

Inthiscase,I’malsohelpedbythefactthatthecodeI’mmovingoverdoesn’thavesideeffectseither.Indeed,Icanfreelyrearrangecodethatlackssideeffectstomyheart’scontent,whichisoneofthereasonswhywiseprogrammersprefertouseside-effect-freecodeasmuchaspossible.

Thereisawrinklehere,however.HowdoIknowthatline2isside-effect-free?Tobesure,I’dneedtolookinside(retrieveOrder())toensuretherearenosideeffectsthere(andinsideanyfunctionsitcalls,andinsideanyfunctionsitsfunctionscall,andsoon).Inpractice,whenworkingonmyowncode,IknowthatIgenerallyfollowtheCommand-QuerySeparation

(https://martinfowler.com/bliki/Command-QuerySeparation.html)principle,soanyfunctionthatreturnsavalueisfreeofsideeffects.ButIcanonlybeconfidentofthatbecauseIknowthecodebase;ifIwereworkinginanunknowncodebase,I’dhavetobemorecautious.ButIdotrytofollowtheCommand-QuerySeparationinmyowncodebecauseit’ssovaluabletoknowthatcodeisfreeofsideeffects.

Whenslidingcodethathasasideeffect,orslidingovercodewithsideeffects,Ihavetobemuchmorecareful.WhatI’mlookingforisinterferencebetweenthetwocodefragments.So,let’ssayIwanttoslideline11(`if(order.isRepeat)`…)downtotheend.I’mpreventedfromdoingthatbyline12becauseitreferencesthevariablewhosestateI’mchanginginline11.Similarly,Ican’ttakeline13(`chargeOrder(charge)`)andmoveitupbecauseline12modifiessomestatethatline13references.However,Icanslideline8(`charge=baseCharge+…`)overlines9–11becausetheretheydon’tmodifyanycommonstate.

ThemoststraightforwardruletofollowisthatIcan’tslideonefragmentofcodeoveranotherifanydatathatbothfragmentsrefertoismodifiedbyeitherone.Butthat’snotacomprehensiverule;Icanhappilyslideeitherofthefollowingtwolinesovertheother.

a=a+10;

a=a+5;

ButjudgingwhetheraslideissafemeansIhavetoreallyunderstandtheoperationsinvolvedandhowtheycompose.

SinceIneedtoworrysomuchaboutupdatingstate,IlooktoremoveasmuchofitasIcan.Sowiththiscode,I’dbelookingtoapplySplitVariable(240)onchargebeforeIindulgeinanyslidingaroundofthatcode.

Here,theanalysisisrelativelysimplebecauseI’mmostlyjustmodifyinglocalvariables.Withmorecomplexdatastructures,it’smuchhardertobesurewhenIgetinterference.Sotestsplayanimportantrole:Slidethefragment,runtests,seeifthingsbreak.Ifmytestcoverageisgood,Icanfeelhappywiththerefactoring.Butiftestsaren’treliable,Ineedtobemorewary—or,morelikely,toimprovethetestsforthecodeI’mworkingon.

Themostimportantconsequenceofatestfailureafteraslideistousesmaller

slides:Insteadofslidingovertenlines,I’lljustpickfive,orslideuptowhatIreckonisadangerousline.Itmayalsomeanthattheslideisn’tworthit,andIneedtoworkonsomethingelsefirst.

Example:Slidingwithconditionals

Icanalsodoslideswithconditionals.ThiswilleitherinvolveremovingduplicatelogicwhenIslideoutofaconditional,oraddingduplicatelogicwhenIslidein.

Here’sacasewhereIhavethesamestatementsinbothlegsofaconditional.

letresult;

if(availableResources.length===0){

result=createResource();

allocatedResources.push(result);

}else{

result=availableResources.pop();

allocatedResources.push(result);

}

returnresult;

Icanslidetheseoutoftheconditional,inwhichcasetheyturnintoasinglestatementoutsideoftheconditionalblock.

letresult;

if(availableResources.length===0){

result=createResource();

}else{

result=availableResources.pop();

}

allocatedResources.push(result);

returnresult;

Inthereversecase,slidingafragmentintoaconditionalmeansrepeatingitineverylegoftheconditional.

FurtherReading

I’veseenanalmostidenticalrefactoringunderthenameofSwapStatement(https://www.industriallogic.com/blog/swap-statement-refactoring/).SwapStatementmovesadjacentfragments,butitonlyworkswithsingle-

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

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

SplitLoop

Motivation

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

Splittingaloopcanalsomakeiteasiertouse.Aloopthatcalculatesasinglevaluecanjustreturnthatvalue.Loopsthatdomanythingsneedtoreturnstructuresorpopulatelocalvariables.IfrequentlyfollowasequenceofSplitLoopfollowedbyExtractFunction(106).

Manyprogrammersareuncomfortablewiththisrefactoring,asitforcesyoutoexecutethelooptwice.Myreminder,asusual,istoseparaterefactoringfromoptimization(RefactoringandPerformance,p.62).OnceIhavemycodeclear,I’lloptimizeit,andifthelooptraversalisabottleneck,it’seasytoslamtheloopsbacktogether.Buttheactualiterationthroughevenalargelistisrarelyabottleneck,andsplittingtheloopsoftenenablesother,morepowerful,optimizations.

Mechanics

Copytheloop.

Identifyandeliminateduplicatesideeffects.

Test.

Whendone,considerExtractFunction(106)oneachloop.

Example

I’llstartwithalittlebitofcodethatcalculatesthetotalsalaryandyoungestage.

letyoungest=people[0]?people[0].age:Infinity;

lettotalSalary=0;

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

totalSalary+=p.salary;

}

return`youngestAge:${youngest},totalSalary:${totalSalary}`;

It’saverysimpleloop,butit’sdoingtwodifferentcalculations.Tosplitthem,Ibeginwithjustcopyingtheloop.

letyoungest=people[0]?people[0].age:Infinity;

lettotalSalary=0;

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

totalSalary+=p.salary;

}

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

totalSalary+=p.salary;

}

return`youngestAge:${youngest},totalSalary:${totalSalary}`;

Withtheloopcopied,Ineedtoremovetheduplicationthatwouldotherwiseresultinwrongresults.Ifsomethingintheloophasnosideeffects,Icanleaveittherefornow,butit’snotthecasewiththisexample.

letyoungest=people[0]?people[0].age:Infinity;

lettotalSalary=0;

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

totalSalary+=p.salary;

}

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

totalSalary+=p.salary;

}

return`youngestAge:${youngest},totalSalary:${totalSalary}`;

Officially,that’stheendoftheSplitLooprefactoring.ButthepointofSplitLoopisn’twhatitdoesonitsownbutwhatitsetsupforthenextmove—andI’musuallylookingtoextracttheloopsintotheirownfunctions.I’lluseSlideStatements(221)toreorganizethecodeabitfirst.

lettotalSalary=0;

for(constpofpeople){

totalSalary+=p.salary;

}

letyoungest=people[0]?people[0].age:Infinity;

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

}

return`youngestAge:${youngest},totalSalary:${totalSalary}`;

ThenIdoacoupleofExtractFunction(106)

return`youngestAge:${youngestAge()},totalSalary:${totalSalary()}`;

functiontotalSalary(){

lettotalSalary=0;

for(constpofpeople){

totalSalary+=p.salary;

}

returntotalSalary;

}

functionyoungestAge(){

letyoungest=people[0]?people[0].age:Infinity;

for(constpofpeople){

if(p.age<youngest)youngest=p.age;

}

returnyoungest;

}

IcanrarelyresistReplaceLoopwithPipeline(230)forthetotalsalary,andthere’sanobviousSubstituteAlgorithm(193)fortheyoungestage.

return`youngestAge:${youngestAge()},totalSalary:${totalSalary()}`;

functiontotalSalary(){

returnpeople.reduce((total,p)=>total+p.salary,0);

}

functionyoungestAge(){

returnMath.min(...people.map(p=>p.age));

}

ReplaceLoopwithPipeline

Motivation

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

Mechanics

Createanewvariablefortheloop’scollection.

Thismaybeasimplecopyofanexistingvariable.

Startingatthetop,takeeachbitofbehaviorintheloopandreplaceitwitha

collectionpipelineoperationinthederivationoftheloopcollectionvariable.Testaftereachchange.

Onceallbehaviorisremovedfromtheloop,removeit.

Ifitassignstoanaccumulator,assignthepipelineresulttotheaccumulator.

Example

I’llbeginwithsomedata:aCSVfileofdataaboutouroffices.

office,country,telephone

Chicago,USA,+13123731000

Beijing,China,+864008900505

Bangalore,India,+918040649570

PortoAlegre,Brazil,+555130793550

Chennai,India,+914466044766

...(moredatafollows)

ThefollowingfunctionpicksouttheofficesinIndiaandreturnstheircitiesandtelephonenumbers.

functionacquireData(input){

constlines=input.split("\n");

letfirstLine=true;

constresult=[];

for(constlineoflines){

if(firstLine){

firstLine=false;

continue;

}

if(line.trim()==="")continue;

constrecord=line.split(",");

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

Iwanttoreplacethatloopwithacollectionpipeline.

Myfirststepistocreateaseparatevariableforthelooptoworkover.

functionacquireData(input){

constlines=input.split("\n");

letfirstLine=true;

constresult=[];

constloopItems=lines

for(constlineofloopItems){

if(firstLine){

firstLine=false;

continue;

}

if(line.trim()==="")continue;

constrecord=line.split(",");

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

ThefirstpartoftheloopisallaboutskippingthefirstlineoftheCSVfile.Thiscallsforaslice,soIremovethatfirstsectionoftheloopandaddasliceoperationtotheformationoftheloopvariable.

functionacquireData(input){

constlines=input.split("\n");

letfirstLine=true;

constresult=[];

constloopItems=lines

.slice(1);

for(constlineofloopItems){

if(firstLine){

firstLine=false;

continue;

}

if(line.trim()==="")continue;

constrecord=line.split(",");

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

Asabonus,thisletsmedeletefirstLine—andIparticularlyenjoydeletingcontrolvariables.

Thenextbitofbehaviorremovesanyblanklines.Icanreplacethiswithafilter

operation.

functionacquireData(input){

constlines=input.split("\n");

constresult=[];

constloopItems=lines

.slice(1)

.filter(line=>line.trim()!=="")

;

for(constlineofloopItems){

if(line.trim()==="")continue;

constrecord=line.split(",");

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

Whenwritingapipeline,Ifinditbesttoputtheterminalsemicolononitsownline.

Iusethemapoperationtoturnlinesintoanarrayofstrings—misleadinglycalledrecordintheoriginalfunction,butit’ssafertokeepthenamefornowandrenamelater.

functionacquireData(input){

constlines=input.split("\n");

constresult=[];

constloopItems=lines

.slice(1)

.filter(line=>line.trim()!=="")

.map(line=>line.split(","))

;

for(constlineofloopItems){

constrecord=line;.split(",");

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

FilteragaintojustgettheIndiarecords.

functionacquireData(input){

constlines=input.split("\n");

constresult=[];

constloopItems=lines

.slice(1)

.filter(line=>line.trim()!=="")

.map(line=>line.split(","))

.filter(record=>record[1].trim()==="India")

;

for(constlineofloopItems){

constrecord=line;

if(record[1].trim()==="India"){

result.push({city:record[0].trim(),phone:record[2].trim()});

}

}

returnresult;

}

Maptotheoutputrecordform.

functionacquireData(input){

constlines=input.split("\n");

constresult=[];

constloopItems=lines

.slice(1)

.filter(line=>line.trim()!=="")

.map(line=>line.split(","))

.filter(record=>record[1].trim()==="India")

.map(record=>({city:record[0].trim(),phone:record[2].trim()}))

;

for(constlineofloopItems){

constrecord=line;

result.push(line);

}

returnresult;

}

Now,alltheloopdoesisassignvaluestotheaccumulator.SoIcanremoveitandassigntheresultofthepipelinetotheaccumulator:

functionacquireData(input){

constlines=input.split("\n");

constresult=lines

.slice(1)

.filter(line=>line.trim()!=="")

.map(line=>line.split(","))

.filter(record=>record[1].trim()==="India")

.map(record=>({city:record[0].trim(),phone:record[2].trim()}))

;

for(constlineofloopItems){

constrecord=line;

result.push(line);

}

returnresult;

}

That’sthecoreoftherefactoring.ButIdohavesomecleanupI’dliketodo.Iinlinedresult,renamedsomelambdavariables,andmadethelayoutreadmorelikeatable.

functionacquireData(input){

constlines=input.split("\n");

returnlines

.slice(1)

.filter(line=>line.trim()!=="")

.map(line=>line.split(","))

.filter(fields=>fields[1].trim()==="India")

.map(fields=>({city:fields[0].trim(),phone:fields[2].trim()}))

;

}

Ithoughtaboutinlininglinestoo,butfeltthatitspresenceexplainswhat’shappening.

FurtherReading

Formoreexamplesonturningloopsintopipelines,seemyessayRefactoringwithLoopsandCollectionPipelines[bib-loop-pipe-article].

RemoveDeadCode

Motivation

Whenweputcodeintoproduction,evenonpeople’sdevices,wearen’tchargedbyweight.Afewunusedlinesofcodedon’tslowdownoursystemsnortakeupsignificantmemory;indeed,decentcompilerswillinstinctivelyremovethem.Butunusedcodeisstillasignificantburdenwhentryingtounderstandhowthesoftwareworks.Itdoesn’tcarryanywarningsignstellingprogrammersthattheycanignorethisfunctionasit’snevercalledanymore,sotheystillhavetospendtimeunderstandingwhatit’sdoingandwhychangingitdoesn’tseemtoaltertheoutputastheyexpected.

Oncecodeisn’tusedanymore,weshoulddeleteit.Idon’tworrythatImayneeditsometimeinthefuture;shouldthathappen,IhavemyversioncontrolsystemsoIcanalwaysdigitoutagain.Ifit’ssomethingIreallythinkImayneedoneday,Imightputacommentintothecodethatmentionsthelostcodeandwhichrevisionitwasremovedin—but,honestly,Ican’trememberthelasttimeIdidthat,orregrettedthatIhadn’tdoneit.

Commentingoutdeadcodewasonceacommonhabit.Thiswasusefulinthedaysbeforeversioncontrolsystemswerewidelyused,orwhentheywereinconvenient.Now,whenIcanputeventhesmallestcodebaseunderversioncontrol,that’snolongerneeded.

Mechanics

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

Removethedeadcode.

Test.

Chapter9OrganizingDataDatastructuresplayanimportantroleinourprograms,soit’snogreatshockthatIhaveaclutchofrefactoringsthatfocusonthem.Avaluethat’susedfordifferentpurposesisabreedinggroundforconfusionandbugs—so,whenIseeone,IuseSplitVariable(240)toseparatetheusages.Aswithanyprogramelement,gettingavariable’snamerightistrickyandimportant,soRenameVariable(137)isoftenmyfriend.ButsometimesthebestthingIcandowithavariableistogetridofitcompletely—withReplaceDerivedVariablewithQuery(248).

Ioftenfindproblemsinacodebaseduetoaconfusionbetweenreferencesandvalues,soIuseChangeReferencetoValue(252)andChangeValuetoReference(256)tochangebetweenthesestyles.

SplitVariable

formerly:RemoveAssignmentstoParameters

formerly:SplitTemp

Motivation

Variableshavevarioususes.Someoftheseusesnaturallyleadtothevariablebeingassignedtoseveraltimes.Loopvariableschangeforeachrunofaloop(suchastheiinfor(leti=0;i<10;i++)).Collectingvariablesstoreavaluethatisbuiltupduringthemethod.

Manyothervariablesareusedtoholdtheresultofalong-windedbitofcodeforeasyreferencelater.Thesekindsofvariablesshouldbesetonlyonce.Iftheyaresetmorethanonce,itisasignthattheyhavemorethanoneresponsibilitywithinthemethod.Anyvariablewithmorethanoneresponsibilityshouldbereplacedwithmultiplevariables,oneforeachresponsibility.Usingavariablefortwodifferentthingsisveryconfusingforthereader.

Mechanics

Changethenameofthevariableatitsdeclarationandfirstassignment.

Ifthelaterassignmentsareoftheformi=i+something,thatisacollectingvariable,sodon’tsplitit.Acollectingvariableisoftenusedforcalculatingsums,stringconcatenation,writingtoastream,oraddingtoacollection.

Ifpossible,declarethenewvariableasimmutable.

Changeallreferencesofthevariableuptoitssecondassignment.

Test.

Repeatinstages,ateachstagerenamingthevariableatthedeclarationandchangingreferencesuntilthenextassignment,untilyoureachthefinalassignment.

Example

Forthisexample,Icomputethedistancetraveledbyahaggis.Fromastanding

start,ahaggisexperiencesaninitialforce.Afteradelay,asecondaryforcekicksintofurtheracceleratethehaggis.Usingthecommonlawsofmotion,Icancomputethedistancetraveledasfollows:

functiondistanceTravelled(scenario,time){

letresult;

letacc=scenario.primaryForce/scenario.mass;

letprimaryTime=Math.min(time,scenario.delay);

result=0.5*acc*primaryTime*primaryTime;

letsecondaryTime=time-scenario.delay;

if(secondaryTime>0){

letprimaryVelocity=acc*scenario.delay;

acc=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;

result+=primaryVelocity*secondaryTime+0.5*acc*secondaryTime*secondaryTime;

}

returnresult;

}

Aniceawkwardlittlefunction.Theinterestingthingforourexampleisthewaythevariableaccissettwice.Ithastworesponsibilities:onetoholdtheinitialaccelerationfromthefirstforceandanotherlatertoholdtheaccelerationfrombothforces.Iwanttosplitthisvariable.

Whentryingtounderstandhowavariableisused,it’shandyifmyeditorcanhighlightalloccurrencesofasymbolwithinafunctionorfile.Mostmoderneditorscandothisprettyeasily.

Istartatthebeginningbychangingthenameofthevariableanddeclaringthenewnameasconst.Then,Ichangeallreferencestothevariablefromthatpointuptothenextassignment.Atthenextassignment,Ideclareit:

functiondistanceTravelled(scenario,time){

letresult;

constprimaryAcceleration=scenario.primaryForce/scenario.mass;

letprimaryTime=Math.min(time,scenario.delay);

result=0.5*primaryAcceleration*primaryTime*primaryTime;

letsecondaryTime=time-scenario.delay;

if(secondaryTime>0){

letprimaryVelocity=primaryAcceleration*scenario.delay;

letacc=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;

result+=primaryVelocity*secondaryTime+0.5*acc*secondaryTime*secondaryTime;

}

returnresult;

}

Ichoosethenewnametorepresentonlythefirstuseofthevariable.Imakeitconsttoensureitisonlyassignedonce.Icanthendeclaretheoriginalvariableatitssecondassignment.NowIcancompileandtest,andallshouldwork.

Icontinueonthesecondassignmentofthevariable.Thisremovestheoriginalvariablenamecompletely,replacingitwithanewvariablenamedfortheseconduse.

functiondistanceTravelled(scenario,time){

letresult;

constprimaryAcceleration=scenario.primaryForce/scenario.mass;

letprimaryTime=Math.min(time,scenario.delay);

result=0.5*primaryAcceleration*primaryTime*primaryTime;

letsecondaryTime=time-scenario.delay;

if(secondaryTime>0){

letprimaryVelocity=primaryAcceleration*scenario.delay;

constsecondaryAcceleration=(scenario.primaryForce+scenario.secondaryForce)/scenario.mass;

result+=primaryVelocity*secondaryTime+

0.5*secondaryAcceleration*secondaryTime*secondaryTime;

}

returnresult;

}

I’msureyoucanthinkofalotmorerefactoringtobedonehere.Enjoyit.(I’msureit’sbetterthaneatingthehaggis—doyouknowwhattheyputinthosethings?)

Example:AssigningtoanInputParameter

Anothercaseofsplittingavariableiswherethevariableisdeclaredasaninputparameter.Considersomethinglike

functiondiscount(inputValue,quantity){

if(inputValue>50)inputValue=inputValue-2;

if(quantity>100)inputValue=inputValue-1;

returninputValue;

}

HereinputValueisusedbothtosupplyaninputtothefunctionandtoholdtheresultforthecaller.(SinceJavaScripthascall-by-valueparameters,anymodificationofinputValueisn’tseenbythecaller.)

Inthissituation,Iwouldsplitthatvariable.

functiondiscount(originalInputValue,quantity){

letinputValue=originalInputValue;

if(inputValue>50)inputValue=inputValue-2;

if(quantity>100)inputValue=inputValue-1;

returninputValue;

}

IthenperformRenameVariable(137)twicetogetbetternames.

functiondiscount(inputValue,quantity){

letresult=inputValue;

if(inputValue>50)result=result-2;

if(quantity>100)result=result-1;

returnresult;

}

You’llnoticethatIchangedthesecondlinetouseinputValueasitsdatasource.Althoughthetwoarethesame,Ithinkthatlineisreallyaboutapplyingthemodificationtotheresultvaluebasedontheoriginalinputvalue,notthe(coincidentallysame)valueoftheresultaccumulator.

RenameField

Motivation

Namesareimportant,andfieldnamesinrecordstructurescanbeespeciallyimportantwhenthoserecordstructuresarewidelyusedacrossaprogram.Datastructuresplayaparticularlyimportantroleinunderstanding.ManyyearsagoFredBrookssaid,“Showmeyourflowchartsandconcealyourtables,andIshallcontinuetobemystified.Showmeyourtables,andIwon’tusuallyneedyourflowcharts;they’llbeobvious.”WhileIdon’tseemanypeopledrawingflowchartsthesedays,theadageremainsvalid.Datastructuresarethekeytounderstandingwhat’sgoingon.

Sincethesedatastructuresaresoimportant,it’sessentialtokeepthemclear.Likeanythingelse,myunderstandingofdataimprovesthemoreIworkonthesoftware,soit’svitalthatthisimprovedunderstandingisembeddedintotheprogram.

Youmaywanttorenameafieldinarecordstructure,buttheideaalsoappliestoclasses.Getterandsettermethodsformaneffectivefieldforusersoftheclass.Renamingthemisjustasimportantaswithbarerecordstructures.

Mechanics

Iftherecordhaslimitedscope,renameallaccessestothefieldandtest;noneedtodotherestofthemechanics.

Iftherecordisn’talreadyencapsulated,applyEncapsulateRecord(160).

Renametheprivatefieldinsidetheobject,adjustinternalmethodstofit.

Test.

Iftheconstructorusesthename,applyChangeFunctionDeclaration(124)torenameit.

ApplyChangeFunctionDeclaration(124)totheaccessors.

Example:RenamingaField

I’llstartwithaconstant.

constorganization={name:"AcmeGooseberries",country:"GB"};

Iwanttochange“name”to“title”.Theobjectiswidelyusedinthecodebase,andthereareupdatestothetitleinthecode.SomyfirstmoveistoapplyEncapsulateRecord(160)

classOrganization{

constructor(data){

this._name=data.name;

this._country=data.country;

}

getname(){returnthis._name;}

setname(aString){this._name=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

constorganization=newOrganization({name:"AcmeGooseberries",country:"GB"});

NowthatI’veencapsulatedtherecordstructureintotheclass,therearefourplacesIneedtolookatforrenaming:thegettingfunction,thesettingfunction,theconstructor,andtheinternaldatastructure.WhilethatmaysoundlikeI’veincreasedmyworkload,itactuallymakesmyworkeasiersinceIcannowchangetheseindependentlyinsteadofallatonce,takingsmallersteps.Smallerstepsmeanfewerthingstogowrongineachstep—therefore,lesswork.Itwouldn’tbelessworkifInevermademistakes—butnotmakingmistakesisafantasyIgaveuponalongtimeago.

SinceI’vecopiedtheinputdatastructureintotheinternaldatastructure,IneedtoseparatethemsoIcanworkonthemindependently.Icandothisbydefiningaseparatefieldandadjustingtheconstructorandaccessorstouseit.

classOrganization…

classOrganization{

constructor(data){

this._title=data.name;

this._country=data.country;

}

getname(){returnthis._title;}

setname(aString){this._title=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

Next,Iaddsupportforusing“title”intheconstructor.

classOrganization…

classOrganization{

constructor(data){

this._title=(data.title!==undefined)?data.title:data.name;

this._country=data.country;

}

getname(){returnthis._title;}

setname(aString){this._title=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

Now,callersofmyconstructorcanuseeithernameortitle(withtitletakingprecedence).Icannowgothroughallconstructorcallersandchangethemoneby-onetousethenewname.

constorganization=newOrganization({title:"AcmeGooseberries",country:"GB"});

OnceI’vedoneallofthem,Icanremovethesupportforthename.

classOrganization…

classOrganization{

constructor(data){

this._title=data.title;

this._country=data.country;

}

getname(){returnthis._title;}

setname(aString){this._title=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

Nowthattheconstructoranddatausethenewname,Icanchangetheaccessors,whichisassimpleasapplyingChangeFunctionDeclaration(124)toeachone.

classOrganization…

classOrganization{

constructor(data){

this._title=data.title;

this._country=data.country;

}

gettitle(){returnthis._title;}

settitle(aString){this._title=aString;}

getcountry(){returnthis._country;}

setcountry(aCountryCode){this._country=aCountryCode;}

}

I’veshownthisprocessinitsmostheavyweightformneededforawidelyuseddatastructure.Ifit’sbeingusedonlylocally,asinasinglefunction,Icanprobablyjustrenamethevariouspropertiesinonegowithoutdoingencapsulation.It’samatterofjudgmentwhentoapplytothefullmechanicshere—but,asusualwithrefactoring,ifmytestsbreak,that’sasignIneedtousethemoregradualprocedure.

Somelanguagesallowmetomakeadatastructureimmutable.Inthiscase,ratherthanencapsulatingit,Icancopythevaluetothenewname,graduallychangetheusers,thenremovetheoldname.Duplicatingdataisarecipefordisasterwithmutabledatastructures;removingsuchdisastersiswhyimmutabledataissopopular.

ReplaceDerivedVariablewithQuery

Motivation

Oneofthebiggestsourcesofproblemsinsoftwareismutabledata.Datachangescanoftencoupletogetherpartsofcodeinawkwardways,withchangesinonepartofleadingtoknock-oneffectsthatarehardtospot.Inmanysituationsit’snotrealistictoentirelyremovemutabledata—butIdoadvocateminimizingthescopeofmutabledataatmuchaspossible.

OnewayIcanmakeabigimpactisbyremovinganyvariablesthatIcouldjustaseasilycalculate.Acalculationoftenmakesitclearerwhatthemeaningofthedatais,anditisprotectedfrombeingcorruptedwhenyoufailtoupdatethevariableasthesourcedatachanges.

Areasonableexceptiontothisiswhenthesourcedataforthecalculationisimmutableandwecanforcetheresulttobeingimmutabletoo.Transformationoperationsthatcreatenewdatastructuresarethusreasonabletokeepeveniftheycouldbereplacedwithcalculations.Indeed,thereisadualityherebetweenobjectsthatwrapadatastructurewithaseriesofcalculatedpropertiesandfunctionsthattransformonedatastructureintoanother.Theobjectrouteisclearlybetterwhenthesourcedatachangesandyouwouldhavetomanagethelifetimeofthederiveddatastructures.Butifthesourcedataisimmutable,orthederiveddataisverytransient,thenbothapproachesareeffective.

Mechanics

Identifyallpointsofupdateforthevariable.Ifnecessary,useSplitVariable(240)toseparateeachpointofupdate.

Createafunctionthatcalculatesthevalueofthevariable.

UseIntroduceAssertion(299)toassertthatthevariableandthecalculationgivethesameresultwheneverthevariableisused.

Ifnecessary,useEncapsulateVariable(132)toprovideahomefortheassertion.

Test.

Replaceanyreaderofthevariablewithacalltothenewfunction.

Test.

ApplyRemoveDeadCode(236)tothedeclarationandupdatestothevariable.

Example

Here’sasmallbutperfectlyformedexampleofugliness.

classProductionPlan…

getproduction(){returnthis._production;}

applyAdjustment(anAdjustment){

this._adjustments.push(anAdjustment);

this._production+=anAdjustment.amount;

}

Uglinessisintheeyeofbeholder;here,Iseeuglinessinduplication—notthecommonduplicationofcodebutduplicationofdata.WhenIapplyanadjustment,I’mnotjuststoringthatadjustmentbutalsousingittomodifyanaccumulator.Icanjustcalculatethatvalue,withouthavingtoupdateit.

ButI’macautiousfellow.ItismyhypothesisisthatIcanjustcalculateit—IcantestthathypothesisbyusingIntroduceAssertion(299):

classProductionPlan…

getproduction(){

assert(this._production===this.calculatedProduction);

returnthis._production;

}

getcalculatedProduction(){

returnthis._adjustments

.reduce((sum,a)=>sum+a.amount,0);

}

Withtheassertioninplace,Irunmytests.Iftheassertiondoesn’tfail,Icanreplacereturningthefieldwithreturningthecalculation:

classProductionPlan…

getproduction(){

assert(this._production===this.calculatedProduction);

returnthis.calculatedProduction;

}

ThenInlineFunction(115):

classProductionPlan…

getproduction(){

returnthis._adjustments

.reduce((sum,a)=>sum+a.amount,0);

}

IcleanupanyreferencestotheoldvariablewithRemoveDeadCode(236):

classProductionPlan…

applyAdjustment(anAdjustment){

this._adjustments.push(anAdjustment);

this._production+=anAdjustment.amount;

}

Example:MoreThanOneSource

Theaboveexampleisniceandeasybecausethere’sclearlyasinglesourceforthevalueofproduction.Butsometimes,morethanoneelementcancombineintheaccumulator.

classProductionPlan…

constructor(production){

this._production=production;

this._adjustments=[];

}

getproduction(){returnthis._production;}

applyAdjustment(anAdjustment){

this._adjustments.push(anAdjustment);

this._production+=anAdjustment.amount;

}

IfIdothesameIntroduceAssertion(299)thatIdidabove,itwillnowfailforanycasewheretheinitialvalueoftheproductionisn’tzero.

ButIcanstillreplacethederiveddata.TheonlydifferenceisthatImustfirstapplySplitVariable(240).

constructor(production){

this._initialProduction=production;

this._productionAccumulator=0;

this._adjustments=[];

}

getproduction(){

returnthis._initialProduction+this._productionAccumulator;

}

NowIcanIntroduceAssertion(299)

classProductionPlan…

getproduction(){

assert(this._productionAccumulator===this.calculatedProductionAccumulator);

returnthis._initialProduction+this._productionAccumulator;

}

getcalculatedProductionAccumulator(){

returnthis._adjustments

.reduce((sum,a)=>sum+a.amount,0);

}

Andcontinueprettymuchasbefore.I’dbeinclined,however,toleavetotalProductionAjustmentsasitsownproperty,withoutinliningit.

ChangeReferencetoValue

inverseof:ChangeValuetoReference(256)

Motivation

WhenInestanobject,ordatastructure,withinanotherIcantreattheinnerobjectasareferenceorasavalue.ThedifferenceismostobviouslyvisibleinhowIhandleupdatesoftheinnerobject’sproperties.IfItreatitasareference,I’llupdatetheinnerobject’spropertykeepingthesameinnerobject.IfItreatitasavalue,Iwillreplacetheentireinnerobjectwithanewonethathasthedesiredproperty.

IfItreatafieldasavalue,IcanchangetheclassoftheinnerobjecttomakeitaValueObject(https://martinfowler.com/bliki/ValueObject.html).Valueobjectsaregenerallyeasiertoreasonabout,particularlybecausetheyareimmutable.Ingeneral,immutabledatastructuresareeasiertodealwith.Icanpassanimmutabledatavalueouttootherpartsoftheprogramandnotworrythatitmightchangewithouttheenclosingobjectbeingawareofthechange.Icanreplicatevaluesaroundmyprogramandnotworryaboutmaintainingmemorylinks.Valueobjectsareespeciallyusefulindistributedandconcurrentsystems.

ThisalsosuggestswhenIshouldn’tdothisrefactoring.IfIwanttoshareanobjectbetweenseveralobjectssothatanychangetothesharedobjectisvisibletoallitscollaborators,thenIneedthesharedobjecttobeareference.

Mechanics

Checkthatthecandidateclassisimmutableorcanbecomeimmutable.

Foreachsetter,applyRemoveSettingMethod(329).

Provideavalue-basedequalitymethodthatusesthefieldsofthevalueobject.

Mostlanguageenvironmentsprovideanoverridableequalityfunctionforthispurpose.Usuallyyoumustoverrideahashcodegeneratormethodaswell.

Example

Imaginewehaveapersonobjectthatholdsontoacrudetelephonenumber.

classPerson…

constructor(){

this._telephoneNumber=newTelephoneNumber();

}

getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}

setofficeAreaCode(arg){this._telephoneNumber.areaCode=arg;}

getofficeNumber(){returnthis._telephoneNumber.number;}

setofficeNumber(arg){this._telephoneNumber.number=arg;}

classTelephoneNumber…

getareaCode(){returnthis._areaCode;}

setareaCode(arg){this._areaCode=arg;}

getnumber(){returnthis._number;}

setnumber(arg){this._number=arg;}

ThissituationistheresultofanExtractClass(180)wheretheoldparentstillholdsupdatemethodsforthenewobject.ThisisagoodtimetoapplyChangeReferencetoValuesincethereisonlyonereferencetothenewclass.

ThefirstthingIneedtodoistomakethetelephonenumberimmutable.IdothisbyapplyingRemoveSettingMethod(329)tothefields.ThefirststepofRemoveSettingMethod(329)istouseChangeFunctionDeclaration(124)toaddthetwofieldstotheconstructorandenhancetheconstructortocallthesetters.

classTelephoneNumber…

constructor(areaCode,number){

this._areaCode=areaCode;

this._number=number;

}

NowIlookatthecallersofthesetters.Foreachone,Ineedtochangeittoare-assignment.Istartwiththeareacode.

classPerson…

getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}

setofficeAreaCode(arg){

this._telephoneNumber=newTelephoneNumber(arg,this.officeNumber);

}

getofficeNumber(){returnthis._telephoneNumber.number;}

setofficeNumber(arg){this._telephoneNumber.number=arg;}

Ithenrepeatthatstepwiththeremainingfield.

classPerson…

getofficeAreaCode(){returnthis._telephoneNumber.areaCode;}

setofficeAreaCode(arg){

this._telephoneNumber=newTelephoneNumber(arg,this.officeNumber);

}

getofficeNumber(){returnthis._telephoneNumber.number;}

setofficeNumber(arg){

this._telephoneNumber=newTelephoneNumber(this.officeAreaCode,arg);

}

Nowthetelephonenumberisimmutable,itisreadytobecomeatruevalue.Thecitizenshiptestforavalueobjectisthatitusesvalue-basedequality.ThisisanareawhereJavaScriptfallsdown,asthereisnothinginthelanguageandcorelibrariesthatunderstandsreplacingareference-basedequalitywithavalue-basedone.ThebestIcandoistocreatemyownequalsmethod.

classTelephoneNumber…

equals(other){

if(!(otherinstanceofTelephoneNumber))returnfalse;

returnthis.areaCode===other.areaCode&&

this.number===other.number;

}

It’salsoimportanttotestitwithsomethinglike

it('telephoneequals',function(){

assert(newTelephoneNumber("312","555-0142")

.equals(newTelephoneNumber("312","555-0142")));

});

TheunusualformattingIusehereshouldmakeitobviousthattheyarethesameconstructorcall.

ThevitalthingIdointhetestiscreatetwoindependentobjectsandtestthattheymatchasequal.

Inmostobject-orientedlanguages,thereisabuilt-inequalitytestthatissupposedtooverriddenforvalue-basedequality.InRuby,Icanoverridethe==operator;inJava,IoverridetheObject.equals()method.AndwheneverIoverrideanequalitymethod,Iusuallyneedtooverrideahashcodegeneratingmethodtoo(e.g.Object.hashCode()inJava)toensurecollectionsthatusehashingworkproperlywithmynewvalue.

Ifthetelephonenumberisusedbymorethanoneclient,theprocedureisstillthesame.AsIapplyRemoveSettingMethod(329),I’llbemodifyingseveralclientsinsteadofjustone.Testsfornon-equaltelephonenumbers,aswellascomparisonstonon-telephone-numbersandnullvalues,arealsoworthwhile.

ChangeValuetoReference

inverseof:ChangeReferencetoValue(252)

Motivation

Adatastructuremayhaveseveralrecordslinkedtothesamelogicaldatastructure.Imightreadinalistoforders,someofwhichareforthesamecustomer.WhenIhavesharinglikethis,Icanrepresentitbytreatingthecustomereitherasavalueorasareference.Withavalue,thecustomerdataiscopiedintoeachorder;withareference,thereisonlyonedatastructurethatmultipleorderslinkto.

Ifthecustomerneverneedstobeupdated,thenbothapproachesarereasonable.Itis,perhaps,abitconfusingtohavemultiplecopiesofthesamedata,butit’scommonenoughtonotbeaproblem.Insomecases,theremaybeissueswithmemoryduetomultiplecopies—but,likeanyperformanceissue,that’srelativelyrare.

ThebiggestdifficultyinhavingphysicalcopiesofthesamelogicaldataoccurswhenIneedtoupdatetheshareddata.Ithenhavetofindallthecopiesandupdatethemall.IfImissone,I’llgetatroublinginconsistencyinmydata.Inthiscase,it’softenworthwhiletochangethecopieddataintoasinglereference.Thatway,anychangeisvisibletoallthecustomer’sorders.

Changingavaluetoareferenceresultsinonlyoneobjectbeingpresentforanentity,anditusuallymeansIneedsomekindofrepositorywhereIcanaccesstheseobjects.Ithenonlycreatetheobjectforanentityonce,andeverywhereelseIretrieveitfromtherepository.

Mechanics

Createarepositoryforinstancesoftherelatedobject(ifoneisn’talreadypresent).

Ensuretheconstructorhasawayoflookingupthecorrectinstanceoftherelatedobject.

Changetheconstructorsforthehostobjecttousetherepositorytoobtaintherelatedobject.Testaftereachchange.

Example

I’llbeginwithaclassthatrepresentsorders,whichImightcreatefromanincomingJSONdocument.PartoftheorderdataisacustomerIDfromwhichI’mcreatingacustomerobject.

classOrder…

constructor(data){

this._number=data.number;

this._customer=newCustomer(data.customer);

//loadotherdata

}

getcustomer(){returnthis._customer;}

classCustomer…

constructor(id){

this._id=id;

}

getid(){returnthis._id;}

ThecustomerobjectIcreatethiswayisavalue.IfIhavefiveordersthatrefertothecustomerIDof123,I’llhavefiveseparatecustomerobjects.AnychangeImaketooneofthemwillnotbereflectedintheothers.ShouldIwanttoenrich

thecustomerobjects,perhapsbygatheringdatafromacustomerservice,I’dhavetoupdateallfivecustomerswiththesamedata.Havingduplicateobjectslikethisalwaysmakesmenervous—it’sconfusingtohavemultipleobjectsrepresentingthesameentity,suchasacustomer.Thisproblemisparticularlyawkwardifthecustomerobjectismutable,whichcanleadtoinconsistenciesbetweenthecustomerobjects.

IfIwanttousethesamecustomerobjecteachtime,I’llneedaplacetostoreit.Exactlywheretostoreentitieslikethiswillvaryfromapplicationtoapplication,butforasimplecaseIliketousearepositoryobject[bib-repository].

let_repositoryData;

exportfunctioninitialize(){

_repositoryData={};

_repositoryData.customers=newMap();

}

exportfunctionregisterCustomer(id){

if(!_repositoryData.customers.has(id))

_repositoryData.customers.set(id,newCustomer(id));

returnfindCustomer(id);

}

exportfunctionfindCustomer(id){

return_repositoryData.customers.get(id);

}

TherepositoryallowsmetoregistercustomerobjectswithanIDandensuresIonlycreateonecustomerobjectwiththesameID.Withthisinplace,Icanchangetheorder’sconstructortouseit.

Often,whendoingthisrefactoring,therepositoryalreadyexists,soIcanjustuseit.

Thenextstepistofigureouthowtheconstructorfortheordercanobtainthecorrectcustomerobject.Inthiscaseit’seasy,sincethecustomer’sIDispresentintheinputdatastream.

classOrder…

constructor(data){

this._number=data.number;

this._customer=registerCustomer(data.customer);

//loadotherdata

}

getcustomer(){returnthis._customer;}

Now,anychangesImaketothecustomerofoneorderwillbesynchronizedacrossalltheorderssharingthesamecustomer.

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

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

Chapter10SimplifyingConditionalLogicMuchofthepowerofprogramscomesfromtheirabilitytoimplementconditionallogic—but,sadly,muchofthecomplexityofprogramsliesintheseconditionals.Ioftenuserefactoringtomakeconditionalsectionseasiertounderstand.IregularlyapplyDecomposeConditional(260)tocomplicatedconditionals,andIuseConsolidateConditionalExpression(263)tomakelogicalcombinationsclearer.IuseReplaceNestedConditionalwithGuardClauses(266)toclarifycaseswhereIwanttorunsomepre-checksbeforemymainprocessing.IfIseeseveralconditionsusingthesameswitchinglogic,it’sagoodtimetopullReplaceConditionalwithPolymorphism(271)outthebox.

Alotofconditionalsareusedtohandlespecialcases,suchasnulls;ifthatlogicismostlythesame,thenIntroduceSpecialCase(287)(oftenreferredtoasIntroduceSpecialCase(287))canremovealotofduplicatecode.And,althoughIliketoremoveconditionsalot,ifIwanttocommunicate(andcheck)aprogram’sstate,IfindIntroduceAssertion(299)aworthwhileaddition.

DecomposeConditional

Motivation

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

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

ThisisreallyjustaparticularcaseofapplyingExtractFunction(106)tomycode,butIliketohighlightthiscaseasonewhereI’veoftenfoundaremarkablygoodvaluefortheexercise.

Mechanics

ApplyExtractFunction(106)ontheconditionandeachlegoftheconditional.

Example

SupposeI’mcalculatingthechargeforsomethingthathasseparateratesforwinterandsummer:

if(!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd))

charge=quantity*plan.summerRate;

else

charge=quantity*plan.regularRate+plan.regularServiceCharge;

Iextracttheconditionintoitsownfunction.

if(summer())

charge=quantity*plan.summerRate;

else

charge=quantity*plan.regularRate+plan.regularServiceCharge;

functionsummer(){

return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);

}

ThenIdothethenleg:

if(summer())

charge=summerCharge();

else

charge=quantity*plan.regularRate+plan.regularServiceCharge;

functionsummer(){

return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);

}

functionsummerCharge(){

returnquantity*plan.summerRate;

}

Finally,theelseleg:

if(summer())

charge=summerCharge();

else

charge=regularCharge();

functionsummer(){

return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);

}

functionsummerCharge(){

returnquantity*plan.summerRate;

}

functionregularCharge(){

returnquantity*plan.regularRate+plan.regularServiceCharge;

}

Withthatdone,Iliketoreformattheconditionalusingtheternaryoperator.

charge=summer()?summerCharge():regularCharge();

functionsummer(){

return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);

}

functionsummerCharge(){

returnquantity*plan.summerRate;

}

functionregularCharge(){

returnquantity*plan.regularRate+plan.regularServiceCharge;

}

ConsolidateConditionalExpression

Motivation

Sometimes,Irunintoaseriesofconditionalcheckswhereeachcheckisdifferentyettheresultingactionisthesame.WhenIseethis,Iuseandandoroperatorstoconsolidatethemintoasingleconditionalcheckwithasingleresult.

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

Thereasonsinfavorofconsolidatingconditionalsalsopointtothereasonsagainstdoingit.IfIconsiderittobetrulyindependentchecksthatshouldn’tbethoughtofasasinglecheck,Idon’tdotherefactoring.

Mechanics

Ensurethatnoneoftheconditionalshaveanysideeffects.

Ifanydo,useSeparateQueryfromModifier(304)onthemfirst.

Taketwooftheconditionalstatementsandcombinetheirconditionsusingalogicaloperator.

Sequencescombinewithor,nestedifstatementscombinewithand.

Test.

Repeatcombiningconditionalsuntiltheyareallinasinglecondition.

ConsiderusingExtractFunction(106)ontheresultingcondition.

Example

Perusingsomecode,Iseethefollowing:

functiondisabilityAmount(anEmployee){

if(anEmployee.seniority<2)return0;

if(anEmployee.monthsDisabled>12)return0;

if(anEmployee.isPartTime)return0;

//computethedisabilityamount

It’sasequenceofconditionalcheckswhichallhavethesameresult.Sincetheresultisthesame,Ishouldcombinetheseconditionsintoasingleexpression.Forasequencelikethis,Idoitusinganoroperator.

functiondisabilityAmount(anEmployee){

if((anEmployee.seniority<2)

||(anEmployee.monthsDisabled>12))return0;

if(anEmployee.isPartTime)return0;

//computethedisabilityamount

Itest,thenfoldintheothercondition:

functiondisabilityAmount(anEmployee){

if((anEmployee.seniority<2)

||(anEmployee.monthsDisabled>12)

||(anEmployee.isPartTime))return0;

//computethedisabilityamount

OnceIhavethemalltogether,IuseExtractFunction(106)onthecondition.

functiondisabilityAmount(anEmployee){

if(isNotEligableForDisability())return0;

//computethedisabilityamount

functionisNotEligableForDisability(){

return((anEmployee.seniority<2)

||(anEmployee.monthsDisabled>12)

||(anEmployee.isPartTime));

}

Example:Usingands

Theexampleaboveshowedcombiningstatementswithanor,butImayrunintocasesthatneedandsaswell.Suchacaseusesnestedifstatements:

if(anEmployee.onVacation)

if(anEmployee.seniority>10)

return1;

return0.5;

Icombinetheseusingandoperators.

if((anEmployee.onVacation)

&&(anEmployee.seniority>10))return1;

return0.5;

IfIhaveamixofthese,Icancombineusingandandoroperatorsasneeded.Whenthishappens,thingsarelikelytogetmessy,soIuseExtractFunction(106)liberallytomakeitallunderstandable.

ReplaceNestedConditionalwithGuardClauses

Motivation

Ioftenfindthatconditionalexpressionscomeintwostyles.Inthefirststyle,bothlegsoftheconditionalarepartofnormalbehavior,whileinthesecondstyle,onelegisnormalandtheotherindicatesanunusualcondition.

Thesekindsofconditionalshavedifferentintentions—andtheseintentionsshouldcomethroughinthecode.Ifbotharepartofnormalbehavior,Iuseaconditionwithanifandanelseleg.Iftheconditionisanunusualcondition,Ichecktheconditionandreturnifit’strue.Thiskindofcheckisoftencalledaguardclause.

ThekeypointofReplaceNestedConditionalwithGuardClausesisemphasis.IfI’musinganif-then-elseconstruct,I’mgivingequalweighttotheiflegandtheelseleg.Thiscommunicatestothereaderthatthelegsareequallylikelyandimportant.Instead,theguardclausesays,“Thisisn’tthecoretothisfunction,andifithappens,dosomethingandgetout.”

IoftenfindIuseReplaceNestedConditionalwithGuardClauseswhenI’mworkingwithaprogrammerwhohasbeentaughttohaveonlyoneentrypointandoneexitpointfromamethod.Oneentrypointisenforcedbymodernlanguages,butoneexitpointisreallynotausefulrule.Clarityisthekeyprinciple:Ifthemethodisclearerwithoneexitpoint,useoneexitpoint;otherwisedon’t.

Mechanics

Selectoutermostconditionthatneedstobereplaced,andchangeitintoaguardclause.

Test.

Repeatasneeded.

Ifalltheguardclausesreturnthesameresult,useConsolidateConditionalExpression(263).

Example

Here’ssomecodetocalculateapaymentamountforanemployee.It’sonlyrelevantiftheemployeeisstillwiththecompany,soithastocheckforthetwoothercases.

functionpayAmount(employee){

letresult;

if(employee.isSeparated){

result={amount:0,reasonCode:"SEP"};

}

else{

if(employee.isRetired){

result={amount:0,reasonCode:"RET"};

}

else{

//logictocomputeamount

lorem.ipsum(dolor.sitAmet);

consectetur(adipiscing).elit();

sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);

ut.enim.ad(minim.veniam);

result=someFinalComputation();

}

}

returnresult;

}

Nestingtheconditionalsheremasksthetruemeaningofwhatitgoingon.Theprimarypurposeofthiscodeonlyappliesiftheseconditionsaren’tthecase.Inthissituation,theintentionofthecodereadsmoreclearlywithguardclauses.

Aswithanyrefactoringchange,Iliketotakesmallsteps,soIbeginwiththetopmostcondition.

functionpayAmount(employee){

letresult;

if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};

if(employee.isRetired){

result={amount:0,reasonCode:"RET"};

}

else{

//logictocomputeamount

lorem.ipsum(dolor.sitAmet);

consectetur(adipiscing).elit();

sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);

ut.enim.ad(minim.veniam);

result=someFinalComputation();

}

returnresult;

}

Itestthatchangeandmoveontothenextone.

functionpayAmount(employee){

letresult;

if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};

if(employee.isRetired)return{amount:0,reasonCode:"RET"};

//logictocomputeamount

lorem.ipsum(dolor.sitAmet);

consectetur(adipiscing).elit();

sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);

ut.enim.ad(minim.veniam);

result=someFinalComputation();

returnresult;

}

Atwhichpointtheresultvariableisn’treallydoinganythinguseful,soIremoveit.

functionpayAmount(employee){

letresult;

if(employee.isSeparated)return{amount:0,reasonCode:"SEP"};

if(employee.isRetired)return{amount:0,reasonCode:"RET"};

//logictocomputeamount

lorem.ipsum(dolor.sitAmet);

consectetur(adipiscing).elit();

sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);

ut.enim.ad(minim.veniam);

returnsomeFinalComputation();

}

Theruleisthatyoualwaysgetanextrastrawberrywhenyouremoveamutablevariable.

Example:ReversingtheConditions

Whenreviewingthemanuscriptofthefirsteditionofthisbook,JoshuaKerievskypointedoutthatweoftendoReplaceNestedConditionalwithGuardClausesbyreversingtheconditionalexpressions.Evenbetter,hegavemeanexamplesoIdidn’thavetofurthertaxmyimagination.

functionadjustedCapital(anInstrument){

letresult=0;

if(anInstrument.capital>0){

if(anInstrument.interestRate>0&&anInstrument.duration>0){

result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

}

}

returnresult;

}

Again,Imakethereplacementsoneatatime,butthistimeIreversetheconditionasIputintheguardclause.

functionadjustedCapital(anInstrument){

letresult=0;

if(anInstrument.capital<=0)returnresult;

if(anInstrument.interestRate>0&&anInstrument.duration>0){

result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

}

returnresult;

}

Thenextconditionalisabitmorecomplicated,soIdoitintwosteps.First,Isimplyaddanot.

functionadjustedCapital(anInstrument){

letresult=0;

if(anInstrument.capital<=0)returnresult;

if(!(anInstrument.interestRate>0&&anInstrument.duration>0))returnresult;

result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

returnresult;

}

Leavingnotsinaconditionallikethattwistsmymindaroundatapainfulangle,soIsimplifyit:

functionadjustedCapital(anInstrument){

letresult=0;

if(anInstrument.capital<=0)returnresult;

if(anInstrument.interestRate<=0||anInstrument.duration<=0)returnresult;

result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

returnresult;

}

Bothofthoselineshaveconditionswiththesameresult,soIapplyConsolidateConditionalExpression(263).

functionadjustedCapital(anInstrument){

letresult=0;

if(anInstrument.capital<=0

||anInstrument.interestRate<=0

||anInstrument.duration<=0)returnresult;

result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

returnresult;

}

Theresultvariableisdoingtwothingshere.Itsfirstsettingtozeroindicateswhattoreturnwhentheguardclausetriggers;itssecondvalueisthefinalcomputation.Icangetridofit,whichbotheliminatesitsdoubleusageandgetsmeastrawberry.

functionadjustedCapital(anInstrument){

if(anInstrument.capital<=0

||anInstrument.interestRate<=0

||anInstrument.duration<=0)return0;

return(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;

}

ReplaceConditionalwithPolymorphism

Motivation

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

AcommoncaseforthisiswhereIcanformasetoftypes,eachhandlingtheconditionallogicdifferently.Imightnoticethatbooks,music,andfoodvaryinhowtheyarehandledbecauseoftheirtype.Thisismademostobviouswhenthereareseveralfunctionsthathaveaswitchstatementonatypecode.Inthatcase,Iremovetheduplicationofthecommonswitchlogicbycreatingclassesforeachcaseandusingpolymorphismtobringoutthetype-specificbehavior.

AnothersituationiswhereIcanthinkofthelogicasabasecasewithvariants.Thebasecasemaybethemostcommonormoststraightforward.Icanputthislogicintoasuperclasswhichallowsmetoreasonaboutitwithouthavingtoworryaboutthevariants.Ithenputeachvariantcaseintoasubclass,whichIexpresswithcodethatemphasizesitsdifferencefromthebasecase.

Polymorphismisoneofthekeyfeaturesofobject-orientedprogramming—and,likeanyusefulfeature,it’spronetooveruse.I’vecomeacrosspeoplewhoarguethatallexamplesofconditionallogicshouldbereplacedwithpolymorphism.Idon’tagreewiththatview.Mostofmyconditionallogicusesbasicconditionalstatements—if/elseandswitch/case.ButwhenIseecomplexconditionallogicthatcanbeimprovedasdiscussedabove,Ifindpolymorphismapowerfultool.

Mechanics

Ifclassesdonotexistforpolymorphicbehavior,createthemtogetherwithafactoryfunctiontoreturnthecorrectinstance.

Usethefactoryfunctionincallingcode.

Movetheconditionalfunctiontothesuperclass.

Iftheconditionallogicisnotaself-containedfunction,useExtractFunction(106)tomakeitso.

Pickoneofthesubclasses.Createasubclassmethodthatoverridestheconditionalstatementmethod.Copythebodyofthatlegoftheconditionalstatementintothesubclassmethodandadjustittofit.

Repeatforeachlegoftheconditional.

Leaveadefaultcaseforthesuperclassmethod.Or,ifsuperclassshouldbe

abstract,declarethatmethodasabstractorthrowanerrortoshowitshouldbetheresponsibilityofasubclass.

Example

Myfriendhasacollectionofbirdsandwantstoknowhowfasttheycanflyandwhattheyhaveforplumage.Sowehaveacoupleofsmallprogramstodeterminetheinformation.

functionplumages(birds){

returnnewMap(birds.map(b=>[b.name,plumage(b)]));

}

functionspeeds(birds){

returnnewMap(birds.map(b=>[b.name,airSpeedVelocity(b)]));

}

functionplumage(bird){

switch(bird.type){

case'EuropeanSwallow':

return"average";

case'AfricanSwallow':

return(bird.numberOfCoconuts>2)?"tired":"average";

case'NorwegianBlueParrot':

return(bird.voltage>100)?"scorched":"beautiful";

default:

return"unknown";

}

}

functionairSpeedVelocity(bird){

switch(bird.type){

case'EuropeanSwallow':

return35;

case'AfricanSwallow':

return40-2*bird.numberOfCoconuts;

case'NorwegianBlueParrot':

return(bird.isNailed)?0:10+bird.voltage/10;

default:

returnnull;

}

}

Wehaveacoupleofdifferentoperationsthatvarywiththetypeofbird,soitmakessensetocreateclassesandusepolymorphismforanytype-specificbehavior.

IbeginbyusingCombineFunctionsintoClass(144)onairSpeedVelocityandplumage

functionplumage(bird){

returnnewBird(bird).plumage;

}

functionairSpeedVelocity(bird){

returnnewBird(bird).airSpeedVelocity;

}

classBird{

constructor(birdObject){

Object.assign(this,birdObject);

}

getplumage(){

switch(this.type){

case'EuropeanSwallow':

return"average";

case'AfricanSwallow':

return(this.numberOfCoconuts>2)?"tired":"average";

case'NorwegianBlueParrot':

return(this.voltage>100)?"scorched":"beautiful";

default:

return"unknown";

}

}

getairSpeedVelocity(){

switch(this.type){

case'EuropeanSwallow':

return35;

case'AfricanSwallow':

return40-2*this.numberOfCoconuts;

case'NorwegianBlueParrot':

return(this.isNailed)?0:10+this.voltage/10;

default:

returnnull;

}

}

}

Inowaddsubclassesforeachkindofbird,togetherwithafactoryfunctiontoinstantiatetheappropriatesubclass.

functionplumage(bird){

returncreateBird(bird).plumage;

}

functionairSpeedVelocity(bird){

returncreateBird(bird).airSpeedVelocity;

}

functioncreateBird(bird){

switch(bird.type){

case'EuropeanSwallow':

returnnewEuropeanSwallow(bird);

case'AfricanSwallow':

returnnewAfricanSwallow(bird);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrot(bird);

default:

returnnewBird(bird);

}

}

classEuropeanSwallowextendsBird{

}

classAfricanSwallowextendsBird{

}

classNorwegianBlueParrotextendsBird{

}

NowthatI’vecreatedtheclassstructurethatIneed,Icanbeginonthetwoconditionalmethods.I’llbeginwithplumage.Itakeonelegoftheswitchstatementandoverrideitintheappropriatesubclass.

classEuropeanSwallow…

getplumage(){

return"average";

}

classBird…

getplumage(){

switch(this.type){

case'EuropeanSwallow':

throw"oops";

case'AfricanSwallow':

return(this.numberOfCoconuts>2)?"tired":"average";

case'NorwegianBlueParrot':

return(this.voltage>100)?"scorched":"beautiful";

default:

return"unknown";

}

}

IputinthethrowbecauseI’mparanoid.

Icancompileandtestatthispoint.Then,ifalliswell,Idothenextleg.

classAfricanSwallow…

getplumage(){

return(this.numberOfCoconuts>2)?"tired":"average";

}

Then,theNorwegianBlue.

classNorwegianBlueParrot…

getplumage(){

return(this.voltage>100)?"scorched":"beautiful";

}

Ileavethesuperclassmethodforthedefaultcase.

classBird…

getplumage(){

return"unknown";

}

IrepeatthesameprocessforairSpeedVelocity.OnceI’mdone,Iendupwiththefollowingcode(Ialsoinlinedthetop-levelfunctionsforairSpeedVelocityandplumage):

functionplumages(birds){

returnnewMap(birds

.map(b=>createBird(b))

.map(bird=>[bird.name,bird.plumage]));

}

functionspeeds(birds){

returnnewMap(birds

.map(b=>createBird(b))

.map(bird=>[bird.name,bird.airSpeedVelocity]));

}

functioncreateBird(bird){

switch(bird.type){

case'EuropeanSwallow':

returnnewEuropeanSwallow(bird);

case'AfricanSwallow':

returnnewAfricanSwallow(bird);

case'NorwegianBlueParrot':

returnnewNorwegianBlueParrot(bird);

default:

returnnewBird(bird);

}

}

classBird{

constructor(birdObject){

Object.assign(this,birdObject);

}

getplumage(){

return"unknown";

}

getairSpeedVelocity(){

returnnull;

}

}

classEuropeanSwallowextendsBird{

getplumage(){

return"average";

}

getairSpeedVelocity(){

return35;

}

}

classAfricanSwallowextendsBird{

getplumage(){

return(this.numberOfCoconuts>2)?"tired":"average";

}

getairSpeedVelocity(){

return40-2*this.numberOfCoconuts;

}

}

classNorwegianBlueParrotextendsBird{

getplumage(){

return(this.voltage>100)?"scorched":"beautiful";

}

getairSpeedVelocity(){

return(this.isNailed)?0:10+this.voltage/10;

}

}

Lookingatthisfinalcode,IcanseethatthesuperclassBirdisn’tstrictlyneeded.

InJavaScript,Idon’tneedatypehierarchyforpolymorphism;aslongasmyobjectsimplementtheappropriatelynamedmethods,everythingworksfine.Inthissituation,however,Iliketokeeptheunnecessarysuperclassasithelpsexplainthewaytheclassesarerelatedinthedomain.

Example:UsingPolymorphismforVariation

Withthebirdsexample,I’musingacleargeneralizationhierarchy.That’showsubclassingandpolymorphismisoftendiscussedintextbooks(includingmine)—butit’snottheonlywayinheritanceisusedinpractice;indeed,itprobablyisn’tthemostcommonorbestway.AnothercaseforinheritanceiswhenIwishtoindicatethatoneobjectismostlysimilartoanother,butwithsomevariations.

Asanexampleofthiscase,considersomecodeusedbyaratingagencytocomputeaninvestmentratingforthevoyagesofsailingships.Theratingagencygivesouteitheran“A”or“B”rating,dependingofvariousfactorsduetoriskandprofitpotential.Theriskcomesfromassessingthenatureofthevoyageaswellasthehistoryofthecaptain’spriorvoyages.

functionrating(voyage,history){

constvpf=voyageProfitFactor(voyage,history);

constvr=voyageRisk(voyage);

constchr=captainHistoryRisk(voyage,history);

if(vpf*3>(vr+chr*2))return"A";

elsereturn"B";

}

functionvoyageRisk(voyage){

letresult=1;

if(voyage.length>4)result+=2;

if(voyage.length>8)result+=voyage.length-8;

if(["china","east-indies"].includes(voyage.zone))result+=4;

returnMath.max(result,0);

}

functioncaptainHistoryRisk(voyage,history){

letresult=1;

if(history.length<5)result+=4;

result+=history.filter(v=>v.profit<0).length;

if(voyage.zone==="china"&&hasChina(history))result-=2;

returnMath.max(result,0);

}

functionhasChina(history){

returnhistory.some(v=>"china"===v.zone);

}

functionvoyageProfitFactor(voyage,history){

letresult=2;

if(voyage.zone==="china")result+=1;

if(voyage.zone==="east-indies")result+=1;

if(voyage.zone==="china"&&hasChina(history)){

result+=3;

if(history.length>10)result+=1;

if(voyage.length>12)result+=1;

if(voyage.length>18)result-=1;

}

else{

if(history.length>8)result+=1;

if(voyage.length>14)result-=1;

}

returnresult;

}

ThefunctionsvoyageRiskandcaptainHistoryRiskscorepointsforrisk,voyageProfitFactorscorespointsforthepotentialprofit,andratingcombinesthesetogivetheoverallratingforthevoyage.

Thecallingcodewouldlooksomethinglikethis:

constvoyage={zone:"west-indies",length:10};

consthistory=[

{zone:"east-indies",profit:5},

{zone:"west-indies",profit:15},

{zone:"china",profit:-2},

{zone:"west-africa",profit:7},

];

constmyRating=rating(voyage,history);

WhatIwanttofocusonhereishowacoupleofplacesuseconditionallogictohandlethecaseofavoyagetoChinawherethecaptainhasbeentoChinabefore.

functionrating(voyage,history){

constvpf=voyageProfitFactor(voyage,history);

constvr=voyageRisk(voyage);

constchr=captainHistoryRisk(voyage,history);

if(vpf*3>(vr+chr*2))return"A";

elsereturn"B";

}

functionvoyageRisk(voyage){

letresult=1;

if(voyage.length>4)result+=2;

if(voyage.length>8)result+=voyage.length-8;

if(["china","east-indies"].includes(voyage.zone))result+=4;

returnMath.max(result,0);

}

functioncaptainHistoryRisk(voyage,history){

letresult=1;

if(history.length<5)result+=4;

result+=history.filter(v=>v.profit<0).length;

if(voyage.zone==="china"&&hasChina(history))result-=2;

returnMath.max(result,0);

}

functionhasChina(history){

returnhistory.some(v=>"china"===v.zone);

}

functionvoyageProfitFactor(voyage,history){

letresult=2;

if(voyage.zone==="china")result+=1;

if(voyage.zone==="east-indies")result+=1;

if(voyage.zone==="china"&&hasChina(history)){

result+=3;

if(history.length>10)result+=1;

if(voyage.length>12)result+=1;

if(voyage.length>18)result-=1;

}

else{

if(history.length>8)result+=1;

if(voyage.length>14)result-=1;

}

returnresult;

}

Iwilluseinheritanceandpolymorphismtoseparateoutthelogicforhandlingthesecasesfromthebaselogic.ThisisaparticularlyusefulrefactoringifI’mabouttointroducemorespeciallogicforthiscase—andthelogicfortheserepeatChinavoyagescanmakeithardertounderstandthebasecase.

I’mbeginningwithasetoffunctions.Tointroducepolymorphism,Ineedtocreateaclassstructure,soIbeginbyapplyingCombineFunctionsintoClass(144).Thisresultsinthefollowingcode.

functionrating(voyage,history){

returnnewRating(voyage,history).value;

}

classRating{

constructor(voyage,history){

this.voyage=voyage;

this.history=history;

}

getvalue(){

constvpf=this.voyageProfitFactor;

constvr=this.voyageRisk;

constchr=this.captainHistoryRisk;

if(vpf*3>(vr+chr*2))return"A";

elsereturn"B";

}

getvoyageRisk(){

letresult=1;

if(this.voyage.length>4)result+=2;

if(this.voyage.length>8)result+=this.voyage.length-8;

if(["china","east-indies"].includes(this.voyage.zone))result+=4;

returnMath.max(result,0);

}

getcaptainHistoryRisk(){

letresult=1;

if(this.history.length<5)result+=4;

result+=this.history.filter(v=>v.profit<0).length;

if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;

returnMath.max(result,0);

}

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

if(this.voyage.zone==="china"&&this.hasChinaHistory){

result+=3;

if(this.history.length>10)result+=1;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

}

else{

if(this.history.length>8)result+=1;

if(this.voyage.length>14)result-=1;

}

returnresult;

}

gethasChinaHistory(){

returnthis.history.some(v=>"china"===v.zone);

}

}

That’sgivenmetheclassforthebasecase.Inowneedtocreateanemptysubclasstohousethevariantbehavior.

classExperiencedChinaRatingextendsRating{

}

Ithencreateafactoryfunctiontoreturnthevariantclasswhenneeded.

functioncreateRating(voyage,history){

if(voyage.zone==="china"&&history.some(v=>"china"===v.zone))

returnnewExperiencedChinaRating(voyage,history);

elsereturnnewRating(voyage,history);

}

Ineedtomodifyanycallerstousethefactoryfunctioninsteadofdirectlyinvokingtheconstructor,whichinthiscaseisjusttheratingfunction.

functionrating(voyage,history){

returncreateRating(voyage,history).value;

}

TherearetwobitsofbehaviorIneedtomoveintoasubclass.IbeginwiththelogicincaptainHistoryRisk:

classRating…

getcaptainHistoryRisk(){

letresult=1;

if(this.history.length<5)result+=4;

result+=this.history.filter(v=>v.profit<0).length;

if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;

returnMath.max(result,0);

}

Iwritetheoverridingmethodinthesubclass:

classExperiencedChinaRating

getcaptainHistoryRisk(){

constresult=super.captainHistoryRisk-2;

returnMath.max(result,0);

}

classRating…

getcaptainHistoryRisk(){

letresult=1;

if(this.history.length<5)result+=4;

result+=this.history.filter(v=>v.profit<0).length;

if(this.voyage.zone==="china"&&this.hasChinaHistory)result-=2;

returnMath.max(result,0);

}

SeparatingthevariantbehaviorfromvoyageProfitFactorisabitmoremessy.Ican’tsimplyremovethevariantbehaviorandcallthesuperclassmethodsincethereisanalternativepathhere.Ialsodon’twanttocopythewholesuperclassmethoddowntothesubclass.

classRating…

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

if(this.voyage.zone==="china"&&this.hasChinaHistory){

result+=3;

if(this.history.length>10)result+=1;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

}

else{

if(this.history.length>8)result+=1;

if(this.voyage.length>14)result-=1;

}

returnresult;

}

SomyresponseistofirstuseExtractFunction(106)ontheentireconditionalblock.

classRating…

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

result+=this.voyageAndHistoryLengthFactor;

returnresult;

}

getvoyageAndHistoryLengthFactor(){

letresult=0;

if(this.voyage.zone==="china"&&this.hasChinaHistory){

result+=3;

if(this.history.length>10)result+=1;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

}

else{

if(this.history.length>8)result+=1;

if(this.voyage.length>14)result-=1;

}

returnresult;

}

Afunctionnamewithan“And”initisaprettybadsmell,butI’llletitsitandreekforamoment,whileIapplythesubclassing.

classRating…

getvoyageAndHistoryLengthFactor(){

letresult=0;

if(this.history.length>8)result+=1;

if(this.voyage.length>14)result-=1;

returnresult;

}

classExperiencedChinaRating…

getvoyageAndHistoryLengthFactor(){

letresult=0;

result+=3;

if(this.history.length>10)result+=1;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

That’s,formally,theendoftherefactoring—I’veseparatedthevariantbehavioroutintothesubclass.Thesuperclass’slogicissimplertounderstandandworkwith,andIonlyneedtodealwithvariantcasewhenI’mworkingonthesubclasscode,whichisexpressedintermsofitsdifferencewiththesuperclass.

ButIfeelIshouldatleastoutlinewhatI’ddowiththeawkwardnewmethod.Introducingamethodpurelyforoverridingbyasubclassisacommonthingtodowhendoingthiskindofbase-and-variationinheritance.Butacrudemethodlikethisobscureswhat’sgoingon,insteadofrevealing.

The“and”givesawaythattherearereallytwoseparatemodificationsgoingonhere—soIthinkit’swisetoseparatethem.I’lldothisbyusingExtractFunction

(106)onthehistorylengthmodification,bothinthesuperclassandsubclass.Istartwithjustthesuperclass:

classRating…

getvoyageAndHistoryLengthFactor(){

letresult=0;

result+=this.historyLengthFactor;

if(this.voyage.length>14)result-=1;

returnresult;

}

gethistoryLengthFactor(){

return(this.history.length>8)?1:0;

}

Idothesamewiththesubclass:

classExperiencedChinaRating…

getvoyageAndHistoryLengthFactor(){

letresult=0;

result+=3;

result+=this.historyLengthFactor;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

gethistoryLengthFactor(){

return(this.history.length>10)?1:0;

}

IcanthenuseMoveStatementstoCallers(215)onthesuperclasscase.

classRating…

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

result+=this.historyLengthFactor;

result+=this.voyageAndHistoryLengthFactor;

returnresult;

}

getvoyageAndHistoryLengthFactor(){

letresult=0;

result+=this.historyLengthFactor;

if(this.voyage.length>14)result-=1;

returnresult;

}

classExperiencedChinaRating…

getvoyageAndHistoryLengthFactor(){

letresult=0;

result+=3;

result+=this.historyLengthFactor;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

I’dthenuseChangeFunctionDeclaration(124).

classRating…

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

result+=this.historyLengthFactor;

result+=this.voyageLengthFactor;

returnresult;

}

getvoyageLengthFactor(){

return(this.voyage.length>14)?-1:0;

}

ChangingtoaternarytosimplifyvoyageLengthFactor.

classExperiencedChinaRating…

getvoyageLengthFactor(){

letresult=0;

result+=3;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

Onelastthing.Idon’tthinkadding3pointsmakessenseaspartofthevoyagelengthfactor—it’sbetteraddedtotheoverallresult.

classExperiencedChinaRating…

getvoyageProfitFactor(){

returnsuper.voyageProfitFactor+3;

}

getvoyageLengthFactor(){

letresult=0;

result+=3;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

Attheendoftherefactoring,Ihavethefollowingcode.First,thereisthebasicratingclasswhichcanignoreanycomplicationsoftheexperiencedChinacase:

classRating{

constructor(voyage,history){

this.voyage=voyage;

this.history=history;

}

getvalue(){

constvpf=this.voyageProfitFactor;

constvr=this.voyageRisk;

constchr=this.captainHistoryRisk;

if(vpf*3>(vr+chr*2))return"A";

elsereturn"B";

}

getvoyageRisk(){

letresult=1;

if(this.voyage.length>4)result+=2;

if(this.voyage.length>8)result+=this.voyage.length-8;

if(["china","east-indies"].includes(this.voyage.zone))result+=4;

returnMath.max(result,0);

}

getcaptainHistoryRisk(){

letresult=1;

if(this.history.length<5)result+=4;

result+=this.history.filter(v=>v.profit<0).length;

returnMath.max(result,0);

}

getvoyageProfitFactor(){

letresult=2;

if(this.voyage.zone==="china")result+=1;

if(this.voyage.zone==="east-indies")result+=1;

result+=this.historyLengthFactor;

result+=this.voyageLengthFactor;

returnresult;

}

getvoyageLengthFactor(){

return(this.voyage.length>14)?-1:0;

}

gethistoryLengthFactor(){

return(this.history.length>8)?1:0;

}

}

ThecodefortheexperiencedChinacasereadsasasetofvariationsonthebase:

classExperiencedChinaRatingextendsRating{

getcaptainHistoryRisk(){

constresult=super.captainHistoryRisk-2;

returnMath.max(result,0);

}

getvoyageLengthFactor(){

letresult=0;

if(this.voyage.length>12)result+=1;

if(this.voyage.length>18)result-=1;

returnresult;

}

gethistoryLengthFactor(){

return(this.history.length>10)?1:0;

}

getvoyageProfitFactor(){

returnsuper.voyageProfitFactor+3;

}

}

IntroduceSpecialCase

formerly:IntroduceNullObject

Motivation

Acommoncaseofduplicatedcodeiswhenmanyusersofadatastructurecheckaspecificvalue,andthenmostofthemdothesamething.IfIfindmanypartsofthecodebasehavingthesamereactiontoaparticularvalue,Iwanttobringthatreactionintoasingleplace.

AgoodmechanismforthisistheSpecialCasepatternwhereIcreateaspecial-caseelementthatcapturesallthecommonbehavior.Thisallowsmetoreplacemostofthespecial-casecheckswithsimplecalls.

Aspecialcasecanmanifestitselfinseveralways.IfallI’mdoingwiththeobjectisreadingdata,IcansupplyaliteralobjectwithallthevaluesIneedfilledin.IfIneedmorebehaviorthansimplevalues,Icancreateaspecialobjectwithmethodsforallthecommonbehavior.Thespecial-caseobjectcanbereturnedbyanencapsulatingclass,orinsertedintoadatastructurewithatransform.

Acommonvaluethatneedsspecial-caseprocessingisnull,whichiswhythispatternisoftencalledtheNullObjectpattern.Butit’sthesameapproachforanyspecialcase—IliketosaythatNullObjectisaspecialcaseofSpecialCase.

Mechanics

Beginwithacontainerdatastructure(orclass)thatcontainsapropertywhichisthesubjectoftherefactoring.Clientsofthecontainercomparethesubjectpropertyofthecontainertoaspecial-casevalue.Wewishtoreplacethespecial-casevalueofthesubjectwithaspecialcaseclassordatastructure.

Addaspecial-casecheckpropertytothesubject,returningfalse.

Createaspecial-caseobjectwithonlythespecial-casecheckproperty,returningtrue.

ApplyExtractFunction(106)tothespecial-casecomparisoncode.Ensurethatallclientsusethenewfunctioninsteadofdirectlycomparingit.

Introducethenewspecial-casesubjectintothecode,eitherbyreturningitfromafunctioncallorbyapplyingatransformfunction.

Changethebodyofthespecial-casecomparisonfunctionsothatitusesthespecial-casecheckproperty.

Test.

UseCombineFunctionsintoClass(144)orCombineFunctionsintoTransform(149)tomoveallofthecommonspecial-casebehaviorintothenewelement.

Sincethespecial-caseclassusuallyreturnsfixedvaluestosimplerequests,thesemaybehandledbymakingthespecialcasealiteralrecord.

UseInlineFunction(115)onthespecial-casecomparisonfunctionfortheplaceswhereit’sstillneeded.

Example

Autilitycompanyinstallsitsservicesinsites.

classSite…

getcustomer(){returnthis._customer;}

Therearevariouspropertiesofthecustomerclass;I’llconsiderthreeofthem.

classCustomer…

getname(){…}

getbillingPlan(){…}

setbillingPlan(arg){…}

getpaymentHistory(){…}

Mostofthetime,asitehasacustomer,butsometimesthereisn’tone.SomeonemayhavemovedoutandIdon’tyetknowwho,ifanyone,hasmovedin.Whenthishappens,thedatarecordfillsthecustomerfieldwiththestring“unknown”.Becausethiscanhappen,clientsofthesiteneedtobeabletohandleanunknowncustomer.Herearesomeexamplefragments.

client1…

constaCustomer=site.customer;

//…lotsofinterveningcode…

letcustomerName;

if(aCustomer==="unknown")customerName="occupant";

elsecustomerName=aCustomer.name;

client2…

constplan=(aCustomer==="unknown")?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

if(aCustomer!=="unknown")aCustomer.billingPlan=newPlan;

client4…

constweeksDelinquent=(aCustomer==="unknown")?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

Lookingthroughthecodebase,Iseemanyclientsofthesiteobjectthathavetodealwithanunknowncustomer.Mostofthemdothesamethingwhentheygetone:Theyuse“occupant”asthename,givethemabasicbillingplan,andclassthemaszero-weeksdelinquent.Thiswidespreadtestingforaspecialcase,plusacommonresponse,iswhattellsmeit’stimeforaSpecialCaseObject.

Ibeginbyaddingamethodtothecustomertoindicateitisunknown.

classCustomer…

getisUnknown(){returnfalse;}

IthenaddanUnknownCustomerclass.

classUnknownCustomer{

getisUnknown(){returntrue;}

}

NotethatIdon’tmakeUnknownCustomerasubclassofCustomer.Inotherlanguages,particularlythosestaticallytyped,Iwould,butJavaScript’srulesforsubclassing,aswellasitsdynamictyping,makeitbettertonotdothathere.

Nowcomesthetrickybit.Ihavetoreturnthisnewspecial-caseobjectwheneverIexpect"unknown"andchangeeachtestforanunknownvaluetousethenewisUnknownmethod.Ingeneral,IalwayswanttoarrangethingssoIcanmakeonesmallchangeatatime,thentest.ButifIchangethecustomerclasstoreturnanunknowncustomerinsteadof“unknown”,Ihavetomakeeveryclienttestingfor“unknown”tocallisUnknown—andIhavetodoitallatonce.Ifindthatasappealingaseatingliver(i.e.notatall).

ThereisacommontechniquetousewheneverIfindmyselfinthisbind.IuseExtractFunction(106)onthecodethatI’dhavetochangeinlotsofplaces—inthiscase,thespecial-casecomparisoncode.

functionisUnknown(arg){

if(!((arginstanceofCustomer)||(arg==="unknown")))

thrownewError(`investigatebadvalue:<${arg}>`);

return(arg==="unknown");

}

I’veputatrapinhereforanunexpectedvalue.ThiscanhelpmetospotanymistakesoroddbehaviorasI’mdoingthisrefactoring.

IcannowusethisfunctionwheneverI’mtestingforanunknowncustomer.Icanchangethesecallsoneatatime,testingaftereachchange.

client1…

letcustomerName;

if(isUnknown(aCustomer))customerName="occupant";

elsecustomerName=aCustomer.name;

Afterawhile,Ihavedonethemall.

client2…

constplan=(isUnknown(aCustomer))?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

if(!isUnknown(aCustomer))aCustomer.billingPlan=newPlan;

client4…

constweeksDelinquent=isUnknown(aCustomer)?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

OnceI’vechangedallthecallerstouseisUnknown,Icanchangethesiteclasstoreturnanunknowncustomer.

classSite…

getcustomer(){

return(this._customer==="unknown")?newUnknownCustomer():this._customer;

}

IcancheckthatI’mnolongerusingthe“unknown”stringbychangingisUnknowntousetheunknownvalue.

client1…

functionisUnknown(arg){

if(!(arginstanceofCustomer||arginstanceofUnknownCustomer))

thrownewError(`investigatebadvalue:<${arg}>`);

returnarg.isUnknown;

}

Itesttoensurethat’sallworking.

Nowthefunbegins.IcanuseCombineFunctionsintoClass(144)totakeeach

client’sspecial-casecheckandseeifIcanreplaceitwithacommonlyexpectedvalue.Atthemoment,Ihavevariousclientsusing“occupant”forthenameofanunknowncustomer,likethis:

client1…

letcustomerName;

if(isUnknown(aCustomer))customerName="occupant";

elsecustomerName=aCustomer.name;

Iaddasuitablemethodtotheunknowncustomer:

classUnknownCustomer…

getname(){return"occupant";}

NowIcanmakeallthatconditionalcodegoaway.

client1…

constcustomerName=aCustomer.name;

OnceI’vetestedthatthisworks,I’llprobablybeabletouseInlineVariable(123)onthatvariabletoo.

Nextisthebillingplanproperty.

client2…

constplan=(isUnknown(aCustomer))?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

if(!isUnknown(aCustomer))aCustomer.billingPlan=newPlan;

Forreadbehavior,IdothesamethingIdidwiththename—takethecommonresponseandreplywithit.Withthewritebehavior,thecurrentcodedoesn’tcallthesetterforanunknowncustomer—soforthespecialcase,Iletthesetterbecalled,butitdoesnothing.

classUnknownCustomer…

getbillingPlan(){returnregistry.billingPlans.basic;}

setbillingPlan(arg){/*ignore*/}

clientreader…

constplan=aCustomer.billingPlan;

clientwriter…

aCustomer.billingPlan=newPlan;

Special-caseobjectsarevalueobjects,andthusshouldalwaysbeimmutable,eveniftheobjectstheyaresubstitutingforarenot.

Thelastcaseisabitmoreinvolvedbecausethespecialcaseneedstoreturnanotherobjectthathasitsownproperties.

client…

constweeksDelinquent=isUnknown(aCustomer)?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

Thegeneralrulewithaspecial-caseobjectisthatifitneedstoreturnrelatedobjects,theyareusuallyspecialcasesthemselves.SohereIneedtocreateanullpaymenthistory.

classUnknownCustomer…

getpaymentHistory(){returnnewNullPaymentHistory();}

classNullPaymentHistory…

getweeksDelinquentInLastYear(){return0;}

client…

constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;

Icarryon,lookingatalltheclientstoseeifIcanreplacethemwiththepolymorphicbehavior.Buttherewillbeexceptions—clientsthatwanttodosomethingdifferentwiththespecialcase.Imayhave23clientsthatuse“occupant”forthenameofanunknowncustomer,butthere’salwaysonethat

needssomethingdifferent.

client…

constname=!isUnknown(aCustomer)?aCustomer.name:"unknownoccupant";

Inthatcase,Ineedtoretainaspecial-casecheck.Iwillchangeittousethemethodoncustomer,essentiallyusingInlineFunction(115)onisUnknown

client…

constname=aCustomer.isUnknown?"unknownoccupant":aCustomer.name;

WhenI’mdonewithalltheclients,IshouldbeabletouseRemoveDeadCode(236)ontheglobalisPresentfunction,asnobodyshouldbecallingitanymore.

Example:UsinganObjectLiteral

Creatingaclasslikethisisafairbitofworkforwhatisreallyasimplevalue.ButfortheexampleIgave,Ihadtomaketheclasssincethecustomercouldbeupdated.If,however,Ionlyreadthedatastructure,Icanusealiteralobjectinstead.

Hereistheopeningcaseagain—justthesame,exceptthistimethereisnoclientthatupdatesthecustomer.

classSite…

getcustomer(){returnthis._customer;}

classCustomer…

getname(){…}

getbillingPlan(){…}

setbillingPlan(arg){…}

getpaymentHistory(){…}

client1…

constaCustomer=site.customer;

//…lotsofinterveningcode…

letcustomerName;

if(aCustomer==="unknown")customerName="occupant";

elsecustomerName=aCustomer.name;

client2…

constplan=(aCustomer==="unknown")?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

constweeksDelinquent=(aCustomer==="unknown")?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

Aswiththepreviouscase,IstartbyaddinganisUnknownpropertytothecustomerandcreatingaspecial-caseobjectwiththatfield.Thedifferenceisthatthistime,thespecialcaseisaliteral.

classCustomer…

getisUnknown(){returnfalse;}

toplevel…

functioncreateUnknownCustomer(){

return{

isUnknown:true,

};

}

IapplyExtractFunction(106)tothespecialcaseconditiontest.

functionisUnknown(arg){

return(arg==="unknown");

}

client1…

letcustomerName;

if(isUnknown(aCustomer))customerName="occupant";

elsecustomerName=aCustomer.name;

client2…

constplan=isUnknown(aCustomer)?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

constweeksDelinquent=isUnknown(aCustomer)?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

Ichangethesiteclassandtheconditiontesttoworkwiththespecialcase.

classSite…

getcustomer(){

return(this._customer==="unknown")?createUnknownCustomer():this._customer;

}

toplevel…

functionisUnknown(arg){

returnarg.isUnknown;

}

ThenIreplaceeachstandardresponsewiththeappropriateliteralvalue.Istartwiththename:

functioncreateUnknownCustomer(){

return{

isUnknown:true,

name:"occupant",

};

}

client1…

constcustomerName=aCustomer.name;

Then,thebillingplan:

functioncreateUnknownCustomer(){

return{

isUnknown:true,

name:"occupant",

billingPlan:registry.billingPlans.basic,

};

}

client2…

constplan=aCustomer.billingPlan;

Similarly,Icancreateanestednullpaymenthistorywiththeliteral:

functioncreateUnknownCustomer(){

return{

isUnknown:true,

name:"occupant",

billingPlan:registry.billingPlans.basic,

paymentHistory:{

weeksDelinquentInLastYear:0,

},

};

}

client3…

constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;

IfIusealiterallikethis,Ishouldmakeitimmutable,whichImightdowithfreeze.Usually,I’dratheruseaclass.

Example:UsingaTransform

Bothpreviouscasesinvolveaclass,butthesameideacanbeappliedtoarecordbyusingatransformstep.

Let’sassumeourinputisasimplerecordstructurethatlookssomethinglikethis:

{

name:"AcmeBoston",

location:"MaldenMA",

//moresitedetails

customer:{

name:"AcmeIndustries",

billingPlan:"plan-451",

paymentHistory:{

weeksDelinquentInLastYear:7

//more

},

//more

}

}

Insomecases,thecustomerisn’tknown,andsuchcasesaremarkedinthesameway:

{

name:"WarehouseUnit15",

location:"MaldenMA",

//moresitedetails

customer:"unknown",

}

Ihavesimilarclientcodethatchecksfortheunknowncustomer:

client1…

constsite=acquireSiteData();

constaCustomer=site.customer;

//…lotsofinterveningcode…

letcustomerName;

if(aCustomer==="unknown")customerName="occupant";

elsecustomerName=aCustomer.name;

client2…

constplan=(aCustomer==="unknown")?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

constweeksDelinquent=(aCustomer==="unknown")?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

Myfirststepistorunthesitedatastructurethroughatransformthat,currently,doesnothingbutadeepcopy.

client1…

constrawSite=acquireSiteData();

constsite=enrichSite(rawSite);

constaCustomer=site.customer;

//…lotsofinterveningcode…

letcustomerName;

if(aCustomer==="unknown")customerName="occupant";

elsecustomerName=aCustomer.name;

functionenrichSite(inputSite){

return_.cloneDeep(inputSite);

}

IapplyExtractFunction(106)tothetestforanunknowncustomer.

functionisUnknown(aCustomer){

returnaCustomer==="unknown";

}

client1…

constrawSite=acquireSiteData();

constsite=enrichSite(rawSite);

constaCustomer=site.customer;

//…lotsofinterveningcode…

letcustomerName;

if(isUnknown(aCustomer))customerName="occupant";

elsecustomerName=aCustomer.name;

client2…

constplan=(isUnknown(aCustomer))?

registry.billingPlans.basic

:aCustomer.billingPlan;

client3…

constweeksDelinquent=(isUnknown(aCustomer))?

0

:aCustomer.paymentHistory.weeksDelinquentInLastYear;

IbegintheenrichmentbyaddinganisUnknownpropertytothecustomer.

functionenrichSite(aSite){

constresult=_.cloneDeep(aSite);

constunknownCustomer={

isUnknown:true,

};

if(isUnknown(result.customer))result.customer=unknownCustomer;

elseresult.customer.isUnknown=false;

returnresult;

}

Icanthenmodifythespecial-caseconditiontesttoincludeprobingforthisnewproperty.Ikeeptheoriginaltestaswell,sothatthetestwillworkonbothrawandenrichedsites.

functionisUnknown(aCustomer){

if(aCustomer==="unknown")returntrue;

elsereturnaCustomer.isUnknown;

}

Itesttoensurethat’sallOK,thenstartapplyingCombineFunctionsintoTransform(149)onthespecialcase.First,Imovethechoiceofnameintotheenrichmentfunction.

functionenrichSite(aSite){

constresult=_.cloneDeep(aSite);

constunknownCustomer={

isUnknown:true,

name:"occupant",

};

if(isUnknown(result.customer))result.customer=unknownCustomer;

elseresult.customer.isUnknown=false;

returnresult;

}

client1…

constrawSite=acquireSiteData();

constsite=enrichSite(rawSite);

constaCustomer=site.customer;

//…lotsofinterveningcode…

constcustomerName=aCustomer.name;

Itest,thendothebillingplan.

functionenrichSite(aSite){

constresult=_.cloneDeep(aSite);

constunknownCustomer={

isUnknown:true,

name:"occupant",

billingPlan:registry.billingPlans.basic,

};

if(isUnknown(result.customer))result.customer=unknownCustomer;

elseresult.customer.isUnknown=false;

returnresult;

}

client2…

constplan=aCustomer.billingPlan;

Itestagain,thendothelastclient.

functionenrichSite(aSite){

constresult=_.cloneDeep(aSite);

constunknownCustomer={

isUnknown:true,

name:"occupant",

billingPlan:registry.billingPlans.basic,

paymentHistory:{

weeksDelinquentInLastYear:0,

}

};

if(isUnknown(result.customer))result.customer=unknownCustomer;

elseresult.customer.isUnknown=false;

returnresult;

}

client3…

constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;

IntroduceAssertion

Motivation

Often,sectionsofcodeworkonlyifcertainconditionsaretrue.Thismaybeassimpleasasquarerootcalculationonlyworkingonapositiveinputvalue.Withanobject,itmayrequirethatatleastoneofagroupoffieldshasavalueinit.

Suchassumptionsareoftennotstatedbutcanonlybededucedbylookingthroughanalgorithm.Sometimes,theassumptionsarestatedwithacomment.Abettertechniqueistomaketheassumptionexplicitbywritinganassertion.

Anassertionisaconditionalstatementthatisassumedtobealwaystrue.Failureofanassertionindicatesaprogrammererror.Assertionfailuresshouldneverbecheckedbyotherpartsofthesystem.Assertionsshouldbewrittensothattheprogramfunctionsequallycorrectlyiftheyareallremoved;indeed,somelanguagesprovideassertionsthatcanbedisabledbyacompile-timeswitch.

Ioftenseepeopleencourageusingassertionsinordertofinderrors.WhilethisiscertainlyaGoodThing,it’snottheonlyreasontousethem.Ifindassertionstobeavaluableformofcommunication—theytellthereadersomethingabouttheassumedstateoftheprogramatthispointofexecution.Ialsofindthemhandyfordebugging,andtheircommunicationvaluemeansI’minclinedtoleavetheminonceI’vefixedtheerrorI’mchasing.Self-testingcodereducestheirvaluefordebugging,assteadilynarrowingunittestsoftendothejobbetter,butIstilllikeassertionsforcommunication.

Mechanics

Whenyouseethataconditionisassumedtobetrue,addanassertiontostateit.

Sinceassertionsshouldnotaffecttherunningofasystem,addingoneisalwaysbehavior-preserving.

Example

Here’sasimpletaleofdiscounts.Acustomercanbegivenadiscountratetoapplytoalltheirpurchases:

classCustomer…

applyDiscount(aNumber){

return(this.discountRate)

?aNumber-(this.discountRate*aNumber)

:aNumber;

}

There’sanassumptionherethatthediscountrateisapositivenumber.Icanmakethatassumptionexplicitbyusinganassertion.ButIcan’teasilyplaceanassertionintoaternaryexpression,sofirstI’llreformulateitasanif-thenstatement.

classCustomer…

applyDiscount(aNumber){

if(!this.discountRate)returnaNumber;

elsereturnaNumber-(this.discountRate*aNumber);

}

NowIcaneasilyaddtheassertion.

classCustomer…

applyDiscount(aNumber){

if(!this.discountRate)returnaNumber;

else{

assert(this.discountRate>=0);

returnaNumber-(this.discountRate*aNumber);

}

}

Inthiscase,I’dratherputthisassertionintothesettingmethod.IftheassertionfailsinapplyDiscount,myfirstpuzzleishowitgotintothefieldinthefirstplace.

classCustomer…

setdiscountRate(aNumber){

assert(null===aNumber||aNumber>=0);

this._discountRate=aNumber;

}

Anassertionlikethiscanbeparticularlyvaluableifit’shardtospottheerrorsource—whichmaybeanerrantminussigninsomeinputdataorsomeinversionelsewhereinthecode.

Thereisarealdangerofoverusingassertions.Idon’tuseassertionstocheckeverythingthatIthinkistrue,butonlytocheckthingsthatneedtobetrue.Duplicationisaparticularproblem,asit’scommontotweakthesekindsofconditions.SoIfindit’sessentialtoremoveanyduplicationintheseconditions,usuallybyaliberaluseofExtractFunction(106).

Ionlyuseassertionsforthingsthatareprogrammererrors.IfI’mreadingdatafromanexternalsource,anyvaluecheckingshouldbeafirst-classpartoftheprogram,notanassertion—unlessI’mreallyconfidentintheexternalsource.Assertionsarealastresorttohelptrackbugs—though,ironically,IonlyusethemwhenIthinktheyshouldneverfail.

Chapter11RefactoringAPIsModulesandtheirfunctionsarethebuildingblocksofoursoftware.APIsarethejointsthatweusetoplugthemtogether.MakingtheseAPIseasytounderstandanduseisimportantbutalsodifficult:IneedtorefactorthemasIlearnhowtoimprovethem.

AgoodAPIclearlyseparatesanyfunctionsthatupdatedatafromthosethatonlyreaddata.IfIseethemcombined,IuseSeparateQueryfromModifier(304)toteasethemapart.IcanunifyfunctionsthatonlyvaryduetoavaluewithParameterizeFunction(308).Someparameters,however,arereallyjustasignalofanentirelydifferentbehaviorandarebestexcisedwithRemoveFlagArgument(312).

Datastructuresareoftenunpackedunnecessarilywhenpassedbetweenfunctions;IprefertokeepthemtogetherwithPreserveWholeObject(317).Decisionsonwhatshouldbepassedasaparameter,andwhatcanberesolvedbythecalledfunction,areonesIoftenneedtorevisitwithReplaceParameterwithQuery(322)andReplaceQuerywithParameter(325).

Aclassisacommonformofmodule.Iprefermyobjectstobeasimmutableaspossible,soIuseRemoveSettingMethod(329)wheneverIcan.Often,whenacallerasksforanewobject,Ineedmoreflexibilitythanasimpleconstructorgives,whichIcangetbyusingReplaceConstructorwithFactoryFunction(332).

Thelasttworefactoringsaddressthedifficultyofbreakingdownaparticularlycomplexfunctionthatpassesalotofdataaround.IcanturnthatfunctionintoanobjectwithReplaceFunctionwithCommand(335),whichmakesiteasiertouseExtractFunction(106)onthefunction’sbody.IfIlatersimplifythefunctionandnolongerneeditasacommandobject,IturnitbackintoafunctionwithReplaceCommandwithFunction(342).

SeparateQueryfromModifier

Motivation

WhenIhaveafunctionthatgivesmeavalueandhasnoobservablesideeffects,Ihaveaveryvaluablething.IcancallthisfunctionasoftenasIlike.Icanmovethecalltootherplacesinacallingfunction.It’seasiertotest.Inshort,Ihavealotlesstoworryabout.

Itisagoodideatoclearlysignalthedifferencebetweenfunctionswithsideeffectsandthosewithout.Agoodruletofollowisthatanyfunctionthatreturnsavalueshouldnothaveobservablesideeffects—thecommand-queryseparation[bib-cqs].Someprogrammerstreatthisasanabsoluterule.I’mnot100percentpureonthis(asonanything),butItrytofollowitmostofthetime,andithasservedmewell.

IfIcomeacrossamethodthatreturnsavaluebutalsohassideeffects,Ialwaystrytoseparatethequeryfromthemodifier.

NotethatIusethephraseobservablesideeffects.Acommonoptimizationisto

cachethevalueofaqueryinafieldsothatrepeatedcallsgoquicker.Althoughthischangesthestateoftheobjectwiththecache,thechangeisnotobservable.Anysequenceofquerieswillalwaysreturnthesameresultsforeachquery.

Mechanics

Copythefunction,nameitasaquery.

Lookintothefunctiontoseewhatisreturned.Ifthequeryisusedtopopulateavariable,thevariable’snameshouldprovideagoodclue.

Removeanysideeffectsfromthenewqueryfunction.

Runstaticchecks.

Findeachcalloftheoriginalmethod.Ifthatcallusesthereturnvalue,replacetheoriginalcallwithacalltothequeryandinsertacalltotheoriginalmethodbelowit.Testaftereachchange.

Removereturnvaluesfromoriginal.

Test.

Oftenafterdoingthistherewillbeduplicationbetweenthequeryandtheoriginalmethodthatcanbetidiedup.

Example

Hereisafunctionthatscansalistofnamesforamiscreant.Ifitfindsone,itreturnsthenameofthebadguyandsetsoffthealarms.Itonlydoesthisforthefirstmiscreantitfinds(Iguessoneisenough).

functionalertForMiscreant(people){

for(constpofpeople){

if(p==="Don"){

setOffAlarms();

return"Don";

}

if(p==="John"){

setOffAlarms();

return"John";

}

}

return"";

}

Ibeginbycopyingthefunction,namingitafterthequeryaspectofthefunction.

functionfindMiscreant(people){

for(constpofpeople){

if(p==="Don"){

setOffAlarms();

return"Don";

}

if(p==="John"){

setOffAlarms();

return"John";

}

}

return"";

}

Iremovethesideeffectsfromthisnewquery.

functionfindMiscreant(people){

for(constpofpeople){

if(p==="Don"){

setOffAlarms();

return"Don";

}

if(p==="John"){

setOffAlarms();

return"John";

}

}

return"";

}

Inowgotoeachcallerandreplaceitwithacalltothequery,followedbyacalltothemodifier.So

constfound=alertForMiscreant(people);

changesto

constfound=findMiscreant(people);

alertForMiscreant(people);

Inowremovethereturnvaluesfromthemodifier.

functionalertForMiscreant(people){

for(constpofpeople){

if(p==="Don"){

setOffAlarms();

return;

}

if(p==="John"){

setOffAlarms();

return;

}

}

return;

}

NowIhavealotofduplicationbetweentheoriginalmodifierandthenewquery,soIcanuseSubstituteAlgorithm(193)sothatthemodifierusesthequery.

functionalertForMiscreant(people){

if(findMiscreant(people)!=="")setOffAlarms();

}

ParameterizeFunction

formerly:ParameterizeMethod

Motivation

IfIseetwofunctionsthatcarryoutverysimilarlogicwithdifferentliteralvalues,Icanremovetheduplicationbyusingasinglefunctionwithparametersforthedifferentvalues.Thisincreasestheusefulnessofthefunction,sinceIcanapplyitelsewherewithdifferentvalues.

Mechanics

Selectoneofthesimilarmethods.

UseChangeFunctionDeclaration(124)toaddanyliteralsthatneedtoturnintoparameters.

Foreachcallerofthefunction,addtheliteralvalue.

Test.

Changethebodyofthefunctiontousethenewparameters.Testaftereachchange.

Foreachsimilarfunction,replacethecallwithacalltotheparameterizedfunction.Testaftereachone.

Iftheoriginalparameterizedfunctiondoesn’tworkforasimilarfunction,adjustitforthenewfunctionbeforemovingontothenext.

Example

Anobviousexampleissomethinglikethis:

functiontenPercentRaise(aPerson){

aPerson.salary=aPerson.salary.multiply(1.1);

}

functionfivePercentRaise(aPerson){

aPerson.salary=aPerson.salary.multiply(1.05);

}

Hopefullyit’sobviousthatIcanreplacethesewith

functionraise(aPerson,factor){

aPerson.salary=aPerson.salary.multiply(1+factor);

}

Butitcanbeabitmoreinvolvedthanthat.Considerthiscode:

functionbaseCharge(usage){

if(usage<0)returnusd(0);

constamount=

bottomBand(usage)*0.03

+middleBand(usage)*0.05

+topBand(usage)*0.07;

returnusd(amount);

}

functionbottomBand(usage){

returnMath.min(usage,100);

}

functionmiddleBand(usage){

returnusage>100?Math.min(usage,200)-100:0;

}

functiontopBand(usage){

returnusage>200?usage-200:0;

}

Herethelogicisclearlyprettysimilar—butisitsimilarenoughtosupportcreatingaparameterizedmethodforthebands?Itis,butmaybeatouchlessobviousthanthetrivialcaseabove.

Whenlookingtoparameterizesomerelatedfunctions,myapproachistotakeoneofthefunctionsandaddparameterstoit,withaneyetotheothercases.Withrange-orientedthingslikethis,usuallytheplacetostartiswiththemiddlerange.SoI’llworkonmiddleBandtochangeittouseparameters,andthenadjustothercallerstofit.

middleBandusestwoliteralvalues:100and200.Theserepresentthebottomandtopofthismiddleband.IbeginbyusingChangeFunctionDeclaration(124)toaddthemtothecall.WhileI’matit,I’llalsochangethenameofthefunctiontosomethingthatmakessensewiththeparameterization.

functionwithinBand(usage,bottom,top){

returnusage>100?Math.min(usage,200)-100:0;

}

functionbaseCharge(usage){

if(usage<0)returnusd(0);

constamount=

bottomBand(usage)*0.03

+withinBand(usage,100,200)*0.05

+topBand(usage)*0.07;

returnusd(amount);

}

Ireplaceeachliteralwithareferencetotheparameter.

functionwithinBand(usage,bottom,top){

returnusage>bottom?Math.min(usage,200)-bottom:0;

}

then

functionwithinBand(usage,bottom,top){

returnusage>bottom?Math.min(usage,top)-bottom:0;

}

Ireplacethecalltothebottombandwithacalltothenewlyparameterizedfunction.

functionbaseCharge(usage){

if(usage<0)returnusd(0);

constamount=

withinBand(usage,0,100)*0.03

+withinBand(usage,100,200)*0.05

+topBand(usage)*0.07;

returnusd(amount);

}

functionbottomBand(usage){

returnMath.min(usage,100);

}

Toreplacethecalltothetopband,Ineedtomakeuseofinfinity.

functionbaseCharge(usage){

if(usage<0)returnusd(0);

constamount=

withinBand(usage,0,100)*0.03

+withinBand(usage,100,200)*0.05

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

returnusd(amount);

}

functiontopBand(usage){

returnusage>200?usage-200:0;

}

Withthelogicworkingthewayitdoesnow,Icouldremovetheinitialguardclause.Butalthoughit’slogicallyunnecessarynow,Iliketokeepitasitdocumentshowtohandlethatcase.

RemoveFlagArgument

formerly:ReplaceParameterwithExplicitMethods

Motivation

Aflagargumentisafunctionargumentthatthecallerusestoindicatewhichlogicthecalledfunctionshouldexecute.Imaycallafunctionthatlookslikethis:

functionbookConcert(aCustomer,isPremium){

if(isPremium){

//logicforpremiumbooking

}else{

//logicforregularbooking

}

}

Tobookapremiumconcert,Iissuethecalllikeso:

bookConcert(aCustomer,true);

Flagargumentscanalsocomeasenums:

bookConcert(aCustomer,CustomerType.PREMIUM);

orstrings(orsymbolsinlanguagesthatusethem):

bookConcert(aCustomer,"premium");

Idislikeflagargumentsbecausetheycomplicatetheprocessofunderstandingwhatfunctioncallsareavailableandhowtocallthem.MyfirstrouteintoanAPIisusuallythelistofavailablefunctions,andflagargumentshidethedifferencesinthefunctioncallsthatareavailable.OnceIselectafunction,Ihavetofigureoutwhatvaluesareavailablefortheflagarguments.Booleanflagsareevenworsesincetheydon’tconveytheirmeaningtothereader—inafunctioncall,Ican’tfigureoutwhattruemeans.It’sclearertoprovideanexplicitfunctionforthetaskIwanttodo.

premiumBookConcert(aCustomer);

Notallargumentslikethisareflagarguments.Tobeaflagargument,thecallersmustbesettingthebooleanvaluetoaliteralvalue,notdatathat’sflowingthroughtheprogram.Also,theimplementationfunctionmustbeusingtheargumenttoinfluenceitscontrolflow,notasdatathatitpassestofurtherfunctions.

Removingflagargumentsdoesn’tjustmakethecodeclearer—italsohelpsmytooling.Codeanalysistoolscannowmoreeasilyseethedifferencebetweencallingthepremiumlogicandcallingregularlogic.

Flagargumentscanhaveaplaceifthere’smorethanoneoftheminthefunction,sinceotherwiseIwouldneedexplicitfunctionsforeverycombinationoftheirvalues.Butthat’salsoasignalofafunctiondoingtoomuch,andIshouldlookforawaytocreatesimplerfunctionsthatIcancomposeforthislogic.

Mechanics

Createanexplicitfunctionforeachvalueoftheparameter.

Ifthemainfunctionhasacleardispatchconditional,useDecomposeConditional(260)tocreatetheexplicitfunctions.Otherwise,createwrappingfunctions.

Foreachcallerthatusesaliteralvaluefortheparameter,replaceitwithacalltotheexplicitfunction.

Example

Lookingthroughsomecode,Iseecallstocalculateadeliverydateforashipment.Someofthecallslooklike

aShipment.deliveryDate=deliveryDate(anOrder,true);

andsomelooklike

aShipment.deliveryDate=deliveryDate(anOrder,false);

Facedwithcodelikethis,Iimmediatelybegintowonderaboutthemeaningofthebooleanvalue.Whatisitdoing?

ThebodyofdeliveryDatelookslikethis:

functiondeliveryDate(anOrder,isRush){

if(isRush){

letdeliveryTime;

if(["MA","CT"].includes(anOrder.deliveryState))deliveryTime=1;

elseif(["NY","NH"].includes(anOrder.deliveryState))deliveryTime=2;

elsedeliveryTime=3;

returnanOrder.placedOn.plusDays(1+deliveryTime);

}

else{

letdeliveryTime;

if(["MA","CT","NY"].includes(anOrder.deliveryState))deliveryTime=2;

elseif(["ME","NH"].includes(anOrder.deliveryState))deliveryTime=3;

elsedeliveryTime=4;

returnanOrder.placedOn.plusDays(2+deliveryTime);

}

}

Here,thecallerisusingaliteralbooleanvaluetodeterminewhichcodeshouldrun—aclassicflagargument.Butthewholepointofusingafunctionistofollowthecaller’sinstructions,soitisbettertoclarifythecaller’sintentwithexplicitfunctions.

Inthiscase,IcandothisbyusingDecomposeConditional(260),whichgivesmethis:

functiondeliveryDate(anOrder,isRush){

if(isRush)returnrushDeliveryDate(anOrder);

elsereturnregularDeliveryDate(anOrder);

}

functionrushDeliveryDate(anOrder){

letdeliveryTime;

if(["MA","CT"].includes(anOrder.deliveryState))deliveryTime=1;

elseif(["NY","NH"].includes(anOrder.deliveryState))deliveryTime=2;

elsedeliveryTime=3;

returnanOrder.placedOn.plusDays(1+deliveryTime);

}

functionregularDeliveryDate(anOrder){

letdeliveryTime;

if(["MA","CT","NY"].includes(anOrder.deliveryState))deliveryTime=2;

elseif(["ME","NH"].includes(anOrder.deliveryState))deliveryTime=3;

elsedeliveryTime=4;

returnanOrder.placedOn.plusDays(2+deliveryTime);

}

Thetwonewfunctionscapturetheintentofthecallbetter,soIcanreplaceeachcallof

aShipment.deliveryDate=deliveryDate(anOrder,true);

with

aShipment.deliveryDate=rushDeliveryDate(anOrder);

andsimilarlywiththeothercase.

WhenI’vereplacedallthecallers,IremovedeliveryDate.

Aflagargumentisn’tjustthepresenceofabooleanvalue;it’sthatthebooleanissetwithaliteralratherthandata.IfallthecallersofdeliveryDatewerelikethis

constisRush=determineIfRush(anOrder);

aShipment.deliveryDate=deliveryDate(anOrder,isRush);

thenI’dhavenoproblemwithdeliveryDate’ssignature(althoughI’dstillwanttoapplyDecomposeConditional(260)).

Itmaybethatsomecallersusetheargumentasaflagargumentbysettingitwithaliteral,whileotherssettheargumentwithdata.Inthiscase,I’dstilluseRemoveFlagArgument,butnotchangethedatacallersandnotremovedeliveryDateattheend.ThatwayIsupportbothinterfacesforthedifferentuses.

Decomposingtheconditionallikethisisagoodwaytocarryoutthisrefactoring,butitonlyworksifthedispatchontheparameteristheouterpartofthefunction(orIcaneasilyrefactorittomakeitso).It’salsopossiblethattheparameterisusedinamuchmoretangledway,suchasthisalternativeversionofdeliveryDate:

functiondeliveryDate(anOrder,isRush){

letresult;

letdeliveryTime;

if(anOrder.deliveryState==="MA"||anOrder.deliveryState==="CT")

deliveryTime=isRush?1:2;

elseif(anOrder.deliveryState==="NY"||anOrder.deliveryState==="NH"){

deliveryTime=2;

if(anOrder.deliveryState==="NH"&&!isRush)

deliveryTime=3;

}

elseif(isRush)

deliveryTime=3;

elseif(anOrder.deliveryState==="ME")

deliveryTime=3;

else

deliveryTime=4;

result=anOrder.placedOn.plusDays(2+deliveryTime);

if(isRush)result=result.minusDays(1);

returnresult;

}

Inthiscase,teasingoutisRushintoatop-leveldispatchconditionalislikelymoreworkthanIfancy.Soinstead,IcanlayerfunctionsoverthedeliveryDate:

functionrushDeliveryDate(anOrder){returndeliveryDate(anOrder,true);}

functionregularDeliveryDate(anOrder){returndeliveryDate(anOrder,false);}

ThesewrappingfunctionsareessentiallypartialapplicationsofdeliveryDate,althoughtheyaredefinedinprogramtextratherthanbycompositionoffunctions.

IcanthendothesamereplacementofcallersthatIdidwiththedecomposedconditionalearlieron.Iftherearen’tanycallersusingtheparameterasdata,Iliketorestrictitsvisibilityorrenameittoanamethatconveysthatitshouldn’tbeuseddirectly(e.g.,deliveryDateHelperOnly)

PreserveWholeObject

Motivation

IfIseecodethatderivesacoupleofvaluesfromarecordandthenpassesthesevaluesintoafunction,Iliketoreplacethosevalueswiththewholerecorditself,lettingthefunctionbodyderivethevaluesitneeds.

Passingthewholerecordhandleschangebettershouldthecalledfunctionneedmoredatafromthewholeinthefuture—thatchangewouldnotrequiremetoaltertheparameterlist.Italsoreducesthesizeoftheparameterlist,whichusuallymakesthefunctioncalleasiertounderstand.Ifmanyfunctionsarecalledwiththeparts,theyoftenduplicatethelogicthatmanipulatestheseparts—logicthatcanoftenbemovedtothewhole.

ThemainreasonIwouldn’tdothisisifIdon’twantthecalledfunctiontohaveadependencyonthewhole—whichtypicallyoccurswhentheyareindifferentmodules.

Pullingseveralvaluesfromanobjecttodosomelogiconthemaloneisasmell(FeatureEnvy,p.75),andusuallyasignalthatthislogicshouldbemovedintothewholeitself.PreserveWholeObjectisparticularlycommonafterI’vedoneIntroduceParameterObject(140),asIhuntdownanyoccurrencesoftheoriginaldataclumptoreplacethemwiththenewobject.

Ifseveralbitsofcodeonlyusethesamesubsetofanobject’sfeatures,thenthatmayindicateagoodopportunityforExtractClass(180).

Onecasethatmanypeoplemissiswhenanobjectcallsanotherobjectwithseveralofitsowndatavalues.IfIseethis,Icanreplacethosevalueswithaself-reference(thisinJavaScript).

Mechanics

Createanemptyfunctionwiththedesiredparameters.

Givethefunctionaneasilysearchablenamesoitcanbereplacedattheend.

Fillthebodyofthenewfunctionwithacalltotheoldfunction,mappingfromthenewparameterstotheoldones.

Runstaticchecks.

Adjusteachcallertousethenewfunction,testingaftereachchange.

Thismaymeanthatsomecodethatderivestheparameterisn’tneeded,socanfalltoRemoveDeadCode(236).

Oncealloriginalcallershavebeenchanged,useInlineFunction(115)ontheoriginalfunction.

Changethenameofthenewfunctionandallitscallers.

Example

Consideraroommonitoringsystem.Itcomparesitsdailytemperaturerangewitharangeinapredefinedheatingplan.

caller…

constlow=aRoom.daysTempRange.low;

consthigh=aRoom.daysTempRange.high;

if(!aPlan.withinRange(low,high))

alerts.push("roomtemperaturewentoutsiderange");

classHeatingPlan…

withinRange(bottom,top){

return(bottom>=this._temperatureRange.low)&&(top<=this._temperatureRange.high);

}

InsteadofunpackingtherangeinformationwhenIpassitin,Icanpassinthewholerangeobject.

IbeginbystatingtheinterfaceIwantasanemptyfunction.

classHeatingPlan…

xxNEWwithinRange(aNumberRange){

}

SinceIintendittoreplacetheexistingwithinRange,Inameitthesamebutwithaneasilyreplaceableprefix.

Ithenaddthebodyofthefunction,whichreliesoncallingtheexisting

withinRange.Thebodythusconsistsofamappingfromthenewparametertotheexistingones.

classHeatingPlan…

xxNEWwithinRange(aNumberRange){

returnthis.withinRange(aNumberRange.low,aNumberRange.high);

}

NowIcanbegintheseriouswork,takingtheexistingfunctioncallsandhavingthemcallthenewfunction.

caller…

constlow=aRoom.daysTempRange.low;

consthigh=aRoom.daysTempRange.high;

if(!aPlan.xxNEWwithinRange(aRoom.daysTempRange))

alerts.push("roomtemperaturewentoutsiderange");

WhenI’vechangedthecalls,Imayseethatsomeoftheearliercodeisn’tneededanymore,soIwieldRemoveDeadCode(236).

caller…

constlow=aRoom.daysTempRange.low;

consthigh=aRoom.daysTempRange.high;

if(!aPlan.xxNEWwithinRange(aRoom.daysTempRange))

alerts.push("roomtemperaturewentoutsiderange");

Ireplacetheseoneatatime,testingaftereachchange.

OnceI’vereplacedthemall,IcanuseInlineFunction(115)ontheoriginalfunction.

classHeatingPlan…

xxNEWwithinRange(aNumberRange){

return(aNumberRange.low>=this._temperatureRange.low)&&

(aNumberRange.high<=this._temperatureRange.high);

}

AndIfinallyremovethatuglyprefixfromthenewfunctionandallitscallers.Theprefixmakesitasimpleglobalreplace,evenifIdon’thavearobustrenamesupportinmyeditor.

classHeatingPlan…

withinRange(aNumberRange){

return(aNumberRange.low>=this._temperatureRange.low)&&

(aNumberRange.high<=this._temperatureRange.high);

}

caller…

if(!aPlan.withinRange(aRoom.daysTempRange))

alerts.push("roomtemperaturewentoutsiderange");

Example:AVariationtoCreatetheNewFunction

Intheaboveexample,Iwrotethecodeforthenewfunctiondirectly.Mostofthetime,that’sprettysimpleandtheeasiestwaytogo.Butthereisavariationonthisthat’soccasionallyuseful—whichcanallowmetocomposethenewfunctionentirelyfromrefactorings.

Istartwithacalleroftheexistingfunction.

caller…

constlow=aRoom.daysTempRange.low;

consthigh=aRoom.daysTempRange.high;

if(!aPlan.withinRange(low,high))

alerts.push("roomtemperaturewentoutsiderange");

IwanttorearrangethecodesoIcancreatethenewfunctionbyusingExtractFunction(106)onsomeexistingcode.Thecallercodeisn’tquitethereyet,butIcangettherebyusingExtractVariable(119)afewtimes.First,Idisentanglethecalltotheoldfunctionfromtheconditional.

caller…

constlow=aRoom.daysTempRange.low;

consthigh=aRoom.daysTempRange.high;

constisWithinRange=aPlan.withinRange(low,high);

if(!isWithinRange)

alerts.push("roomtemperaturewentoutsiderange");

Ithenextracttheinputparameter.

caller…

consttempRange=aRoom.daysTempRange;

constlow=tempRange.low;

consthigh=tempRange.high;

constisWithinRange=aPlan.withinRange(low,high);

if(!isWithinRange)

alerts.push("roomtemperaturewentoutsiderange");

Withthatdone,IcannowuseExtractFunction(106)tocreatethenewfunction.

caller…

consttempRange=aRoom.daysTempRange;

constisWithinRange=xxNEWwithinRange(aPlan,tempRange);

if(!isWithinRange)

alerts.push("roomtemperaturewentoutsiderange");

toplevel…

functionxxNEWwithinRange(aPlan,tempRange){

constlow=tempRange.low;

consthigh=tempRange.high;

constisWithinRange=aPlan.withinRange(low,high);

returnisWithinRange;

}

Sincetheoriginalfunctionisinadifferentcontext(theHeatingPlanclass),IneedtouseMoveFunction(196).

caller…

consttempRange=aRoom.daysTempRange;

constisWithinRange=aPlan.xxNEWwithinRange(tempRange);

if(!isWithinRange)

alerts.push("roomtemperaturewentoutsiderange");

classHeatingPlan…

xxNEWwithinRange(tempRange){

constlow=tempRange.low;

consthigh=tempRange.high;

constisWithinRange=this.withinRange(low,high);

returnisWithinRange;

}

Ithencontinueasbefore,replacingothercallersandinliningtheoldfunctionintothenewone.IwouldalsoinlinethevariablesIextractedtoprovidethecleanseparationforextractingthenewfunction.

Becausethisvariationisentirelycomposedofrefactorings,it’sparticularlyhandywhenIhavearefactoringtoolwithrobustextractandinlineoperations.

ReplaceParameterwithQuery

formerly:ReplaceParameterwithMethod

inverseof:ReplaceQuerywithParameter(325)

Motivation

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

Ifacallpassesinavaluethatthefunctioncanjustaseasilydetermineforitself,

that’saformofduplication—onethatunnecessarilycomplicatesthecallerwhichhastodeterminethevalueofaparameterwhenitcouldbefreedfromthatwork.

Thelimitonthisissuggestedbythephrase“justaseasily.”Byremovingtheparameter,I’mshiftingtheresponsibilityfordeterminingtheparametervalue.Whentheparameterispresent,determiningitsvalueisthecaller’sresponsibility;otherwise,thatresponsibilityshiftstothefunctionbody.Myusualhabitistosimplifylifeforcallers,whichimpliesmovingresponsibilitytothefunctionbody—butonlyifthatresponsibilityisappropriatethere.

ThemostcommonreasontoavoidReplaceParameterwithQueryisifremovingtheparameteraddsanunwanteddependencytothefunctionbody—forcingittoaccessaprogramelementthatI’dratheritremainedignorantof.Thismaybeanewdependency,oranexistingonethatI’dliketoremove.UsuallythiscomesupwhereI’dneedtoaddaproblematicfunctioncalltothefunctionbody,oraccesssomethingwithinareceiverobjectthatI’dprefertomoveoutlater.

ThesafestcaseforReplaceParameterwithQueryiswhenthevalueoftheparameterIwanttoremoveisdeterminedmerelybyqueryinganotherparameterinthelist.There’srarelyanypointinpassingtwoparametersifonecanbedeterminedfromtheother.

OnethingtowatchoutforisifthefunctionI’mlookingathasreferentialtransparency—thatis,ifIcanbesurethatitwillbehavethesamewaywheneverit’scalledwiththesameparametervalues.Suchfunctionsaremucheasiertoreasonaboutandtest,andIdon’twanttoalterthemtolosethatproperty.SoIwouldn’treplaceaparameterwithanaccesstoamutableglobalvariable.

Mechanics

Ifnecessary,useExtractFunction(106)onthecalculationoftheparameter.

Replacereferencestotheparameterinthefunctionbodywithreferencestotheexpressionthatyieldstheparameter.Testaftereachchange.

UseChangeFunctionDeclaration(124)toremovetheparameter.

Example

ImostoftenuseReplaceParameterwithQuerywhenI’vedonesomeotherrefactoringsthatmakeaparameternolongerneeded.Considerthiscode.

classOrder…

getfinalPrice(){

constbasePrice=this.quantity*this.itemPrice;

letdiscountLevel;

if(this.quantity>100)discountLevel=2;

elsediscountLevel=1;

returnthis.discountedPrice(basePrice,discountLevel);

}

discountedPrice(basePrice,discountLevel){

switch(discountLevel){

case1:returnbasePrice*0.95;

case2:returnbasePrice*0.9;

}

}

WhenI’msimplifyingafunction,I’mkeentoapplyReplaceTempwithQuery(176),whichwouldleadmeto

classOrder…

getfinalPrice(){

constbasePrice=this.quantity*this.itemPrice;

returnthis.discountedPrice(basePrice,this.discountLevel);

}

getdiscountLevel(){

return(this.quantity>100)?2:1;

}

OnceI’vedonethis,there’snoneedtopasstheresultofdiscountLeveltodiscountedPrice—itcanjustaseasilymakethecallitself.

Ireplaceanyreferencetotheparameterwithacalltothemethodinstead.

classOrder…

discountedPrice(basePrice,discountLevel){

switch(this.discountLevel){

case1:returnbasePrice*0.95;

case2:returnbasePrice*0.9;

}

}

IcanthenuseChangeFunctionDeclaration(124)toremovetheparameter.

classOrder…

getfinalPrice(){

constbasePrice=this.quantity*this.itemPrice;

returnthis.discountedPrice(basePrice,this.discountLevel);

}

discountedPrice(basePrice,discountLevel){

switch(this.discountLevel){

case1:returnbasePrice*0.95;

case2:returnbasePrice*0.9;

}

}

ReplaceQuerywithParameter

inverseof:ReplaceParameterwithQuery(322)

Motivation

Whenlookingthroughafunction’sbody,Isometimesseereferencestosomethinginthefunction’sscopethatI’mnothappywith.Thismightbeareferencetoaglobalvariable,ortoanelementinthesamemodulethatIintendtomoveaway.Toresolvethis,Ineedtoreplacetheinternalreferencewithaparameter,shiftingtheresponsibilityofresolvingthereferencetothecallerofthefunction.

Mostofthesecasesareduetomywishtoalterthedependencyrelationshipsinthecode—tomakethetargetfunctionnolongerdependentontheelementIwanttoparameterize.There’satensionherebetweenconvertingeverythingtoparameters,whichresultsinlongrepetitiveparameterlists,andsharingalotofscopewhichcanleadtoalotofcouplingbetweenfunctions.Likemosttrickydecisions,it’snotsomethingIcanreliablygetright,soit’simportantthatIcanreliablychangethingssotheprogramcantakeadvantageofmyincreasingunderstanding.

It’seasiertoreasonaboutafunctionthatwillalwaysgivethesameresultwhencalledwithsameparametervalues—thisiscalledreferentialtransparency.Ifafunctionaccessessomeelementinitsscopethatisn’treferentiallytransparent,thenthecontainingfunctionalsolacksreferentialtransparency.Icanfixthatbymovingthatelementtoaparameter.Althoughsuchamovewillshiftresponsibilitytothecaller,thereisoftenalottobegainedbycreatingclearmoduleswithreferentialtransparency.AcommonpatternistohavemodulesconsistingofpurefunctionswhicharewrappedbylogicthathandlestheI/Oandothervariableelementsofaprogram.IcanuseReplaceQuerywithParametertopurifypartsofaprogram,makingthosepartseasiertotestandreasonabout.

ButReplaceQuerywithParameterisn’tjustabagofbenefits.Bymovingaquerytoaparameter,Iforcemycallertofigureouthowtoprovidethisvalue.Thiscomplicateslifeforcallersofthefunctions,andmyusualbiasistodesigninterfacesthatmakelifeeasierfortheirconsumers.Intheend,itboilsdowntoallocationofresponsibilityaroundtheprogram,andthat’sadecisionthat’sneithereasynorimmutable—whichiswhythisrefactoring(anditsinverse)isonethatIneedtobeveryfamiliarwith.

Mechanics

UseExtractVariable(119)onthequerycodetoseparateitfromtherestofthefunctionbody.

ApplyExtractFunction(106)tothebodycodethatisn’tthecalltothequery.

Givethenewfunctionaneasilysearchablename,forlaterrenaming.

UseInlineVariable(123)togetridofthevariableyoujustcreated.

ApplyInlineFunction(115)totheoriginalfunction.

Renamethenewfunctiontothatoftheoriginal.

Example

Considerasimple,yetannoying,controlsystemfortemperature.Itallowstheusertoselectatemperatureonathermostat—butonlysetsthetargettemperaturewithinarangedeterminedbyaheatingplan.

classHeatingPlan…

gettargetTemperature(){

if(thermostat.selectedTemperature>this._max)returnthis._max;

elseif(thermostat.selectedTemperature<this._min)returnthis._min;

elsereturnthermostat.selectedTemperature;

}

caller…

if(thePlan.targetTemperature>thermostat.currentTemperature)setToHeat();

elseif(thePlan.targetTemperature<thermostat.currentTemperature)setToCool();

elsesetOff();

Asauserofsuchasystem,Imightbeannoyedtohavemydesiresoverriddenbytheheatingplanrules,butasaprogrammerImightbemoreconcernedabouthowthetargetTemperaturefunctionhasadependencyonaglobalthermostatobject.Icanbreakthisdependencybymovingittoaparameter.

MyfirststepistouseExtractVariable(119)ontheparameterthatIwanttohaveinmyfunction.

classHeatingPlan…

gettargetTemperature(){

constselectedTemperature=thermostat.selectedTemperature;

if(selectedTemperature>this._max)returnthis._max;

elseif(selectedTemperature<this._min)returnthis._min;

elsereturnselectedTemperature;

}

ThatmakesiteasytoapplyExtractFunction(106)ontheentirebodyofthefunctionexceptforthebitthatfiguresouttheparameter.

classHeatingPlan…

gettargetTemperature(){

constselectedTemperature=thermostat.selectedTemperature;

returnthis.xxNEWtargetTemperature(selectedTemperature);

}

xxNEWtargetTemperature(selectedTemperature){

if(selectedTemperature>this._max)returnthis._max;

elseif(selectedTemperature<this._min)returnthis._min;

elsereturnselectedTemperature;

}

ItheninlinethevariableIjustextracted,whichleavesthefunctionasasimplecall.

classHeatingPlan…

gettargetTemperature(){

returnthis.xxNEWtargetTemperature(thermostat.selectedTemperature);

}

IcannowuseInlineFunction(115)onthismethod.

caller…

if(thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature)

thermostat.currentTemperature)

setToHeat();

elseif(thePlan.xxNEWtargetTemperature(thermostat.selectedTemperature)

thermostat.currentTemperature)

setToCool();

else

setOff();

Itakeadvantageoftheeasilysearchablenameofthenewfunctiontorenameit

byremovingtheprefix.

caller…

if(thePlan.targetTemperature(thermostat.selectedTemperature)>

thermostat.currentTemperature)

setToHeat();

elseif(thePlan.targetTemperature(thermostat.selectedTemperature)<

thermostat.currentTemperature)

setToCool();

else

setOff();

classHeatingPlan…

targetTemperature(selectedTemperature){

if(selectedTemperature>this._max)returnthis._max;

elseif(selectedTemperature<this._min)returnthis._min;

elsereturnselectedTemperature;

}

Asisoftenthecasewiththisrefactoring,thecallingcodelooksmoreunwieldythanbefore.Movingadependencyoutofamodulepushestheresponsibilityofdealingwiththatdependencybacktothecaller.That’sthetrade-offforthereducedcoupling.

Butremovingthecouplingtothethermostatobjectisn’ttheonlygainI’vemadewiththisrefactoring.TheHeatingPlanclassisimmutable—itsfieldsaresetintheconstructorwithnomethodstoalterthem.(I’llsaveyoutheeffortoflookingatthewholeclass;justtrustmeonthis.)Givenanimmutableheatingplan,bymovingthethermostatreferenceoutofthefunctionbodyI’vealsomadetargetTemperaturereferentiallytransparent.EverytimeIcalltargetTemperatureonthesameobject,withthesameargument,Iwillgetthesameresult.Ifallthemethodsoftheheatingplanhavereferentialtransparency,thatmakesthisclassmucheasiertotestandreasonabout.

AproblemwithJavaScript’sclassmodelisthatit’simpossibletoenforceanimmutableclass—there’salwaysawaytogetatanobject’sdata.Butwritingaclasstosignalandencourageimmutabilityisoftengoodenough.CreatingclassesthathavethischaracteristicisoftenasoundstrategyandReplaceQuerywithParameterisahandytoolfordoingthis.

RemoveSettingMethod

Motivation

Providingasettingmethodindicatesthatafieldmaybechanged.IfIdon’twantthatfieldtochangeoncetheobjectiscreated,Idon’tprovideasettingmethod(andmakethefieldimmutable).Thatway,thefieldissetonlyintheconstructor,myintentiontohaveitnotchangeisclear,andIusuallyremovetheverypossibilitythatthefieldwillchange.

There’sacoupleofcommoncaseswherethiscomesup.Oneiswherepeoplealwaysuseaccessormethodstomanipulateafield,evenwithinconstructors.Thisleadstotheonlycalltoasettingmethodbeingfromtheconstructor.Iprefertoremovethesettingmethodtomakeitclearthatupdatesmakenosenseafterconstruction.

Anothercaseiswheretheobjectiscreatedbyclientsusingcreationscriptratherthanbyasimpleconstructorcall.Suchacreationscriptstartswiththeconstructorcallfollowedbyasequenceofsettermethodcallstocreatethenewobject.Oncethescriptisfinished,wedon’texpectthenewobjecttochangesome(orevenall)ofitsfields.Thesettersareonlyexpectedtobecalledduringthisinitialcreation.Inthiscase,I’dgetridofthemtomakemyintentions

clearer.

Mechanics

Ifthevaluethat’sbeingsetisn’tprovidedtotheconstructor,useChangeFunctionDeclaration(124)toaddit.Addacalltothesettingmethodwithintheconstructor.

Ifyouwishtoremoveseveralsettingmethods,addalltheirvaluestotheconstructoratonce.Thissimplifiesthelatersteps.

Removeeachcallofasettingmethodoutsideoftheconstructor,usingthenewconstructorvalueinstead.Testaftereachone.

Ifyoucan’treplacethecalltothesetterbycreatinganewobject(becauseyouareupdatingasharedreferenceobject),abandontherefactoring.

UseInlineFunction(115)onthesettingmethod.Makethefieldimmutableifpossible.

Test.

Example

IhaveasimplePersonclass.

classPerson…

getname(){returnthis._name;}

setname(arg){this._name=arg;}

getid(){returnthis._id;}

setid(arg){this._id=arg;}

Atthemoment,Icreateanewobjectwithcodelikethis:

constmartin=newPerson();

martin.name="martin";

martin.id="1234";

Thenameofapersonmaychangeafterit’screated,buttheIDdoesnot.Tomakethisclear,IwanttoremovethesettingmethodforID.

IstillneedtosettheIDinitially,soI’lluseChangeFunctionDeclaration(124)toaddittotheconstructor.

classPerson…

constructor(id){

this.id=id;

}

IthenadjustthecreationscripttosettheIDviatheconstructor.

constmartin=newPerson("1234");

martin.name="martin";

martin.id="1234";

IdothisineachplaceIcreateaperson,testingaftereachchange.

Whentheyarealldone,IcanapplyInlineFunction(115)tothesettingmethod.

classPerson…

constructor(id){

this._id=id;

}

getname(){returnthis._name;}

setname(arg){this._name=arg;}

getid(){returnthis._id;}

setid(arg){this._id=arg;}

ReplaceConstructorwithFactoryFunction

formerly:ReplaceConstructorwithFactoryMethod

Motivation

Manyobject-orientedlanguageshaveaspecialconstructorfunctionthat’scalledtoinitializeanobject.Clientstypicallycallthisconstructorwhentheywanttocreateanewobject.Buttheseconstructorsoftencomewithawkwardlimitationsthataren’tthereformoregeneralfunctions.AJavaconstructormustreturnaninstanceoftheclassitwascalledwith,whichmeansIcan’treplaceitwithasubclassorproxydependingontheenvironmentorparameters.Constructornamingisfixed,whichmakesitimpossibleformetouseanamethatisclearerthanthedefault.Constructorsoftenrequireaspecialoperatortoinvoke(“new”inmanylanguages)whichmakesthemdifficulttouseincontextsthatexpectnormalfunctions.

Afactoryfunctionsuffersfromnosuchlimitations.Itwilllikelycalltheconstructoraspartofitsimplementation,butIcanfreelysubstitutesomethingelse.

Mechanics

Createafactoryfunction,itsbodybeingacalltotheconstructor.

Replaceeachcalltotheconstructorwithacalltothefactoryfunction.

Testaftereachchange.

Limittheconstructor’svisibilityasmuchaspossible.

Example

Aquickbutwearisomeexampleuseskindsofemployees.Consideranemployeeclass:

classEmployee…

constructor(name,typeCode){

this._name=name;

this._typeCode=typeCode;

}

getname(){returnthis._name;}

gettype(){

returnEmployee.legalTypeCodes[this._typeCode];

}

staticgetlegalTypeCodes(){

return{"E":"Engineer","M":"Manager","S":"Salesman"};

}

Thisisusedfrom

caller…

candidate=newEmployee(document.name,document.empType);

and

caller…

constleadEngineer=newEmployee(document.leadEngineer,'E');

Myfirststepistocreatethefactoryfunction.Itsbodyisasimpledelegationtotheconstructor.

toplevel…

functioncreateEmployee(name,typeCode){

returnnewEmployee(name,typeCode);

}

Ithenfindthecallersoftheconstructorandchangethem,oneatatime,tousethefactoryfunctioninstead.

Thefirstoneisobvious:

caller…

candidate=createEmployee(document.name,document.empType);

Withthesecondcase,Icouldusethenewfactoryfunctionlikethis:

caller…

constleadEngineer=createEmployee(document.leadEngineer,'E');

ButIdon’tlikeusingthetypecodehere—it’sgenerallyabadsmelltopassacodeasaliteralstring.SoIprefertocreateanewfactoryfunctionthatembedsthekindofemployeeIwantintoitsname.

caller…

constleadEngineer=createEngineer(document.leadEngineer);

toplevel…

functioncreateEngineer(name){

returnnewEmployee(name,'E');

}

ReplaceFunctionwithCommand

formerly:ReplaceMethodwithMethodObject

inverseof:ReplaceCommandwithFunction(342)

Motivation

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

Acommandoffersagreaterflexibilityforthecontrolandexpressionofafunctionthantheplainfunctionmechanism.Commandscanhavecomplimentaryoperations,suchasundo.Icanprovidemethodstobuilduptheirparameterstosupportaricherlifecycle.Icanbuildincustomizationsusinginheritanceandhooks.IfI’mworkinginalanguagewithobjectsbutwithoutfirst-classfunctions,Icanprovidemuchofthatcapabilitybyusingcommandsinstead.Similarly,Icanusemethodsandfieldstohelpbreakdownacomplexfunction,eveninalanguagethatlacksnestedfunctions,andIcancallthosemethodsdirectlywhiletestinganddebugging.

Allthesearegoodreasonstousecommands,andIneedtobereadytorefactorfunctionsintocommandswhenIneedto.Butwemustnotforgetthatthisflexibility,asever,comesatapricepaidincomplexity.So,giventhechoicebetweenafirst-classfunctionandacommand,I’llpickthefunction95%ofthetime.IonlyuseacommandwhenIspecificallyneedafacilitythatsimplerapproachescan’tprovide.

Likemanywordsinsoftwaredevelopment,“command”isratheroverloaded.InthecontextI’musingithere,itisanobjectthatencapsulatesarequest,followingthecommandpatterninDesignPatterns[bib-gof].WhenIuse“command”inthissense,Iuse“commandobject”tosetthecontext,and“command”afterwards.Theword“command”isalsousedinthecommand-queryseparationprinciple[bib-cqs],whereacommandisanobjectmethodthatchangesobservablestate.I’vealwaystriedtoavoidusingcommandinthatsense,preferring“modifier”or“mutator”.

Mechanics

Createanemptyclassforthefunction.Nameitbasedonthefunction.

UseMoveFunction(196)tomovethefunctiontotheemptyclass.

Keeptheoriginalfunctionasaforwardingfunctionuntilatleasttheendoftherefactoring.

Followanyconventionthelanguagehasfornamingcommands.Ifthereisnoconvention,chooseagenericnameforthecommand’sexecutefunction,suchas“execute”or“call”.

Considermakingafieldforeachargument,andmovetheseargumentstotheconstructor.

Example

TheJavaScriptlanguagehasmanyfaults,butoneofitsgreatdecisionswastomakefunctionsfirst-classentities.Ithusdon’thavetogothroughallthehoopsofcreatingcommandsforcommontasksthatIneedtodoinlanguageswithoutthisfacility.Buttherearestilltimeswhenacommandistherighttoolforthejob.

OneofthesecasesisbreakingupacomplexfunctionsoIcanbetterunderstandandmodifyit.Toreallyshowthevalueofthisrefactoring,Ineedalongandcomplicatedfunction—butthatwouldtaketoolongtowrite,letaloneforyoutoread.Instead,I’llgowithafunctionthat’sshortenoughnottoneedit.Thisonescorespointsforaninsuranceapplication.

functionscore(candidate,medicalExam,scoringGuide){

letresult=0;

lethealthLevel=0;

lethighMedicalRiskFlag=false;

if(medicalExam.isSmoker){

healthLevel+=10;

highMedicalRiskFlag=true;

}

letcertificationGrade="regular";

if(scoringGuide.stateWithLowCertification(candidate.originState)){

certificationGrade="low";

result-=5;

}

//lotsmorecodelikethis

result-=Math.max(healthLevel-5,0);

returnresult;

}

IbeginbycreatinganemptyclassandthenMoveFunction(196)tomovethefunctionintoit.

functionscore(candidate,medicalExam,scoringGuide){

returnnewScorer().execute(candidate,medicalExam,scoringGuide);

}

classScorer{

execute(candidate,medicalExam,scoringGuide){

letresult=0;

lethealthLevel=0;

lethighMedicalRiskFlag=false;

if(medicalExam.isSmoker){

healthLevel+=10;

highMedicalRiskFlag=true;

}

letcertificationGrade="regular";

if(scoringGuide.stateWithLowCertification(candidate.originState)){

certificationGrade="low";

result-=5;

}

//lotsmorecodelikethis

result-=Math.max(healthLevel-5,0);

returnresult;

}

}

Mostofthetime,Iprefertopassargumentstoacommandontheconstructorandhavetheexecutemethodtakenoparameters.Whilethismatterslessforasimpledecompositionscenariolikethis,it’sveryhandywhenIwanttomanipulatethecommandwithamorecomplicatedparametersettinglifecycleorcustomizations.Differentcommandclassescanhavedifferentparametersbutbemixedtogetherwhenqueuedforexecution.

Icandotheseparametersoneatatime.

functionscore(candidate,medicalExam,scoringGuide){

returnnewScorer(candidate).execute(candidate,medicalExam,scoringGuide);

}

classScorer…

constructor(candidate){

this._candidate=candidate;

}

execute(candidate,medicalExam,scoringGuide){

letresult=0;

lethealthLevel=0;

lethighMedicalRiskFlag=false;

if(medicalExam.isSmoker){

healthLevel+=10;

highMedicalRiskFlag=true;

}

letcertificationGrade="regular";

if(scoringGuide.stateWithLowCertification(this._candidate.originState)){

certificationGrade="low";

result-=5;

}

//lotsmorecodelikethis

result-=Math.max(healthLevel-5,0);

returnresult;

}

Icontinuewiththeotherparameters

functionscore(candidate,medicalExam,scoringGuide){

returnnewScorer(candidate,medicalExam,scoringGuide).execute();

}

classScorer…

constructor(candidate,medicalExam,scoringGuide){

this._candidate=candidate;

this._medicalExam=medicalExam;

this._scoringGuide=scoringGuide;

}

execute(){

letresult=0;

lethealthLevel=0;

lethighMedicalRiskFlag=false;

if(this._medicalExam.isSmoker){

healthLevel+=10;

highMedicalRiskFlag=true;

}

letcertificationGrade="regular";

if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){

certificationGrade="low";

result-=5;

}

//lotsmorecodelikethis

result-=Math.max(healthLevel-5,0);

returnresult;

}

ThatcompletesReplaceFunctionwithCommand,butthewholepointofdoingthisrefactoringistoallowmetobreakdownthecomplicatedfunctions—soletmeoutlinesomestepstoachievethat.Mynextmovehereistochangeallthe

localvariablesintofields.Again,Idotheseoneatatime.

classScorer…

constructor(candidate,medicalExam,scoringGuide){

this._candidate=candidate;

this._medicalExam=medicalExam;

this._scoringGuide=scoringGuide;

}

execute(){

this._result=0;

lethealthLevel=0;

lethighMedicalRiskFlag=false;

if(this._medicalExam.isSmoker){

healthLevel+=10;

highMedicalRiskFlag=true;

}

letcertificationGrade="regular";

if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){

certificationGrade="low";

this._result-=5;

}

//lotsmorecodelikethis

this._result-=Math.max(healthLevel-5,0);

returnthis._result;

}

Irepeatthisforallthelocalvariables.(ThisisoneofthoserefactoringsthatIfeltwassufficientlysimplethatIhaven’tgivenitanentryinthecatalog.Ifeelslightlyguiltyaboutthis.)

classScorer…

constructor(candidate,medicalExam,scoringGuide){

this._candidate=candidate;

this._medicalExam=medicalExam;

this._scoringGuide=scoringGuide;

}

execute(){

this._result=0;

this._healthLevel=0;

this._highMedicalRiskFlag=false;

if(this._medicalExam.isSmoker){

this._healthLevel+=10;

this._highMedicalRiskFlag=true;

}

this._certificationGrade="regular";

if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){

this._certificationGrade="low";

this._result-=5;

}

//lotsmorecodelikethis

this._result-=Math.max(this._healthLevel-5,0);

returnthis._result;

}

NowI’vemovedallthefunction’sstatetothecommandobject,IcanuserefactoringslikeExtractFunction(106)withoutgettingtangledupinallthevariablesandtheirscopes.

classScorer…

constructor(candidate,medicalExam,scoringGuide){

this._candidate=candidate;

this._medicalExam=medicalExam;

this._scoringGuide=scoringGuide;

}

execute(){

this._result=0;

this._healthLevel=0;

this._highMedicalRiskFlag=false;

this.scoreSmoking();

this._certificationGrade="regular";

if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){

this._certificationGrade="low";

this._result-=5;

}

//lotsmorecodelikethis

this._result-=Math.max(this._healthLevel-5,0);

returnthis._result;

}

scoreSmoking(){

if(this._medicalExam.isSmoker){

this._healthLevel+=10;

this._highMedicalRiskFlag=true;

}

}

ThisallowsmetotreatthecommandsimilarlytohowI’ddealwithanested

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

ReplaceCommandwithFunction

inverseof:ReplaceFunctionwithCommand(335)

Motivation

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

itsthing.Ifthat’sthecase,andthefunctionisn’ttoocomplex,thenacommandobjectismoretroublethanitsworthandshouldbeturnedintoaregularfunction.

Mechanics

ApplyExtractFunction(106)tothecreationofthecommandandthecalltothecommand’sexecutionmethod.

Thiscreatesthenewfunctionthatwillreplacethecommandinduecourse.

Foreachmethodcalledbythecommand’sexecutionmethod,applyInlineFunction(115).

Ifthesupportingfunctionreturnsavalue,useExtractVariable(119)onthecallfirstandthenInlineFunction(115).

UseChangeFunctionDeclaration(124)toputalltheparametersoftheconstructorintothecommand’sexecutionmethodinstead.

Foreachfield,alterthereferencesinthecommand’sexecutionmethodtousetheparameterinstead.Testaftereachchange.

Inlinetheconstructorcallandcommand’sexecutionmethodcallintothecaller(whichisthereplacementfunction).

Test.

ApplyRemoveDeadCode(236)tothecommandclass.

Example

I’llbeginwiththissmallcommandobject.

classChargeCalculator{

constructor(customer,usage,provider){

this._customer=customer;

this._usage=usage;

this._provider=provider;

}

getbaseCharge(){

returnthis._customer.baseRate*this._usage;

}

getcharge(){

returnthis.baseCharge+this._provider.connectionCharge;

}

}

Itisusedbycodelikethis:

caller…

monthCharge=newChargeCalculator(customer,usage,provider).charge;

Thecommandclassissmallandsimpleenoughtobebetteroffasafunction.

IbeginbyusingExtractFunction(106)towraptheclasscreationandinvocation.

caller…

monthCharge=charge(customer,usage,provider);

toplevel…

functioncharge(customer,usage,provider){

returnnewChargeCalculator(customer,usage,provider).charge;

}

Ihavetodecidehowtodealwithanysupportingfunctions,inthiscasebaseCharge.MyusualapproachforafunctionthatreturnsavalueistofirstExtractVariable(119)onthatvalue.

classChargeCalculator…

getbaseCharge(){

returnthis._customer.baseRate*this._usage;

}

getcharge(){

constbaseCharge=this.baseCharge;

returnbaseCharge+this._provider.connectionCharge;

}

Then,IuseInlineFunction(115)onthesupportingfunction.

classChargeCalculator…

getcharge(){

constbaseCharge=this._customer.baseRate*this._usage;

returnbaseCharge+this._provider.connectionCharge;

}

Inowhavealltheprocessinginasinglefunction,somynextmoveistomovethedatapassedtotheconstructortothemainmethod.IfirstuseChangeFunctionDeclaration(124)toaddalltheconstructorparameterstothechargemethod.

classChargeCalculator…

constructor(customer,usage,provider){

this._customer=customer;

this._usage=usage;

this._provider=provider;

}

charge(customer,usage,provider){

constbaseCharge=this._customer.baseRate*this._usage;

returnbaseCharge+this._provider.connectionCharge;

}

toplevel…

functioncharge(customer,usage,provider){

returnnewChargeCalculator(customer,usage,provider)

.charge(customer,usage,provider);

}

NowIcanalterthebodyofchargetousethepassedparametersinstead.Icandothisoneatatime.

classChargeCalculator…

constructor(customer,usage,provider){

this._customer=customer;

this._usage=usage;

this._provider=provider;

}

charge(customer,usage,provider){

constbaseCharge=customer.baseRate*this._usage;

returnbaseCharge+this._provider.connectionCharge;

}

Idon’thavetoremovetheassignmenttothis._customerintheconstructor,asitwilljustbeignored.ButIprefertodoitsincethatwillmakeatestfailifImisschangingauseoffieldtotheparameter.(Andifatestdoesn’tfail,Ishouldconsideraddinganewtest.)

Irepeatthisfortheotherparameters,endingupwith

classChargeCalculator…

charge(customer,usage,provider){

constbaseCharge=customer.baseRate*usage;

returnbaseCharge+provider.connectionCharge;

}

OnceI’vedoneallofthese,Icaninlineintothetop-levelchargefunction.ThisisaspecialkindofInlineFunction(115),asit’sinliningboththeconstructorandmethodcalltogether.

toplevel…

functioncharge(customer,usage,provider){

constbaseCharge=customer.baseRate*usage;

returnbaseCharge+provider.connectionCharge;

}

Thecommandclassisnowdeadcode,soI’lluseRemoveDeadCode(236)togiveitanhonorableburial.

Chapter12DealingwithInheritanceInthisfinalchapter,I’llturntooneofthebestknownfeaturesofobject-orientedprogramming:inheritance.Likeanypowerfulmechanism,itisbothveryusefulandeasytomisuse,andit’softenhardtoseethemisuseuntilit’sintherear-viewmirror.

Often,featuresneedtomoveupordowntheinheritancehierarchy.Severalrefactoringsdealwiththat:PullUpMethod(348),PullUpField(351),PullUpConstructorBody(353),PushDownMethod(357),andPushDownField(359).IcanaddandremoveclassesfromthehierachywithExtractSuperclass(373),RemoveSubclass(368),andCollapseHierarchy(378).ImaywanttoaddasubclasstoreplaceafieldthatI’musingtotriggerdifferentbehaviorbasedonitsvalue;IdothiswithReplaceTypeCodewithSubclasses(361).

Inheritanceisapowerfultool,butsometimesitgetsusedinthewrongplace—ortheplaceit’susedinbecomeswrong.Inthatcase,IuseReplaceSubclasswithDelegate(379)orReplaceSuperclasswithDelegate(397)toturninheritanceintodelegation.

PullUpMethod

inverseof:PushDownMethod(357)

Motivation

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

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

putsalotofrelianceonmytests.Iusuallyfinditvaluabletolookforthedifferences—often,theyshowupbehaviorthatIforgottotestfor.

Often,PullUpMethodcomesafterothersteps.Iseetwomethodsindifferentclassesthatcanbeparameterizedinsuchawaythattheyendupasessentiallythesamemethod.Inthatcase,thesmalleststepisformetoapplyParameterizeFunction(308)separatelyandthenPullUpMethod.

ThemostawkwardcomplicationwithPullUpMethodisifthebodyofthemethodsreferstofeaturesthatareonthesubclassbutnotonthesuperclass.Whenthathappens,IneedtousePullUpField(351)andPullUpMethodonthoseelementsfirst.

IfIhavetwomethodswithasimilaroverallflow,butdifferingindetails,I’llconsidertheFormTemplateMethod[bib-form-template].

Mechanics

Inspectmethodstoensuretheyareidentical.

Iftheydothesamething,butarenotidentical,refactorthemuntiltheyhaveidenticalbodies.

Checkthatallmethodcallsandfieldreferencesinsidethemethodbodyrefertofeaturesthatcanbecalledfromthesuperclass.

Ifthemethodshavedifferentsignatures,useChangeFunctionDeclaration(124)togetthemtotheoneyouwanttouseonthesuperclass.

Createanewmethodinthesuperclass.Copythebodyofoneofthemethodsovertoit.

Runstaticchecks.

Deleteonesubclassmethod.

Test.

Keepdeletingsubclassmethodsuntiltheyareallgone.

Example

Ihavetwosubclassmethodsthatdothesamething.

classEmployeeextendsParty…

getannualCost(){

returnthis.monthlyCost*12;

}

classDepartmentextendsParty…

gettotalAnnualCost(){

returnthis.monthlyCost*12;

}

IlookatbothclassesandseethattheyrefertothemonthlyCostpropertywhichisn’tdefinedonthesuperclass,butispresentinbothsubclasses.SinceI’minadynamiclanguage,I’mOK;ifIwereinastaticlanguage,I’dneedtodefineanabstractmethodonParty.

Themethodshavedifferentnames,soIChangeFunctionDeclaration(124)tomakethemthesame.

classDepartment…

getannualCost(){

returnthis.monthlyCost*12;

}

Icopythemethodfromonesubclassandpasteitintothesuperclass.

classParty…

getannualCost(){

returnthis.monthlyCost*12;

}

Inastaticlanguage,I’dcompiletoensurethatallthereferenceswereOK.Thatwon’thelpmehere,soIfirstremoveannualCostfromEmployee,test,andthenremoveitfromDepartment.

Thatcompletestherefactoring,butdoesleaveaquestion.annualCostcalls

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

classParty…

getmonthlyCost(){

thrownewSubclassResponsibilityError();

}

IcallsuchanerrorasubclassresponsibilityerrorasthatwasthenameusedinSmalltalk.

PullUpField

inverseof:PushDownField(359)

Motivation

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

Bydoingthis,Ireduceduplicationintwoways.Iremovetheduplicatedata

declarationandIcanthenmovebehaviorthatusesthefieldfromthesubclassestothesuperclass.

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

Mechanics

Inspectallusersofthecandidatefieldtoensuretheyareusedinthesameway.

Ifthefieldshavedifferentnames,useRenameField(244)togivethemthesamename.

Createanewfieldinthesuperclass.

Thenewfieldwillneedtobeaccessibletosubclasses(protectedincommonlanguages).

Deletethesubclassfields.

Test.

PullUpConstructorBody

Motivation

Constructorsaretrickythings.Theyaren’tquitenormalmethods—soI’mmorerestrictedinwhatIcandowiththem.

IfIseesubclassmethodswithcommonbehavior,myfirstthoughtistouseExtractFunction(106)followedbyPullUpMethod(348),whichwillmoveitnicelyintothesuperclass.Constructorstanglethat—becausetheyhavespecialrulesaboutwhatcanbedoneinwhatorder,soIneedaslightlydifferentapproach.

Ifthisrefactoringstartsgettingmessy,IreachforReplaceConstructorwithFactoryFunction(332).

Mechanics

Defineasuperclassconstructor,ifonedoesn’talreadyexist.Ensureit’scalledbysubclassconstructors.

UseSlideStatements(221)tomoveanycommonstatementstojustafterthesupercall.

Removethecommoncodefromeachsubclassandputitinthesuperclass.Addtothesupercallanyconstructorparametersreferencedinthecommoncode.

Test.

Ifthereisanycommoncodethatcannotmovetothestartoftheconstructor,useExtractFunction(106)followedbyPullUpMethod(348).

Example

Istartwiththefollowingcode:

classParty{}

classEmployeeextendsParty{

constructor(name,id,monthlyCost){

super();

this._id=id;

this._name=name;

this._monthlyCost=monthlyCost;

}

//restofclass…

classDepartmentextendsParty{

constructor(name,staff){

super();

this._name=name;

this._staff=staff;

}

//restofclass…

Thecommoncodehereistheassignmentofthename.IuseSlideStatements(221)tomovetheassignmentinEmployeenexttothecalltosuper():

classEmployeeextendsParty{

constructor(name,id,monthlyCost){

super();

this._name=name;

this._id=id;

this._monthlyCost=monthlyCost;

}

//restofclass…

Withthattested,Imovethecommoncodetothesuperclass.Sincethatcodecontainsareferencetoaconstructorargument,Ipassthatinasaparameter.

classParty…

constructor(name){

this._name=name;

}

classEmployee…

constructor(name,id,monthlyCost){

super(name);

this._id=id;

this._monthlyCost=monthlyCost;

}

classDepartment…

constructor(name,staff){

super(name);

this._staff=staff;

}

Runthetests,andI’mdone.

Mostofthetime,constructorbehaviorwillworklikethis:Dothecommonelementsfirst(withasupercall),thendoextraworkthatthesubclassneeds.Occasionally,however,thereissomecommonbehaviorlater.

Considerthisexample:

classEmployee…

constructor(name){…}

getisPrivileged(){…}

assignCar(){…}

classManagerextendsEmployee…

constructor(name,grade){

super(name);

this._grade=grade;

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

}

getisPrivileged(){

returnthis._grade>4;

}

ThewrinkleherecomesfromthefactthatthecalltoisPrivilegedcan’tbemadeuntilafterthegradefieldisassigned,andthatcanonlybedoneinthesubclass.

Inthiscase,IdoExtractFunction(106)onthecommoncode:

classManager…

constructor(name,grade){

super(name);

this._grade=grade;

this.finishConstruction();

}

finishConstruction(){

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

}

Then,IusePullUpMethod(348)tomoveittothesuperclass.

classEmployee…

finishConstruction(){

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

}

PushDownMethod

inverseof:PullUpMethod(348)

Motivation

Ifamethodisonlyrelevanttoonesubclass(orasmallproportionofsubclasses),removingitfromthesuperclassandputtingitonlyonthesubclass(es)makes

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

Mechanics

Copythemethodintoeverysubclassthatneedsit.

Removethemethodfromthesuperclass.

Test.

Removethemethodfromeachsuperclassthatdoesn’tneedit.

Test.

PushDownField

inverseof:PullUpField(351)

Motivation

Ifafieldisonlyusedbyonesubclass(orasmallproportionofsubclasses),Imoveittothosesubclasses.

Mechanics

Declarefieldinallsubclassesthatneedit.

Removethefieldfromthesuperclass.

Test.

Removethefieldfromallsubclassesthatdon’tneedit.

Test.

ReplaceTypeCodewithSubclasses

subsumes:ReplaceTypeCodewithState/Strategy

subsumes:ExtractSubclass

inverseof:RemoveSubclass(368)

Motivation

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

codefield—dependingonthelanguage,thatmightbeanenum,symbol,string,ornumber.Often,thistypecodewillcomefromanexternalservicethatprovidesmewiththedataI’mworkingon.

Mostofthetime,suchatypecodeisallIneed.ButthereareacoupleofsituationswhereIcoulddowithsomethingmore,andthatsomethingmorearesubclasses.Therearetwothingsthatareparticularlyenticingaboutsubclasses.First,theyallowmetousepolymorphismtohandleconditionallogic.IfindthismosthelpfulwhenIhaveseveralfunctionsthatinvokedifferentbehaviordependingonthevalueofthetypecode.Withsubclasses,IcanapplyReplaceConditionalwithPolymorphism(271)tothesefunctions.

ThesecondcaseiswhereIhavefieldsormethodsthatareonlyvalidforparticularvaluesofatypecode,suchasasalesquotathat’sonlyapplicabletothe“salesman”typecode.IcanthencreatethesubclassandapplyPushDownField(359).WhileIcanincludevalidationlogictoensureafieldisonlyusedwhenthetypecodehasthecorrectvalue,usingasubclassmakestherelationshipmoreexplicit.

WhenusingReplaceTypeCodewithSubclasses,IneedtoconsiderwhethertoapplyitdirectlytotheclassI’mlookingat,ortothetypecodeitself.DoImakeengineerasubtypeofemployee,orshouldIgivetheemployeeanemployeetypepropertywhichcanhavesubtypesforengineerandmanager?Usingdirectsubclassingissimpler,butIcan’tuseitforthejobtypeifIneeditforsomethingelse.Ialsocan’tusedirectsubclassesifthetypeismutable.IfIneedtomovethesubclassestoanemployeetypeproperty,IcandothatbyusingReplacePrimitivewithObject(172)onthetypecodetocreateanemployeetypeclassandthenusingReplaceTypeCodewithSubclassesonthatnewclass.

Mechanics

Self-encapsulatethetypecodefield.

Pickonetypecodevalue.Createasubclassforthattypecode.Overridethetypecodegettertoreturntheliteraltypecodevalue.

Createselectorlogictomapfromthetypecodeparametertothenewsubclass.

Withdirectinheritance,useReplaceConstructorwithFactoryFunction(332)

andputtheselectorlogicinthefactory.Withindirectinheritance,theselectorlogicmaystayintheconstructor.

Test.

Repeatcreatingthesubclassandaddingtotheselectorlogicforeachtypecodevalue.Testaftereachchange.

Removethetypecodefield.

Test.

UsePushDownMethod(357)andReplaceConditionalwithPolymorphism(271)onanymethodsthatusethetypecodeaccessors.Onceallarereplaced,youcanremovethetypecodeaccessors.

Example

I’llstartwiththisoverusedemployeeexample.

classEmployee…

constructor(name,type){

this.validateType(type);

this._name=name;

this._type=type;

}

validateType(arg){

if(!["engineer","manager","salesman"].includes(arg))

thrownewError(`Employeecannotbeoftype${arg}`);

}

toString(){return`${this._name}(${this._type})`;}

MyfirststepistouseEncapsulateVariable(132)toself-encapsulatethetypecode.

classEmployee…

gettype(){returnthis._type;}

toString(){return`${this._name}(${this.type})`;}

NotethattoStringusesthenewgetterbyremovingtheunderscore.

Ipickonetypecode,theengineer,tostartwith.Iusedirectinheritance,subclassingtheemployeeclassitself.Theemployeesubclassissimple—justoverridingthetypecodegetterwiththeappropriateliteralvalue.

classEngineerextendsEmployee{

gettype(){return"engineer";}

}

AlthoughJavaScriptconstructorscanreturnotherobjects,thingswillgetmessyifItrytoputselectorlogicinthere,sincethatlogicgetsintertwinedwithfieldinitialization.SoIuseReplaceConstructorwithFactoryFunction(332)tocreateanewspaceforit.

functioncreateEmployee(name,type){

returnnewEmployee(name,type);

}

Tousethenewsubclass,Iaddselectorlogicintothefactory.

functioncreateEmployee(name,type){

switch(type){

case"engineer":returnnewEngineer(name,type);

}

returnnewEmployee(name,type);

}

Itesttoensurethatworkedoutcorrectly.But,becauseI’mparanoid,Ithenalterthereturnvalueoftheengineer’soverrideandtestagaintoensurethetestfails.ThatwayIknowthesubclassisbeingused.Icorrectthereturnvalueandcontinuewiththeothercases.Icandothemoneatatime,testingaftereachchange.

classSalesmanextendsEmployee{

gettype(){return"salesman";}

}

classManagerextendsEmployee{

gettype(){return"manager";}

}

functioncreateEmployee(name,type){

switch(type){

case"engineer":returnnewEngineer(name,type);

case"salesman":returnnewSalesman(name,type);

case"manager":returnnewManager(name,type);

}

returnnewEmployee(name,type);

}

OnceI’mdonewiththemall,Icanremovethetypecodefieldandthesuper-classgettingmethod(theonesinthesubclassesremain).

classEmployee…

constructor(name,type){

this.validateType(type);

this._name=name;

this._type=type;

}

gettype(){returnthis._type;}

toString(){return`${this._name}(${this.type})`;}

Aftertestingtoensureallisstillwell,Icanremovethevalidationlogic,sincetheswitchiseffectivelydoingthesamething.

classEmployee…

constructor(name,type){

this.validateType(type);

this._name=name;

}

functioncreateEmployee(name,type){

switch(type){

case"engineer":returnnewEngineer(name,type);

case"salesman":returnnewSalesman(name,type);

case"manager":returnnewManager(name,type);

default:thrownewError(`Employeecannotbeoftype${type}`);

}

returnnewEmployee(name,type);

}

Thetypeargumenttotheconstructorisnowuseless,soitfallsvictimtoChangeFunctionDeclaration(124).

classEmployee…

constructor(name,type){

this._name=name;

}

functioncreateEmployee(name,type){

switch(type){

case"engineer":returnnewEngineer(name,type);

case"salesman":returnnewSalesman(name,type);

case"manager":returnnewManager(name,type);

default:thrownewError(`Employeecannotbeoftype${type}`);

}

}

Istillhavethetypecodeaccessorsonthesubclasses—gettype.I’llusuallywanttoremovethesetoo,butthatmaytakeabitoftimeduetoothermethodsthatdependonthem.I’lluseReplaceConditionalwithPolymorphism(271)andPushDownMethod(357)todealwiththese.Atsomepoint,I’llhavenocodethatusesthetypegetters,soIwillsubjectthemtothetendermerciesofRemoveDeadCode(236).

Example:UsingIndirectInheritance

Let’sgobacktothestartingcase—butthistime,Ialreadyhaveexistingsubclassesforpart-timeandfull-timeemployees,soIcan’tsubclassfromEmployeeforthetypecodes.Anotherreasontonotusedirectinheritanceiskeepingtheabilitytochangethetypeofemployee.

classEmployee…

constructor(name,type){

this.validateType(type);

this._name=name;

this._type=type;

}

validateType(arg){

if(!["engineer","manager","salesman"].includes(arg))

thrownewError(`Employeecannotbeoftype${arg}`);

}

gettype(){returnthis._type;}

settype(arg){this._type=arg;}

getcapitalizedType(){

returnthis._type.charAt(0).toUpperCase()+this._type.substr(1).toLowerCase();

}

toString(){

return`${this._name}(${this.capitalizedType})`;

}

ThistimetoStringisabitmorecomplicated,toallowmetoillustratesomethingshortly.

MyfirststepistouseReplacePrimitivewithObject(172)onthetypecode.

classEmployeeType{

constructor(aString){

this._value=aString;

}

toString(){returnthis._value;}

}

classEmployee…

constructor(name,type){

this.validateType(type);

this._name=name;

this.type=type;

}

validateType(arg){

if(!["engineer","manager","salesman"].includes(arg))

thrownewError(`Employeecannotbeoftype${arg}`);

}

gettypeString(){returnthis._type.toString();}

gettype(){returnthis._type;}

settype(arg){this._type=newEmployeeType(arg);}

getcapitalizedType(){

returnthis.typeString.charAt(0).toUpperCase()

+this.typeString.substr(1).toLowerCase();

}

toString(){

return`${this._name}(${this.capitalizedType})`;

}

IthenapplytheusualmechanicsofReplaceTypeCodewithSubclassestotheemployeetype.

classEmployee…

settype(arg){this._type=Employee.createEmployeeType(arg);}

staticcreateEmployeeType(aString){

switch(aString){

case"engineer":returnnewEngineer();

case"manager":returnnewManager();

case"salesman":returnnewSalesman();

default:thrownewError(`Employeecannotbeoftype${aString}`);

}

}

classEmployeeType{

}

classEngineerextendsEmployeeType{

toString(){return"engineer";}

}

classManagerextendsEmployeeType{

toString(){return"manager";}

}

classSalesmanextendsEmployeeType{

toString(){return"salesman";}

}

IfIwereleavingitatthat,IcouldremovetheemptyEmployeeType.ButIprefertoleaveitthereasitmakesexplicittherelationshipbetweenthevarioussubclasses.It’salsoahandyspotformovingotherbehaviorthere,suchasthecapitalizationlogicItossedintotheexamplespecificallytoillustratethispoint.

classEmployee…

toString(){

return`${this._name}(${this.type.capitalizedName})`;

}

classEmployeeType…

getcapitalizedName(){

returnthis.toString().charAt(0).toUpperCase()

+this.toString().substr(1).toLowerCase();

}

Forthosefamiliarwiththefirsteditionofthebook,thisexampleessentiallysupersedestheReplaceTypeCodewithState/Strategy.InowthinkofthatrefactoringasReplaceTypeCodewithSubclassesusingindirectinheritance,sodidn’tconsideritworthitsownentryinthecatalog.(Ineverlikedthenameanyway.)

RemoveSubclass

formerly:ReplaceSubclasswithFields

inverseof:ReplaceTypeCodewithSubclasses(361)

Motivation

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

Asubclassthatdoestoolittleincursacostinunderstandingthatisnolongerworthwhile.Whenthattimecomes,it’sbesttoremovethesubclass,replacingit

withafieldonitssuperclass.

Mechanics

UseReplaceConstructorwithFactoryFunction(332)onthesubclassconstructor.

Iftheclientsoftheconstructorsuseadatafieldtodecidewhichsubclasstocreate,putthatdecisionlogicintoasuperclassfactorymethod.

Ifanycodetestsagainstthesubclass’stypes,useExtractFunction(106)onthetypetestandMoveFunction(196)tomoveittothesuperclass.Testaftereachchange.

Createafieldtorepresentthesubclasstype.

Changethemethodsthatrefertothesubclasstousethenewtypefield.

Deletethesubclass.

Test.

Often,thisrefactoringisusedonagroupofsubclassesatonce—inwhichcasecarryoutthestepstoencapsulatethem(addfactoryfunction,movetypetests)first,thenindividuallyfoldthemintothesuperclass.

Example

I’llstartwiththisstumpofsubclasses.

classPerson…

constructor(name){

this._name=name;

}

getname(){returnthis._name;}

getgenderCode(){return"X";}

//snip

classMaleextendsPerson{

getgenderCode(){return"M";}

}

classFemaleextendsPerson{

getgenderCode(){return"F";}

}

Ifthat’sallthatasubclassdoes,it’snotreallyworthhaving.ButbeforeIremovethesesubclasses,it’susuallyworthcheckingtoseeifthere’sanysubclass-dependentbehaviorintheclientsthatshouldbemovedinthere.Inthiscase,Idon’tfindanythingworthkeepingthesubclassesfor.

client…

constnumberOfMales=people.filter(p=>pinstanceofMale).length;

WheneverIwanttochangehowIrepresentsomething,Itrytofirstencapsulatethecurrentrepresentationtominimizetheimpactonanyclientcode.Whenitcomestocreatingsubclasses,thewaytoencapsulateistouseReplaceConstructorwithFactoryFunction(332).Inthiscase,there’sacoupleofwaysIcouldmakethefactory.

Themostdirectwayistocreateafactorymethodforeachconstructor.

functioncreatePerson(name){

returnnewPerson(name);

}

functioncreateMale(name){

returnnewMale(name);

}

functioncreateFemale(name){

returnnewFemale(name);

}

Butalthoughthat’sthedirectchoice,objectslikethisareoftenloadedfromasourcethatusesthegendercodesdirectly.

functionloadFromInput(data){

constresult=[];

data.forEach(aRecord=>{

letp;

switch(aRecord.gender){

case'M':p=newMale(aRecord.name);break;

case'F':p=newFemale(aRecord.name);break;

default:p=newPerson(aRecord.name);

}

result.push(p);

});

returnresult;

}

Inthatcase,IfinditbettertouseExtractFunction(106)ontheselectionlogicforwhichclasstocreate,andmakethatthefactoryfunction.

functioncreatePerson(aRecord){

letp;

switch(aRecord.gender){

case'M':p=newMale(aRecord.name);break;

case'F':p=newFemale(aRecord.name);break;

default:p=newPerson(aRecord.name);

}

returnp;

}

functionloadFromInput(data){

constresult=[];

data.forEach(aRecord=>{

result.push(createPerson(aRecord));

});

returnresult;

}

WhileI’mthere,I’llcleanupthosetwofunctions.I’lluseInlineVariable(123)oncreatePerson:

functioncreatePerson(aRecord){

switch(aRecord.gender){

case'M':returnnewMale(aRecord.name);

case'F':returnnewFemale(aRecord.name);

default:returnnewPerson(aRecord.name);

}

}

AndReplaceLoopwithPipeline(230)onloadFromInput:

functionloadFromInput(data){

returndata.map(aRecord=>createPerson(aRecord));

}

Thefactoryencapsulatesthecreationofthesubclasses,butthereisalsotheuseofinstanceof—whichneversmellsgood.IuseExtractFunction(106)onthetypecheck.

client…

constnumberOfMales=people.filter(p=>isMale(p)).length;

functionisMale(aPerson){returnaPersoninstanceofMale;}

ThenIuseMoveFunction(196)tomoveitintoPerson.

classPerson…

getisMale(){returnthisinstanceofMale;}

client…

constnumberOfMales=people.filter(p=>p.isMale).length;

Withthatrefactoringdone,allknowledgeofthesubclassesisnowsafelyencasedwithinthesuperclassandthefactoryfunction.(UsuallyI’mwaryofasuperclassreferringtoasubclass,butthiscodeisn’tgoingtolastuntilmynextcupoftea,soI’mnotgoingworryaboutit.)

Inowaddafieldtorepresentthedifferencebetweenthesubclasses;sinceI’musingacodeloadedfromelsewhere,Imightaswelljustusethat.

classPerson…

constructor(name,genderCode){

this._name=name;

this._genderCode=genderCode||"X";

}

getgenderCode(){returnthis._genderCode;}

Wheninitializingit,Isetittothedefaultcase.(Asasidenote,althoughmostpeoplecanbeclassifiedasmaleorfemale,therearepeoplewhocan’t.It’sacommonmodelingmistaketoforgetthat.)

Ithentakethemalecaseandfolditslogicintothesuperclass.ThisinvolvesmodifyingthefactorytoreturnaPersonandmodifyinganyinstanceofteststousethegendercodefield.

functioncreatePerson(aRecord){

switch(aRecord.gender){

case'M':returnnewPerson(aRecord.name,"M");

case'F':returnnewFemale(aRecord.name);

default:returnnewPerson(aRecord.name);

}

}

classPerson…

getisMale(){return"M"===this._genderCode;}

Itest,removethemalesubclass,testagain,andrepeatforthefemalesubclass.

functioncreatePerson(aRecord){

switch(aRecord.gender){

case'M':returnnewPerson(aRecord.name,"M");

case'F':returnnewPerson(aRecord.name,"F");

default:returnnewPerson(aRecord.name);

}

}

Ifindthelackofsymmetrywiththegendercodetobeannoying.Afuturereaderofthecodewillalwayswonderaboutthislackofsymmetry.SoIprefertochangethecodetomakeitsymmetrical—ifIcandoitwithoutintroducinganyothercomplexity,whichisthecasehere.

functioncreatePerson(aRecord){

switch(aRecord.gender){

case'M':returnnewPerson(aRecord.name,"M");

case'F':returnnewPerson(aRecord.name,"F");

default:returnnewPerson(aRecord.name,"X");

}

}

classPerson…

constructor(name,genderCode){

this._name=name;

this._genderCode=genderCode||"X";

}

ExtractSuperclass

Motivation

IfIseetwoclassesdoingsimilarthings,Icantakeadvantageofthebasic

mechanismofinheritancetopulltheirsimilaritiestogetherintoasuperclass.IcanusePullUpField(351)tomovecommondataintothesuperclass,andPullUpMethod(348)tomovethecommonbehavior.

Manywritersonobjectorientationtreatinheritanceassomethingthatshouldbecarefullyplannedinadvance,basedonsomekindofclassificationstructureinthe“realworld.”Suchclassificationstructurescanbeahinttowardsusinginheritance—butjustasofteninheritanceissomethingIrealizeduringtheevolutionofaprogram,asIfindcommonelementsthatIwanttopulltogether.

AnalternativetoExtractSuperclassisExtractClass(180).Hereyouhave,essentially,achoicebetweenusinginheritanceordelegationasawaytounifyduplicatebehavior.OftenExtractSuperclassisthesimplerapproach,soI’lldothisfirstknowingIcanuseReplaceSuperclasswithDelegate(397)shouldIneedtolater.

Mechanics

Createanemptysuperclass.Maketheoriginalclassesitssubclasses.

Ifneeded,useChangeFunctionDeclaration(124)ontheconstructors.

Test.

Onebyone,usePullUpConstructorBody(353),PullUpMethod(348),andPullUpField(351)tomovecommonelementstothesuperclass.

Examineremainingmethodsonthesubclasses.Seeiftherearecommonparts.Ifso,useExtractFunction(106)followedbyPullUpMethod(348).

Checkclientsoftheoriginalclasses.Consideradjustingthemtousethesuperclassinterface.

Example

I’mponderingthesetwoclasses:

classEmployee{

constructor(name,id,monthlyCost){

this._id=id;

this._name=name;

this._monthlyCost=monthlyCost;

}

getmonthlyCost(){returnthis._monthlyCost;}

getname(){returnthis._name;}

getid(){returnthis._id;}

getannualCost(){

returnthis.monthlyCost*12;

}

}

classDepartment{

constructor(name,staff){

this._name=name;

this._staff=staff;

}

getstaff(){returnthis._staff.slice();}

getname(){returnthis._name;}

gettotalMonthlyCost(){

returnthis.staff

.map(e=>e.monthlyCost)

.reduce((sum,cost)=>sum+cost);

}

getheadCount(){

returnthis.staff.length;

}

gettotalAnnualCost(){

returnthis.totalMonthlyCost*12;

}

}

Theysharesomecommonfunctionality—theirnameandthenotionsofannualandmonthlycosts.Icanmakethiscommonalitymoreexplicitbyextractingacommonsuperclassfromthem.

Ibeginbycreatinganemptysuperclassandlettingthembothextendfromit.

classParty{}

classEmployeeextendsParty{

constructor(name,id,monthlyCost){

super();

this._id=id;

this._name=name;

this._monthlyCost=monthlyCost;

}

//restofclass…

classDepartmentextendsParty{

constructor(name,staff){

super();

this._name=name;

this._staff=staff;

}

//restofclass…

WhendoingExtractSuperclass,Iliketostartwiththedata,whichinJavaScriptinvolvesmanipulatingtheconstructor.SoIstartwithPullUpField(351)topullupthename.

classParty…

constructor(name){

this._name=name;

}

classEmployee…

constructor(name,id,monthlyCost){

super(name);

this._id=id;

this._monthlyCost=monthlyCost;

}

classDepartment…

constructor(name,staff){

super(name);

this._staff=staff;

}

AsIgetdatauptothesuperclass,IcanalsoapplyPullUpMethod(348)onassociatedmethods.First,thename:

classParty…

getname(){returnthis._name;}

classEmployee…

getname(){returnthis._name;}

classDepartment…

getname(){returnthis._name;}

Ihavetwomethodswithsimilarbodies.

classEmployee…

getannualCost(){

returnthis.monthlyCost*12;

}

classDepartment…

gettotalAnnualCost(){

returnthis.totalMonthlyCost*12;

}

Themethodstheyuse,monthlyCostandtotalMonthlyCost,havedifferentnamesanddifferentbodies—butdotheyrepresentthesameintent?Ifso,IshoulduseChangeFunctionDeclaration(124)tounifytheirnames.

classDepartment…

gettotalAnnualCost(){

returnthis.monthlyCost*12;

}

getmonthlyCost(){…}

Ithendoasimilarrenamingtotheannualcosts:

classDepartment…

getannualCost(){

returnthis.monthlyCost*12;

}

IcannowapplyPullUpMethod(348)totheannualcostmethods.

classParty…

getannualCost(){

returnthis.monthlyCost*12;

}

classEmployee…

getannualCost(){

returnthis.monthlyCost*12;

}

classDepartment…

getannualCost(){

returnthis.monthlyCost*12;

}

CollapseHierarchy

Motivation

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

Mechanics

Choosewhichonetoremove.

Ichoosebasedonwhichnamemakesmostsenseinthefuture.Ifneithernameisbest,I’llpickonearbitrarily.

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

Adjustanyreferencestothevictimtochangethemtotheclassthatwillstay.

Removetheemptyclass.

Test.

ReplaceSubclasswithDelegate

Motivation

IfIhavesomeobjectswhosebehaviorvariesfromcategorytocategory,thenaturalmechanismtoexpressthisisinheritance.Iputallthecommondataandbehaviorinthesuperclass,andleteachsubclassaddandoverridefeaturesasneeded.Object-orientedlanguagesmakethissimpletoimplementandthusa

familiarmechanism.

Butinheritancehasitsdownsides.Mostobviously,it’sacardthatcanonlybeplayedonce.IfIhavemorethanonereasontovarysomething,Icanonlyuseinheritanceforasingleaxisofvariation.So,ifIwanttovarybehaviorofpeoplebytheiragecategoryandbytheirincomelevel,Icaneitherhavesubclassesforyoungandsenior,orforwell-offandpoor—Ican’thaveboth.

Afurtherproblemisthatinheritanceintroducesaverycloserelationshipbetweenclasses.AnychangeIwanttomaketotheparentcaneasilybreakchildren,soIhavetobecarefulandunderstandhowchildrenderivefromthesuperclass.Thisproblemismadeworsewhenthelogicofthetwoclassesresidesindifferentmodulesandislookedafterbydifferentteams.

Delegationhandlesbothoftheseproblems.Icandelegatetomanydifferentclassesfordifferentreasons.Delegationisaregularrelationshipbetweenobjects—soIcanhaveaclearinterfacetoworkwith,whichismuchlesscouplingthansubclassing.It’sthereforecommontorunintotheproblemswithsubclassingandapplyReplaceSubclasswithDelegate.

Thereisapopularprinciple:“Favorobjectcompositionoverclassinheritance”(wherecompositioniseffectivelythesameasdelegation).Manypeopletakethistomean“inheritanceconsideredharmful”andclaimthatweshouldneveruseinheritance.Iuseinheritancefrequently,partlybecauseIalwaysknowIcanuseReplaceSubclasswithDelegateshouldIneedtochangeitlater.Inheritanceisavaluablemechanismthatdoesthejobmostofthetimewithoutproblems.SoIreachforitfirst,andmoveontodelegationwhenitstartstorubbadly.Thisusageisactuallyconsistentwiththeprinciple—whichcomesfromtheGangofFourbook[bib-gof]thatexplainshowinheritanceandcompositionworktogether.Theprinciplewasareactiontotheoveruseofinheritance.

ThosewhoarefamiliarwiththeGangofFourbookmayfindithelpfultothinkofthisrefactoringasreplacingsubclasseswiththeStateorStrategypatterns.Bothofthesepatternsarestructurallythesame,relyingonthehostdelegatingtoaseparatehierarchy.NotallcasesofReplaceSubclasswithDelegateinvolveaninheritancehierarchyforthedelegate(asthefirstexamplebelowillustrates),butsettingupahierarchyforstatesorstrategiesisoftenuseful.

Mechanics

Iftherearemanycallersfortheconstructors,applyReplaceConstructorwithFactoryFunction(332).

Createanemptyclassforthedelegate.Itsconstructorshouldtakeanysubclass-specificdataaswellas,usually,aback-referencetothesuperclass.

Addafieldtothesuperclasstoholdthedelegate.

Modifythecreationofthesubclasssothatitinitializesthedelegatefieldwithaninstanceofthedelegate.

Thiscanbedoneinthefactoryfunction,orintheconstructoriftheconstructorcanreliablytellwhethertocreatethecorrectdelegate.

Chooseasubclassmethodtomovetothedelegateclass.

UseMoveFunction(196)tomoveittothedelegateclass.Don’tremovethesource’sdelegatingcode.

Ifthemethodneedselementsthatshouldmovetothedelegate,movethem.Ifitneedselementsthatshouldstayinthesuperclass,addafieldtothedelegatethatreferstothesuperclass.

Ifthesourcemethodhascallersoutsidetheclass,movethesource’sdelegatingcodefromthesubclasstothesuperclass,guardingitwithacheckforthepresenceofthedelegate.Ifnot,applyRemoveDeadCode(236).

Ifthere’smorethanonesubclass,andyoustartduplicatingcodewithinthem,useExtractSuperclass(373).Inthiscase,anydelegatingmethodsonthesourcesuper-classnolongerneedaguardifthedefaultbehaviorismovedtothedelegatesuperclass.

Test.

Repeatuntilallthemethodsofthesubclassaremoved.

Findallcallersofthesubclasses’sconstructorandchangethemtousethesuperclassconstructor.

Test.

UseRemoveDeadCode(236)onthesubclass.

Example

Ihaveaclassthatmakesabookingforashow.

classBooking…

constructor(show,date){

this._show=show;

this._date=date;

}

Thereisasubclassforpremiumbookingthattakesintoaccountsvariousextrasthatareavailable.

classPremiumBookingextendsBooking…

constructor(show,date,extras){

super(show,date);

this._extras=extras;

}

Therearequiteafewchangesthatthepremiumbookingmakestowhatitinheritsfromthesuperclass.Asistypicalwiththiskindofprogramming-by-difference,insomecasesthesubclassoverridesmethodsonthesuperclass,inothersitaddsnewmethodsthatareonlyrelevantforthesubclass.Iwon’tgointoallofthem,butIwillpickoutafewinterestingcases.

First,thereisasimpleoverride.Regularbookingsofferatalkbackaftertheshow,butonlyonnon-peakdays.

classBooking…

gethasTalkback(){

returnthis._show.hasOwnProperty('talkback')&&!this.isPeakDay;

}

Premiumbookingsoverridethistooffertalkbacksonalldays.

classPremiumBooking…

gethasTalkback(){

returnthis._show.hasOwnProperty('talkback');

}

Determiningthepriceisasimilaroverride,withatwistthatthepremiummethodcallsthesuperclassmethod.

classBooking…

getbasePrice(){

letresult=this._show.price;

if(this.isPeakDay)result+=Math.round(result*0.15);

returnresult;

}

classPremiumBooking…

getbasePrice(){

returnMath.round(super.basePrice+this._extras.premiumFee);

}

Thelastexampleiswherethepremiumbookingoffersabehaviorthatisn’tpresentonthesuperclass.

classPremiumBooking…

gethasDinner(){

returnthis._extras.hasOwnProperty('dinner')&&!this.isPeakDay;

}

Inheritanceworkswellforthisexample.Icanunderstandthebaseclasswithouthavingtounderstandthesubclass.Thesubclassisdefinedjustbysayinghowitdiffersfromthebasecase—bothreducingduplicationandclearlycommunicatingwhatarethedifferencesit’sintroducing.

Actually,itisn’tquiteasperfectasthepreviousparagraphimplies.Therearethingsinthesuperclassstructurethatonlymakesenseduetothesubclass—suchasmethodsthathavebeenfactoredinsuchawayastomakeiteasiertooverridejusttherightkindsofbehavior.SoalthoughmostofthetimeIcanmodifythebaseclasswithouthavingtounderstandsubclasses,thereareoccasionswheresuchmindfulignoranceofthesubclasseswillleadmetobreakingasubclassbymodifyingthesuperclass.However,iftheseoccasionsarenottoocommon,theinheritancepaysoff—providedIhavegoodteststodetectasubclassbreakage.

SowhywouldIwanttochangesuchahappysituationbyusingReplaceSubclasswithDelegate?Inheritanceisatoolthatcanonlybeusedonce—soifIhaveanotherreasontouseinheritance,andIthinkitwillbenefitmemorethanthepremiumbookingsubclass,I’llneedtohandlepremiumbookingsadifferentway.Also,Imayneedtochangefromthedefaultbookingtothepremiumbookingdynamically—i.e.supportamethodlikeaBooking.bePremium().Insomecases,Icanavoidthisbycreatingawholenewobject(acommonexampleiswhereanHTTPrequestloadsnewdatafromtheserver).Butsometimes,Ineedtomodifyadatastructureandnotrebuilditfromscratch,anditisdifficulttojustreplaceasinglebookingthat’sreferredtofrommanydifferentplaces.Insuchsituations,itcanbeusefultoallowabookingtoswitchfromdefaulttopremiumandbackagain.

Whentheseneedscropup,IneedtoapplyReplaceSubclasswithDelegate.Ihaveclientscalltheconstructorsofthetwoclassestomakethebookings:

bookingclient

aBooking=newBooking(show,date);

premiumclient

aBooking=newPremiumBooking(show,date,extras);

Removingsubclasseswillalterallofthis,soIliketoencapsulatetheconstructorcallswithReplaceConstructorwithFactoryFunction(332)

toplevel…

functioncreateBooking(show,date){

returnnewBooking(show,date);

}

functioncreatePremiumBooking(show,date,extras){

returnnewPremiumBooking(show,date,extras);

}

bookingclient

aBooking=createBooking(show,date);

premiumclient

aBooking=createPremiumBooking(show,date,extras);

Inowmakethenewdelegateclass.Itsconstructorparametersarethoseparametersthatareonlyusedinthesubclass,togetherwithaback-referencetothebookingobject.I’llneedthisbecauseseveralsubclassmethodsrequireaccesstodatastoredinthesuperclass.Inheritancemakesthiseasytodo,butwithadelegateIneedaback-reference.

classPremiumBookingDelegate…

constructor(hostBooking,extras){

this._host=hostBooking;

this._extras=extras;

}

Inowconnectthenewdelegatetothebookingobject.Idothisbymodifyingthefactoryfunctionforpremiumbookings.

toplevel…

functioncreatePremiumBooking(show,date,extras){

constresult=newPremiumBooking(show,date,extras);

result._bePremium(extras);

returnresult;

}

classBooking…

_bePremium(extras){

this._premiumDelegate=newPremiumBookingDelegate(this,extras);

}

Iusealeadingunderscoreon_bePremiumtoindicatethatitshouldn’tbepartofthepublicinterfaceforBooking.Ofcourse,ifthepointofdoingthisrefactoringistoallowabookingtomutatetopremium,itcanbeapublicmethod.

Alternatively,IcandoalltheconnectionsintheconstructorforBooking.Inordertodothat,Ineedsomewaytosignaltotheconstructorthatwehaveapremiumbooking.Thatcouldbeanextraparameter,orjusttheuseofextrasifIcanbesurethatitisalwayspresentwhenusedwithapremiumbooking.Here,Iprefertheexplicitnessofdoingthisthroughthefactoryfunction.

Withthestructuressetup,it’stimetostartmovingthebehavior.Thefirstcase

I’llconsideristhesimpleoverrideofhasTalkback.Here’stheexistingcode:

classBooking…

gethasTalkback(){

returnthis._show.hasOwnProperty('talkback')&&!this.isPeakDay;

}

classPremiumBooking…

gethasTalkback(){

returnthis._show.hasOwnProperty('talkback');

}

IuseMoveFunction(196)tomovethesubclassmethodtothedelegate.Tomakeitfititshome,Irouteanyaccesstosuperclassdatawithacallto_host.

classPremiumBookingDelegate…

gethasTalkback(){

returnthis._host._show.hasOwnProperty('talkback');

}

classPremiumBooking…

gethasTalkback(){

returnthis._premiumDelegate.hasTalkback;

}

Itesttoensureeverythingisworking,thendeletethesubclassmethod:

classPremiumBooking…

gethasTalkback(){

returnthis._premiumDelegate.hasTalkback;

}

Irunthetestsatthispoint,expectingsometofail.

NowIfinishthemovebyaddingdispatchlogictothesuperclassmethodtousethedelegateifitispresent.

classBooking…

gethasTalkback(){

return(this._premiumDelegate)

?this._premiumDelegate.hasTalkback

:this._show.hasOwnProperty('talkback')&&!this.isPeakDay;

}

ThenextcaseI’lllookatisthebaseprice.

classBooking…

getbasePrice(){

letresult=this._show.price;

if(this.isPeakDay)result+=Math.round(result*0.15);

returnresult;

}

classPremiumBooking…

getbasePrice(){

returnMath.round(super.basePrice+this._extras.premiumFee);

}

Thisisalmostthesame,butthereisawrinkleintheformofthepeskycallonsuper(whichisprettycommoninthesekindsofsubclassextensioncases).WhenImovethesubclasscodetothedelegate,I’llneedtocalltheparentcase—butIcan’tjustcallthis._host._basePricewithoutgettingintoanendlessrecursion.

Ihaveacoupleofoptionshere.OneistoapplyExtractFunction(106)onthebasecalculationtoallowmetoseparatethedispatchlogicfrompricecalculation.(Therestofthemoveisasbefore.)

classBooking…

getbasePrice(){

return(this._premiumDelegate)

?this._premiumDelegate.basePrice

:this._privateBasePrice;

}

get_privateBasePrice(){

letresult=this._show.price;

if(this.isPeakDay)result+=Math.round(result*0.15);

returnresult;

}

classPremiumBookingDelegate…

getbasePrice(){

returnMath.round(this._host._privateBasePrice+this._extras.premiumFee);

}

Alternatively,Icanrecastthedelegate’smethodasanextensionofthebasemethod.

classBooking…

getbasePrice(){

letresult=this._show.price;

if(this.isPeakDay)result+=Math.round(result*0.15);

return(this._premiumDelegate)

?this._premiumDelegate.extendBasePrice(result)

:result;

}

classPremiumBookingDelegate…

extendBasePrice(base){

returnMath.round(base+this._extras.premiumFee);

}

Bothworkreasonablyhere;Ihaveaslightpreferenceforthelatterasit’sabitsmaller.

Thelastcaseisamethodthatonlyexistsonthesubclass.

classPremiumBooking…

gethasDinner(){

returnthis._extras.hasOwnProperty('dinner')&&!this.isPeakDay;

}

Imoveitfromthesubclasstothedelegate:

classPremiumBookingDelegate…

gethasDinner(){

returnthis._extras.hasOwnProperty('dinner')&&!this._host.isPeakDay;

}

IthenadddispatchlogictoBooking:

classBooking…

gethasDinner(){

return(this._premiumDelegate)

?this._premiumDelegate.hasDinner

:undefined;

}

InJavaScript,accessingapropertyonanobjectwhereitisn’tdefinedreturnsundefined,soIdothathere.(Althoughmyeveryinstinctistohaveitraiseanerror,whichwouldbethecaseinotherobject-orienteddynamiclanguagesI’musedto.)

OnceI’vemovedallthebehavioroutofthesubclass,Icanchangethefactorymethodtoreturnthesuperclass—and,onceI’verunteststoensurealliswell,deletethesubclass.

toplevel…

functioncreatePremiumBooking(show,date,extras){

constresult=newPremiumBooking(show,date,extras);

result._bePremium(extras);

returnresult;

}

classPremiumBookingextendsBooking…

ThisisoneofthoserefactoringswhereIdon’tfeelthatrefactoringaloneimprovesthecode.Inheritancehandlesthissituationverywell,whereasusingdelegationinvolvesaddingdispatchlogic,two-wayreferences,andthusextracomplexity.Therefactoringmaystillbeworthwhile,sincetheadvantageofamutablepremiumstatus,oraneedtouseinheritanceforotherpurposes,mayoutweighthedisadvantageoflosinginheritance.

Example:ReplacingaHierarchy

ThepreviousexampleshowedusingReplaceSubclasswithDelegateonasinglesubclass,butIcandothesamethingwithanentirehierarchy.

functioncreateBird(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallow(data);

case'AfricanSwallow':

returnnewAfricanSwallow(data);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrot(data);

default:

returnnewBird(data);

}

}

classBird{

constructor(data){

this._name=data.name;

this._plumage=data.plumage;

}

getname(){returnthis._name;}

getplumage(){

returnthis._plumage||"average";

}

getairSpeedVelocity(){returnnull;}

}

classEuropeanSwallowextendsBird{

getairSpeedVelocity(){return35;}

}

classAfricanSwallowextendsBird{

constructor(data){

super(data);

this._numberOfCoconuts=data.numberOfCoconuts;

}

getairSpeedVelocity(){

return40-2*this._numberOfCoconuts;

}

}

classNorwegianBlueParrotextendsBird{

constructor(data){

super(data);

this._voltage=data.voltage;

this._isNailed=data.isNailed;

}

getplumage(){

if(this._voltage>100)return"scorched";

elsereturnthis._plumage||"beautiful";

}

getairSpeedVelocity(){

return(this._isNailed)?0:10+this._voltage/10;

}

}

Thesystemwillshortlybemakingabigdifferencebetweenbirdstaggedinthewildandthosetaggedincaptivity.ThatdifferencecouldbemodeledastwosubclassesforBird:WildBirdandCaptiveBird.However,Icanonlyuseinheritanceonce,soifIwanttousesubclassesforwildversuscaptive,I’llhavetoremovethemforthespecies.

Whenseveralsubclassesareinvolved,I’lltacklethemoneatatime,startingwithasimpleone—inthiscase,EuropeanSwallow.Icreateanemptydelegateclassforthedelegate.

classEuropeanSwallowDelegate{

}

Idon’tputinanydataorback-referenceparametersyet.Forthisexample,I’llintroducethemasIneedthem.

Ineedtodecidewheretohandletheinitializationofthedelegatefield.Here,sinceIhavealltheinformationinthesingledataargumenttotheconstructor,Idecidetodoitintheconstructor.SincethereareseveraldelegatesIcouldadd,Imakeafunctiontoselectthecorrectonebasedonthetypecodeinthedocument.

classBird…

constructor(data){

this._name=data.name;

this._plumage=data.plumage;

this._speciesDelegate=this.selectSpeciesDelegate(data);

}

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate();

default:returnnull;

}

}

NowIhavethestructuresetup,IcanapplyMoveFunction(196)totheEuropeanswallow’sairspeedvelocity.

classEuropeanSwallowDelegate…

getairSpeedVelocity(){return35;}

classEuropeanSwallow…

getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity

IchangeairSpeedVelocityonthesuperclasstocalladelegate,ifpresent.

classBird…

getairSpeedVelocity(){

returnthis._speciesDelegate?this._speciesDelegate.airSpeedVelocity:

}

Iremovethesubclass.

classEuropeanSwallowextendsBird{

getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity;}

}

toplevel…

functioncreateBird(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallow(data);

case'AfricanSwallow':

returnnewAfricanSwallow(data);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrot(data);

default:

returnnewBird(data);

}

}

NextI’lltackletheAfricanswallow.Icreateaclass;thistime,theconstructorneedsthedatadocument.

classAfricanSwallowDelegate…

constructor(data){

this._numberOfCoconuts=data.numberOfCoconuts;

}

classBird…

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate();

case'AfricanSwallow':

returnnewAfricanSwallowDelegate(data);

default:returnnull;

}

}

IuseMoveFunction(196)onairSpeedVelocity.

classAfricanSwallowDelegate…

getairSpeedVelocity(){

return40-2*this._numberOfCoconuts;

}

classAfricanSwallow…

getairSpeedVelocity(){

returnthis._speciesDelegate.airSpeedVelocity;

}

IcannowremovetheAfricanswallowsubclass.

classAfricanSwallowextendsBird{

//allofthebody…

}

functioncreateBird(data){

switch(data.type){

case'AfricanSwallow':

returnnewAfricanSwallow(data);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrot(data);

default:

returnnewBird(data);

}

}

NowfortheNorwegianblue.Creatingtheclassandmovingtheairspeedvelocityusesthesamestepsasbefore,soI’lljustshowtheresult.

classBird…

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate();

case'AfricanSwallow':

returnnewAfricanSwallowDelegate(data);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrotDelegate(data);

default:returnnull;

}

}

classNorwegianBlueParrotDelegate…

constructor(data){

this._voltage=data.voltage;

this._isNailed=data.isNailed;

}

getairSpeedVelocity(){

return(this._isNailed)?0:10+this._voltage/10;

}

Allwellandgood,buttheNorwegianblueoverridestheplumageproperty,whichIdidn’thavetodealwithfortheothercases.TheinitialMoveFunction(196)issimpleenough,albeitwiththeneedtomodifytheconstructortoputinaback-referencetothebird.

classNorwegianBlueParrot…

getplumage(){

returnthis._speciesDelegate.plumage;

}

classNorwegianBlueParrotDelegate…

getplumage(){

if(this._voltage>100)return"scorched";

elsereturnthis._bird._plumage||"beautiful";

}

constructor(data,bird){

this._bird=bird;

this._voltage=data.voltage;

this._isNailed=data.isNailed;

}

classBird…

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate();

case'AfricanSwallow':

returnnewAfricanSwallowDelegate(data);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrotDelegate(data,this);

default:returnnull;

}

}

Thetrickystepishowtoremovethesubclassmethodforplumage.IfIdo

classBird…

getplumage(){

if(this._speciesDelegate)

returnthis._speciesDelegate.plumage;

else

returnthis._plumage||"average";

}

ThenI’llgetabunchoferrorsbecausethereisnoplumagepropertyontheotherspecies’delegateclasses.

Icoulduseamorepreciseconditional:

classBird…

getplumage(){

if(this._speciesDelegateinstanceofNorwegianBlueParrotDelegate)

returnthis._speciesDelegate.plumage;

else

returnthis._plumage||"average";

}

ButIhopethatsmellsasmuchofdecomposingparrottoyouasitdoestome.It’salmostneveragoodideatouseanexplicitclasschecklikethis.

Anotheroptionistoimplementthedefaultcaseontheotherdelegates.

classBird…

getplumage(){

if(this._speciesDelegate)

returnthis._speciesDelegate.plumage;

else

returnthis._plumage||"average";

}

classEuropeanSwallowDelegate…

getplumage(){

returnthis._bird._plumage||"average";

}

classAfricanSwallowDelegate…

getplumage(){

returnthis._bird._plumage||"average";

}

Butthisduplicatesthedefaultmethodforplumage.Andifthat’snotbadenough,Ialsogetsomebonusduplicationintheconstructorstoassigntheback-reference.

Thesolutiontotheduplicationis,naturally,inheritance—IapplyExtractSuperclass(373)tothespeciesdelegates:

classSpeciesDelegate{

constructor(data,bird){

this._bird=bird;

}

getplumage(){

returnthis._bird._plumage||"average";

}

classEuropeanSwallowDelegateextendsSpeciesDelegate{

classAfricanSwallowDelegateextendsSpeciesDelegate{

constructor(data,bird){

super(data,bird);

this._numberOfCoconuts=data.numberOfCoconuts;

}

classNorwegianBlueParrotDelegateextendsSpeciesDelegate{

constructor(data,bird){

super(data,bird);

this._voltage=data.voltage;

this._isNailed=data.isNailed;

}

Indeed,nowIhaveasuperclass,IcanmoveanydefaultbehaviorfromBirdtoSpeciesDelegatebyensuringthere’salwayssomethinginthespeciesDelegatefield.

classBird…

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate(data,this);

case'AfricanSwallow':

returnnewAfricanSwallowDelegate(data,this);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrotDelegate(data,this);

default:returnnewSpeciesDelegate(data,this);

}

}

//restofbird'scode…

getplumage(){returnthis._speciesDelegate.plumage;}

getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity

classSpeciesDelegate…

getairSpeedVelocity(){returnnull;}

Ilikethis,asitsimplifiesthedelegatingmethodsonBird.Icaneasilyseewhichbehaviorisdelegatedtothespeciesdelegateandwhichstaysbehind.

Here’sthefinalstateoftheseclasses:

functioncreateBird(data){

returnnewBird(data);

}

classBird{

constructor(data){

this._name=data.name;

this._plumage=data.plumage;

this._speciesDelegate=this.selectSpeciesDelegate(data);

}

getname(){returnthis._name;}

getplumage(){returnthis._speciesDelegate.plumage;}

getairSpeedVelocity(){returnthis._speciesDelegate.airSpeedVelocity;}

selectSpeciesDelegate(data){

switch(data.type){

case'EuropeanSwallow':

returnnewEuropeanSwallowDelegate(data,this);

case'AfricanSwallow':

returnnewAfricanSwallowDelegate(data,this);

case'NorweigianBlueParrot':

returnnewNorwegianBlueParrotDelegate(data,this);

default:returnnewSpeciesDelegate(data,this);

}

}

//restofbird'scode…

}

classSpeciesDelegate{

constructor(data,bird){

this._bird=bird;

}

getplumage(){

returnthis._bird._plumage||"average";

}

getairSpeedVelocity(){returnnull;}

}

classEuropeanSwallowDelegateextendsSpeciesDelegate{

getairSpeedVelocity(){return35;}

}

classAfricanSwallowDelegateextendsSpeciesDelegate{

constructor(data,bird){

super(data,bird);

this._numberOfCoconuts=data.numberOfCoconuts;

}

getairSpeedVelocity(){

return40-2*this._numberOfCoconuts;

}

}

classNorwegianBlueParrotDelegateextendsSpeciesDelegate{

constructor(data,bird){

super(data,bird);

this._voltage=data.voltage;

this._isNailed=data.isNailed;

}

getairSpeedVelocity(){

return(this._isNailed)?0:10+this._voltage/10;

}

getplumage(){

if(this._voltage>100)return"scorched";

elsereturnthis._bird._plumage||"beautiful";

}

}

Thisexamplereplacestheoriginalsubclasseswithadelegate,butthereisstillaverysimilarinheritancestructureinSpeciesDelegate.HaveIgainedanythingfromthisrefactoring,otherthanfreeingupinheritanceonBird?Thespeciesinheritanceisnowmoretightlyscoped,coveringjustthedataandfunctionsthatvaryduetothespecies.Anycodethat’sthesameforallspeciesremainsonBirdanditsfuturesubclasses.

Icouldapplythesameideaofcreatingasuperclassdelegatetothebookingexampleearlier.ThiswouldallowmetoreplacethosemethodsonBookingthathavedispatchlogicwithsimplecallstothedelegateandlettingitsinheritancesortoutthedispatch.However,it’snearlydinner-time,soI’llleavethatasanexerciseforthereader.

Theseexamplesillustratethatthephrase“Favorobjectcompositionoverclassinheritance”mightbetterbesaidas“Favorajudiciousmixtureofcompositionandinheritanceovereitheralone”—butIfearthatisnotascatchy.

ReplaceSuperclasswithDelegate

formerly:ReplaceInheritancewithDelegation

Motivation

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

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

ThisisanexampleofonereasontouseReplaceSuperclasswithDelegate—if

functionsofthesuperclassdon’tmakesenseonthesubclass,that’sasignthatIshouldn’tbeusinginheritancetousethesuperclass’sfunctionality.

Aswellasusingallthefunctionsofthesuperclass,itshouldalsobetruethateveryinstanceofthesubclassisaninstanceofthesuperclassandavalidobjectinallcaseswherewe’reusingthesuperclass.IfIhaveacarmodelclass,withthingslikenameandenginesize,ImightthinkIcouldreusethesefeaturestorepresentaphysicalcar,addingfunctionsforVINnumberandmanufacturingdate.Thisisacommon,andoftensubtle,modelingmistakewhichI’vecalledthetype-instancehomonym(https://martinfowler.com/bliki/TypeInstanceHomonym.html).

Thesearebothexamplesofproblemsleadingtoconfusionanderrors—whichcanbeeasilyavoidedbyreplacinginheritancewithdelegationtoaseparateobject.Usingdelegationmakesitclearthatitisaseparatething—onewhereonlysomeofthefunctionscarryover.

Evenincaseswherethesubclassisreasonablemodeling,IuseReplaceSuper-classwithDelegatebecausetherelationshipbetweenasub-andsuperclassishighlycoupled,withthesubclasseasilybrokenbychangesinthesuperclass.ThedownsideisthatIneedtowriteaforwardingfunctionforanyfunctionthatisthesameinthehostandinthedelegate—but,fortunately,eventhoughsuchforwardingfunctionsareboringtowrite,theyaretoosimpletogetwrong.

Asaconsequenceofallthis,somepeopleadviseavoidinginheritanceentirely—butIdon’tagreewiththat.Providedtheappropriatesemanticconditionsapply(everymethodonthesupertypeappliestothesubtype,everyinstanceofthesubtypeisaninstanceofthesupertype),inheritanceisasimpleandeffectivemechanism.IcaneasilyapplyReplaceSuperclasswithDelegateshouldthesituationchangeandinheritanceisnolongerthebestoption.Somyadviceisto(mostly)useinheritancefirst,andapplyReplaceSuperclasswithDelegatewhen(andif)itbecomesaproblem.

Mechanics

Createafieldinthesubclassthatreferstothesuperclassobject.Initializethisdelegatereferencetoanewinstance.

Foreachelementofthesuperclass,createaforwardingfunctioninthesubclass

thatforwardstothedelegatereference.Testafterforwardingeachconsistentgroup.

Mostofthetimeyoucantestaftereachfunctionthat’sforwarded,but,forexample,get/setpairscanonlybetestedoncebothhavebeenmoved.

Whenallsuperclasselementshavebeenoverriddenwithforwarders,removetheinheritancelink.

Example

Irecentlywasconsultingforanoldtown’slibraryofancientscrolls.Theykeepdetailsoftheirscrollsinacatalog.EachscrollhasanIDnumberandrecordsitstitleandlistoftags.

classCatalogItem…

constructor(id,title,tags){

this._id=id;

this._title=title;

this._tags=tags;

}

getid(){returnthis._id;}

gettitle(){returnthis._title;}

hasTag(arg){returnthis._tags.includes(arg);}

Oneofthethingsthatscrollsneedisregularcleaning.Thecodeforthatusesthecatalogitemandextendsitwiththedataitneedsforcleaning.

classScrollextendsCatalogItem…

constructor(id,title,tags,dateLastCleaned){

super(id,title,tags);

this._lastCleaned=dateLastCleaned;

}

needsCleaning(targetDate){

constthreshold=this.hasTag("revered")?700:1500;

returnthis.daysSinceLastCleaning(targetDate)>threshold;

}

daysSinceLastCleaning(targetDate){

returnthis._lastCleaned.until(targetDate,ChronoUnit.DAYS);

}

Thisisanexampleofacommonmodelingerror.Thereisadifferencebetweenthephysicalscrollandthecatalogitem.Thescrolldescribingthetreatmentforthegreyscalediseasemayhaveseveralcopies,butbejustoneiteminthecatalog.

Itmanysituations,Icangetawaywithanerrorlikethis.Icanthinkofthetitleandtagsascopiesofdatainthecatalog.Shouldthisdataneverchange,Icangetawaywiththisrepresentation.ButifIneedtoupdateeither,Imustbecarefultoensurethatallcopiesofthesamecatalogitemareupdatedcorrectly.

Evenwithoutthisissue,I’dstillwanttochangetherelationship.Usingcatalogitemasasuperclasstoscrollislikelytoconfuseprogrammersinthefuture,andisthusapoormodeltoworkwith.

Ibeginbycreatingapropertyinscrollthatreferstothecatalogitem,initializingitwithanewinstance.

classScrollextendsCatalogItem…

constructor(id,title,tags,dateLastCleaned){

super(id,title,tags);

this._catalogItem=newCatalogItem(id,title,tags);

this._lastCleaned=dateLastCleaned;

}

IcreateforwardingmethodsforeachelementofthesuperclassthatIuseonthesubclass.

classScroll…

getid(){returnthis._catalogItem.id;}

gettitle(){returnthis._catalogItem.title;}

hasTag(aString){returnthis._catalogItem.hasTag(aString);}

Iremovetheinheritancelinktothecatalogitem.

classScrollextendsCatalogItem{

constructor(id,title,tags,dateLastCleaned){

super(id,title,tags);

this._catalogItem=newCatalogItem(id,title,tags);

this._lastCleaned=dateLastCleaned;

}

BreakingtheinheritancelinkfinishesthebasicReplaceSuperclasswithDelegaterefactoring,butthereissomethingmoreIneedtodointhiscase.

Therefactoringshiftstheroleofthecatalogitemtothatofacomponentofscroll;eachscrollcontainsauniqueinstanceofacatalogitem.InmanycaseswhereIdothisrefactoring,thisisenough.However,inthissituationabettermodelistolinkthegreyscalecatalogitemtothesixscrollsinthelibrarythatarecopiesofthatwriting.Doingthisis,essentially,ChangeValuetoReference(256).

There’saproblemthatIhavetofix,however,beforeIuseChangeValuetoReference(256).Intheoriginalinheritancestructure,thescrollusedthecatalogitem’sIDfieldtostoreitsID.ButifItreatthecatalogitemasareference,itneedstousethatIDforthecatalogitemIDratherthanthescrollID.ThismeansIneedtocreateanIDfieldonscrollandusethatinsteadofoneincatalogitem.It’sasort-ofmove,sort-ofsplit.

classScroll…

constructor(id,title,tags,dateLastCleaned){

this._id=id;

this._catalogItem=newCatalogItem(null,title,tags);

this._lastCleaned=dateLastCleaned;

}

getid(){returnthis._id;}

CreatingacatalogitemwithanullIDwouldusuallyraiseredflagsandcausealarmstosound.Butthat’sjusttemporarywhileIgetthingsintoshape.OnceI’vedonethat,thescrollswillrefertoasharedcatalogitemwithitsproperID.

Currentlythescrollsareloadedaspartofaloadroutine.

loadroutine…

constscrolls=aDocument

.map(record=>newScroll(record.id,

record.catalogData.title,

record.catalogData.tags,

LocalDate.parse(record.lastCleaned)));

ThefirststepinChangeValuetoReference(256)isfindingorcreatinga

repository.IfindthereisarepositorythatIcaneasilyimportintotheloadroutine.TherepositorysuppliescatalogitemsindexedbyanID.MynexttaskistoseehowtogetthatIDintotheconstructorofthescroll.Fortunately,it’spresentintheinputdataandwasbeingignoredasitwasn’tusefulwhenusinginheritance.Withthatsortedout,IcannowuseChangeFunctionDeclaration(124)toaddboththecatalogandthecatalogitem’sIDtotheconstructorparameters.

loadroutine…

constscrolls=aDocument

.map(record=>newScroll(record.id,

record.catalogData.title,

record.catalogData.tags,

LocalDate.parse(record.lastCleaned),

record.catalogData.id,

catalog));

classScroll…

constructor(id,title,tags,dateLastCleaned,catalogID,catalog){

this._id=id;

this._catalogItem=newCatalogItem(null,title,tags);

this._lastCleaned=dateLastCleaned;

}

InowmodifytheconstructortousethecatalogIDtolookupthecatalogitemanduseitinsteadofcreatinganewone.

classScroll…

constructor(id,title,tags,dateLastCleaned,catalogID,catalog){

this._id=id;

this._catalogItem=catalog.get(catalogID);

this._lastCleaned=dateLastCleaned;

}

Inolongerneedthetitleandtagspassedintotheconstructor,soIuseChangeFunctionDeclaration(124)toremovethem.

loadroutine…

constscrolls=aDocument

.map(record=>newScroll(record.id,

record.catalogData.title,

record.catalogData.tags,

LocalDate.parse(record.lastCleaned),

record.catalogData.id,

catalog));

classScroll…

constructor(id,title,tags,dateLastCleaned,catalogID,catalog){

this._id=id;

this._catalogItem=catalog.get(catalogID);

this._lastCleaned=dateLastCleaned;

}

Bibliography[bib-beck-xpe]ISBN0321278658.

[bib-gof]ISBN0201634988.

[bib-refactoring-book]https://martinfowler.com/books/refactoring.html.

[bib-opdyke]TODO:fix.

[bib-beck-sbpp]ISBN013476904X.

[bib-xp]ISBN0321278658.

[bib-agile]https://martinfowler.com/articles/newMethodology.html.

[bib-tdd]https://martinfowler.com/bliki/TestDrivenDevelopment.html.

[bib-refact-doc]https://martinfowler.com/articles/refactoring-document-load.html.

[bib-forsgren]ISBN1942788339.

[bib-feathers-welc]ISBN0131177052.

[bib-refact-db]https://martinfowler.com/books/refactoringDatabases.html.

[bib-evo-db]https://martinfowler.com/articles/evodb.html.

[bib-evo-arch]ISBN1491986360.

[bib-yagni]https://martinfowler.com/bliki/Yagni.html.

[bib-wake-workbook]ISBN0321109295.

[bib-r2p]https://martinfowler.com/books/r2p.html.

[bib-loop-pipe-article]https://martinfowler.com/articles/refactoring-

pipelines.html.

[bib-refact-html]https://martinfowler.com/books/refactoringHtml.html.

[bib-refact-ruby]https://martinfowler.com/books/refactoringRubyEd.html.

[bib-ref.com]https://refactoring.com.

[bib-bazuzi-safe]http://jay.bazuzi.com/Safely-extract-a-method-in-any-C++-code/.

[bib-parnas]https://dl.acm.org/citation.cfm?id=361623.

[bib-cqs]http://martinfowler.com/bliki/CommandQuerySeparation.html.

[bib-coll-pipe]https://martinfowler.com/articles/collection-pipeline/.

[bib-humble-object]http://xunitpatterns.com/Humble%20Object.html.

[bib-list-and-hash]https://www.martinfowler.com/bliki/ListAndHash.html.

[bib-observer]ISBN0201634988.

[bib-overloaded-getter-setter]http://martinfowler.com/bliki/OverloadedGetterSetter.html.

[bib-pres-domain-sep]https://martinfowler.com/bliki/PresentationDomainSeparation.html.

[bib-range-pattern]http://martinfowler.com/eaaDev/Range.html.

[bib-repository]https://martinfowler.com/eaaCatalog/repository.html.

[bib-test-coverage]https://martinfowler.com/bliki/TestCoverage.html.

[bib-value-object]http://martinfowler.com/bliki/ValueObject.html.

[bib-null-object]https://en.wikipedia.org/wiki/Null_Object_pattern.

[bib-uniform-access]

https://martinfowler.com/bliki/UniformAccessPrinciple.html.

[bib-2hard]https://martinfowler.com/bliki/TwoHardThings.html.

[bib-form-template]https://martinfowler.com/books/r2p.html.

[bib-xunit]https://www.martinfowler.com/bliki/Xunit.html.

[bib-jackson]https://github.com/FasterXML/jackson.

[bib-lang-server]https://langserver.org.

[bib-intellij]https://www.jetbrains.com/idea/.

[bib-eclipse]http://www.eclipse.org.

[bib-mocha]https://mochajs.org.

[bib-chai]http://chaijs.com.

[bib-babel]https://babeljs.io.

[bib-maudite]https://en.wikipedia.org/wiki/Unibroue.