Developer’s Guide€¦ · Table of Contents About this Guide ..... 1
Table of Contents · Table of Contents Mastering Python Design Patterns Credits About the Author...
Transcript of Table of Contents · Table of Contents Mastering Python Design Patterns Credits About the Author...
-
TableofContentsMasteringPythonDesignPatternsCreditsAbouttheAuthorAbouttheReviewerswww.PacktPub.comSupportfiles,eBooks,discountoffers,andmore
Whysubscribe?FreeaccessforPacktaccountholders
PrefaceDesignpatternsCommonmisunderstandingsaboutdesignpatternsDesignpatternsandPythonWhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1.TheFactoryPatternFactoryMethod
Areal-lifeexampleAsoftwareexampleUsecasesImplementation
AbstractFactoryAreal-lifeexampleAsoftwareexampleUsecasesImplementation
Summary2.TheBuilderPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
3.ThePrototypePatternAreal-lifeexampleAsoftwareexample
-
UsecasesImplementationSummary
4.TheAdapterPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
5.TheDecoratorPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
6.TheFacadePatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
7.TheFlyweightPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
8.TheModel-View-ControllerPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
9.TheProxyPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
10.TheChainofResponsibilityPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
11.TheCommandPatternAreal-lifeexample
-
AsoftwareexampleUsecasesImplementationSummary
12.TheInterpreterPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
13.TheObserverPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
14.TheStatePatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
15.TheStrategyPatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
16.TheTemplatePatternAreal-lifeexampleAsoftwareexampleUsecasesImplementationSummary
Index
-
MasteringPythonDesignPatterns
-
MasteringPythonDesignPatternsCopyright©2015PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:January2015
Productionreference:1220115
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78398-932-4
www.packtpub.com
http://www.packtpub.com
-
CreditsAuthor
SakisKasampalis
Reviewers
EvanDempsey
AmitabhSharma
YogendraSharma
PatrycjaSzabłowska
CommissioningEditor
KunalParikh
AcquisitionEditor
OwenRoberts
ContentDevelopmentEditor
SumeetSawant
TechnicalEditors
TanviBhatt
GauravSuri
CopyEditors
ShivangiChaturvedi
NithyaP.
AdithiShetty
ProjectCoordinator
AboliAmbardekar
Proofreaders
AmeeshaGreen
-
JoyceLittlejohn
Indexer
TejalSoni
Graphics
AbhinashSahu
ProductionCoordinator
AparnaBhagat
CoverWork
AparnaBhagat
-
AbouttheAuthorSakisKasampalis(@SKasampalis)isasoftwareengineerlivingintheNetherlands.Heisnotdogmaticaboutparticularprogramminglanguagesandtools;hisprincipleisthattherighttoolshouldbeusedfortherightjob.OneofhisfavoritetoolsisPythonbecausehefindsitveryproductive.
SakiswasalsothetechnicalreviewerofMasteringObject-orientedPythonandLearningPythonDesignPatterns,publishedbyPacktPublishing.
Iwanttothankmysweetheart,Georgia,forsupportingthiseffort.ManythankstoOwenRobertswhoencouragedmetowritethisbook.IalsowanttothankSumeetSawantforbeingaverykindandcooperativecontentdevelopmenteditor.Lastbutnotleast,Iwanttothankthereviewersofthisbookfortheirvaluablefeedback.
-
AbouttheReviewersEvanDempseyisasoftwaredeveloperfromWaterford,Ireland.Whenheisn'thackinginPythonforfunandprofit,heenjoyscraftbeers,commonLisp,andkeepingupwithmodernresearchinmachinelearning.Heisacontributortoseveralopensourceprojects.
AmitabhSharmaisaprofessionalsoftwareengineer.Hehasworkedextensivelyonenterpriseapplicationsintelecommunicationsandbusinessanalytics.Hisworkisfocusedonservice-orientedarchitecture,datawarehouses,andlanguagessuchasJava,Python,andothers.
IwouldliketothankmygrandfatherandmyfatherforallowingmetolearnallthatIcan.Iwouldalsoliketothankmywife,Komal,forhersupportandencouragement.
YogendraSharmawasbornandbroughtupinasmallbutculturaltown,Pratapgarh,inthestateofRajasthan.Hisbasiceducationhasbeenimpartedinhishometownitself,andhecompletedhisBTechinComputerSciencefromJaipur.Heisbasicallyanengineerbyheartandatechnicalenthusiastbynature.
HehasvastexperienceinthefieldsofPython,Djangoframework,webappsecurity,networking,Web2.0,andC++.
AlongwithCCNA,manyotheresteemedcertificationshavebeenawardedtohim.HeisanactivememberofInternationalAssociationofEngineers,Ubuntu,India,andComputerSocietyofIndia.
Morerecently,heparticipatedinbugbountyprogramsandwonmanybugbounties,includingtherespectedYahoo,Ebay,PayPalbugbounty.Hehasbeenappointedassecurityresearcherforseveralrespectedorganizations,suchasAdobe,Ebay,Avira,Moodle,Cisco,Atlassian,Basecamp,CodeClimate,Abacus,Rediff,Assembla,RecruiterBox,Tumbler,Wrike,Indeed,HybridSaaS,Sengrid,andSnapEngag.
Hehasreviewedmanybooksfromreputedpublishinghouses.YoucanfindhimonLinkedInathttp://in.linkedin.com/in/yogendra0sharma.
Iwouldliketothankallmyfriendswhoalwaysencouragedmetodosomethingnewandbelievinginme.
PatrycjaSzabłowskaisaPythondeveloperwithsomeJavabackground,withexperiencemainlyinbackenddevelopment.ShegraduatedfromNicolausCopernicusUniversityinToruń,Poland.
SheiscurrentlyworkinginWarsaw,Poland,atGrupaWirtualnaPolska.Sheisconstantlyexploringtechnicalnoveltiesandisopen-mindedandeagertolearnaboutthenextPython
http://in.linkedin.com/in/yogendra0sharma
-
libraryorframework.HerfavoriteprogrammingmottoisCodeisreadmuchmoreoftenthanitiswritten.
I'dliketothankmyhusband,Wacław,forencouragingmetoexplorenewfrontiers,andalsomyparentsforteachingmewhatmattersthemost.
-
www.PacktPub.com
Supportfiles,eBooks,discountoffers,andmoreForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www2.packtpub.com/books/subscription/packtlib
DoyouneedinstantsolutionstoyourITquestions?PacktLibisPackt'sonlinedigitalbooklibrary.Here,youcansearch,access,andreadPackt'sentirelibraryofbooks.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
FreeaccessforPacktaccountholdersIfyouhaveanaccountwithPacktatwww.PacktPub.com,youcanusethistoaccessPacktLibtodayandviewnineentirelyfreebooks.Simplyuseyourlogincredentialsforimmediateaccess.
http://www.PacktPub.comhttp://www.PacktPub.commailto:[email protected]://www.PacktPub.comhttps://www2.packtpub.com/books/subscription/packtlibhttp://www.PacktPub.com
-
Preface
DesignpatternsInsoftwareengineering,adesignpatternisarecommendedsolutiontoasoftwaredesignproblem.Designpatternsgenerallydescribehowtostructureourcodetosolvecommondesignproblemsusingbestpractices.Itisimportanttonotethatadesignpatternisahigh-levelsolution;itdoesn'tfocusonimplementationdetailssuchasalgorithmsanddatastructures[GOF95,page13],[j.mp/srcmdp].Itisuptous,assoftwareengineers,todecidewhichalgorithmanddatastructureisoptimaltousefortheproblemwearetryingtosolve.
NoteIfyouarewonderingwhatisthemeaningofthetextwithin[],pleasejumptotheConventionssectionofthisprefaceforamomenttoseehowreferencesareformattedinthisbook.
Themostimportantpartofadesignpatternisprobablyitsname.Thebenefitofnamingallpatternsisthatwehave,onourhands,acommonvocabularytocommunicate[GOF95,page13].Thus,ifyousendsomecodeforreviewandyourpeerreviewergivesfeedbackmentioning"IthinkthatyoucanuseaStrategyhereinsteadof...",evenifyoudon'tknoworrememberwhatastrategyis,youcanimmediatelylookitup.
Asprogramminglanguagesevolve,somedesignpatternssuchasSingletonbecomeobsoleteorevenantipatterns[j.mp/jalfdp],othersarebuiltintheprogramminglanguage(iterator),andnewpatternsareborn(Borg/Monostate[j.mp/amdpp],[j.mp/wikidpc]).
http://j.mp/srcmdphttp://j.mp/jalfdphttp://j.mp/amdpphttp://j.mp/wikidpc
-
CommonmisunderstandingsaboutdesignpatternsThereareafewmisunderstandingsaboutdesignpatterns.Onemisunderstandingisthatdesignpatternsshouldbeusedrightfromthestartwhenwritingcode.Itisnotunusualtoseedevelopersstrugglingwithwhichpatterntheyshoulduseintheircode,eveniftheyhaven'tfirsttriedtosolvetheproblemintheirownway[j.mp/prsedp],[j.mp/stedp].
Notonlyisthiswrong,butitisalsoagainstthenatureofdesignpatterns.Designpatternsarediscovered(incontrasttoinvented)asbettersolutionsoverexistingsolutions.Ifyouhavenoexistingsolution,itdoesn'tmakesensetolookforabetterone.Justgoaheadanduseyourskillstosolveyourproblemasbestasyouthink.Ifyourcodereviewershavenoobjectionsandthroughtimeyouseethatyoursolutionissmartandflexibleenough,itmeansthatyoudon'tneedtowasteyourtimeonstrugglingaboutwhichpatterntouse.Youmighthaveevendiscoveredabetterdesignpatternthantheexistingone.Whoknows?Thepointisdonotlimityourcreativityinfavorofforcingyourselftouseexistingdesignpatterns.
Asecondmisunderstandingisthatdesignpatternsshouldbeusedeverywhere.Thisresultsincreatingcomplexsolutionswithunnecessaryinterfacesandhierarchies,whereasimplerandstraightforwardsolutionwouldbesufficient.Donotreatdesignpatternsasapanaceabecausetheyarenot.Theymustbeusedonlyifthereisproofthatyourexistingcode"smells",andishardtoextendandmaintain.Trythinkingintermsofyouaren'tgonnaneedit(YAGNI[j.mp/c2yagni])andKeepitsimplestupid(KISS[j.mp/wikikis]).Usingdesignpatternseverywhereisasevilasprematureoptimization[j.mp/c2pro].
http://j.mp/prsedphttp://j.mp/stedphttp://j.mp/c2yagnihttp://j.mp/wikikishttp://j.mp/c2pro
-
DesignpatternsandPythonThisbookfocusesondesignpatternsinPython.Pythonisdifferentthanmostcommonprogramminglanguagesusedinpopulardesignpatternsbooks(usuallyJava[FFBS04]orC++[GOF95]).Itsupportsduck-typing,functionsarefirst-classcitizens,andsomepatterns(forinstance,iteratoranddecorator)arebuilt-infeatures.Theintentofthisbookistodemonstratethemostfundamentaldesignpatterns,notallpatternsthathavebeendocumentedsofar[j.mp/wikidpc].ThecodeexamplesfocusonusingidiomaticPythonwhenapplicable[j.mp/idiompyt].IfyouarenotfamiliarwiththeZenofPython,itisagoodideatoopenthePythonREPLrightnowandexecuteimportthis.TheZenofPythonisbothamusingandmeaningful.
http://j.mp/wikidpchttp://j.mp/idiompyt
-
WhatthisbookcoversPart1:Creationalpatternspresentsdesignpatternsthatdealwithobjectcreation.
Chapter1,TheFactoryPattern,willteachyouhowtousetheFactorydesignpattern(FactoryMethodandAbstractFactory)toinitializeobjects,andcoverthebenefitsofusingtheFactorydesignpatterninsteadofdirectobjectinstantiation.
Chapter2,TheBuilderPattern,willteachyouhowtosimplifythecreationofobjectsthataretypicallycomposedbymorethanonerelatedobjects.
Chapter3,ThePrototypePattern,willteachyouhowtocreateanewobjectthatisafullcopy(hence,thenameclone)ofanexistingobject.
Part2:Structuralpatternspresentsdesignpatternsthatdealwithrelationshipsbetweentheentities(classes,objects,andsoon)ofasystem.
Chapter4,TheAdapterPattern,willteachyouhowtomakeyourexistingcodecompatiblewithaforeigninterface(forexample,anexternallibrary)withminimalchanges.
Chapter5,TheDecoratorPattern,willteachyouhowtoenhancethefunctionalityofanobjectwithoutusinginheritance.
Chapter6,TheFacadePattern,willteachyouhowtocreateasingleentrypointtohidethecomplexityofasystem.
Chapter7,TheFlyweightPattern,willteachyouhowtoreuseobjectsfromanobjectpooltoimprovethememoryusageandpossiblytheperformanceofyourapplications.
Chapter8,TheModel-View-ControllerPattern,willteachyouhowtoimprovethemaintainabilityofyourapplicationsbyavoidingmixingthebusinesslogicwiththeuserinterface.
Chapter9,TheProxyPattern,willteachyouhowtoimprovethesecurityofyourapplicationbyaddinganextralayerofprotection.
Part3:Behavioralpatternspresentsdesignpatternsthatdealwiththecommunicationofthesystem'sentities.
Chapter10,TheChainofResponsibilityPattern,willteachyouhowtosendarequesttomultiplereceivers.
Chapter11,TheCommandPattern,willteachyouhowtomakeyourapplicationcapableofrevertingalreadyappliedoperations.
Chapter12,TheInterpreterPattern,willteachyouhowtocreateasimplelanguageontopofPython,whichcanbeusedbydomainexpertswithoutforcingthemtolearnhowtoprograminPython.
-
Chapter13,TheObserverPattern,willteachyouhowtosendnotificationstotheregisteredstakeholdersofanobjectwheneveritsstatechanges.
Chapter14,TheStatePattern,willteachyouhowtocreateastatemachinetomodelaproblemandthebenefitsofthistechnique.
Chapter15,TheStrategyPattern,willteachyouhowtopick(duringruntime)analgorithmbetweenmanyavailablealgorithms,basedonsomeinputcriteria(forexample,theelementsize).
Chapter16,TheTemplatePattern,willteachyouhowtomakeaclearseparationbetweenthecommonanddifferentpartsofanalgorithmtoavoidunnecessarycodeduplication.
-
WhatyouneedforthisbookThecodeiswrittenexclusivelyinPython3.Python3is,inmanyaspects,notcompatiblewithPython2.x[j.mp/p2orp3].ThefocusisonPython3.4.0butusingPython3.3.0shouldalsobefine,sincetherearenosyntaxdifferencesbetweenPython3.3.0andPython3.4.0[j.mp/py3dot4].Ingeneral,ifyouinstallthelatestPython3versionfromwww.python.org,youshouldbefinewithrunningtheexamples.Mostmodules/librariesthatareusedintheexamplesareapartofthePython3distribution.Ifanexamplerequiresanyextramodulestobeinstalled,instructionsonhowtoinstallthemaregivenbeforepresentingtherelatedcode.
http://j.mp/p2orp3http://j.mp/py3dot4http://www.python.org
-
WhothisbookisforTheaudienceofthisbookisPythonprogrammerswithanintermediatebackgroundandaninterestindesignpatternsimplementedinidiomaticPython.ProgrammersofotherlanguageswhoareinterestedinPythoncanalsobenefit,butit'sbetteriftheyfirstreadsomematerialsthatexplainhowthingsaredoneinPython[j.mp/idiompyt],[j.mp/dspython].
http://j.mp/idiompythttp://j.mp/dspython
-
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"WewillusetwolibrariesthatarepartofthePythondistributionforworkingwithXMLandJSON:xml.etree.ElementTreeandjson."
Ablockofcodeissetasfollows:
@propertydefparsed_data(self):returnself.data
Whenwewishtodrawyourattentiontoaparticularpartofacodeblock,therelevantlinesoritemsaresetinbold:
@propertydefparsed_data(self):returnself.data
Anycommand-lineinputoroutputiswrittenasfollows:
>>>python3factory_method.py
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"ClickingtheNextbuttonmovesyoutothenextscreen."
NoteWarningsorimportantnotesappearinaboxlikethis.
TipTipsandtricksappearlikethis.
Bookreferencesfollowtheformat[Author,page].Forexample,thereference[GOF95,page10]referstothe10thpageoftheGOF(DesignPatterns:ElementsofReusableObject-OrientedSoftware)book.Attheendofthebook,thereisasectiondevotedtoallbookreferences.
-
Webreferencesfollowtheformat[j.mp/shortened].TheseareshortenedURLaddressesthatyoucantypeorcopy/pasteintoyourwebbrowserandberedirectedtothereal(usuallylongerandsometimesuglier)webreference.Forexample,typingj.mp/idiompytinyouwebbrowser'saddressbarshouldredirectyoutohttp://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html.
http://j.mp/idiompythttp://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html
-
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail,andmentionthebook'stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
mailto:[email protected]://www.packtpub.com/authors
-
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesfromyouraccountathttp://www.packtpub.comforallthePacktPublishingbooksyouhavepurchased.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusat,andwewilldoourbesttoaddresstheproblem.
http://www.packtpub.comhttp://www.packtpub.com/supporthttp://www.packtpub.com/submit-erratahttps://www.packtpub.com/books/content/supportmailto:[email protected]:[email protected]
-
Chapter1.TheFactoryPatternCreationaldesignpatternsdealwithanobjectcreation[j.mp/wikicrea].Theaimofacreationaldesignpatternistoprovidebetteralternativesforsituationswhereadirectobjectcreation(whichinPythonhappensbythe__init__()function[j.mp/divefunc],[Lott14,page26])isnotconvenient.
IntheFactorydesignpattern,aclientasksforanobjectwithoutknowingwheretheobjectiscomingfrom(thatis,whichclassisusedtogenerateit).Theideabehindafactoryistosimplifyanobjectcreation.Itiseasiertotrackwhichobjectsarecreatedifthisisdonethroughacentralfunction,incontrasttolettingaclientcreateobjectsusingadirectclassinstantiation[Eckel08,page187].Afactoryreducesthecomplexityofmaintaininganapplicationbydecouplingthecodethatcreatesanobjectfromthecodethatusesit[Zlobin13,page30].
Factoriestypicallycomeintwoforms:theFactoryMethod,whichisamethod(orinPythonicterms,afunction)thatreturnsadifferentobjectperinputparameter[j.mp/factorympat];theAbstractFactory,whichisagroupofFactoryMethodsusedtocreateafamilyofrelatedproducts[GOF95,page100],[j.mp/absfpat].
FactoryMethodIntheFactoryMethod,weexecuteasinglefunction,passingaparameterthatprovidesinformationaboutwhatwewant.Wearenotrequiredtoknowanydetailsabouthowtheobjectisimplementedandwhereitiscomingfrom.
Areal-lifeexampleAnexampleoftheFactoryMethodpatternusedinrealityisinplastictoyconstruction.Themoldingpowderusedtoconstructplastictoysisthesame,butdifferentfigurescanbeproducedusingdifferentplasticmolds.ThisislikehavingaFactoryMethodinwhichtheinputisthenameofthefigurethatwewant(duckandcar)andtheoutputistheplasticfigurethatwerequested.Thetoyconstructioncaseisshowninthefollowingfigure,whichisprovidedbywww.sourcemaking.com[j.mp/factorympat].
http://j.mp/wikicreahttp://j.mp/divefunchttp://j.mp/factorympathttp://j.mp/absfpathttp://www.sourcemaking.comhttp://j.mp/factorympat
-
AsoftwareexampleTheDjangoframeworkusestheFactoryMethodpatternforcreatingthefieldsofaform.TheformsmoduleofDjangosupportsthecreationofdifferentkindsoffields(CharField,EmailField)andcustomizations(max_length,required)[j.mp/djangofacm].
UsecasesIfyourealizethatyoucannottracktheobjectscreatedbyyourapplicationbecausethecodethatcreatesthemisinmanydifferentplacesinsteadofasinglefunction/method,youshouldconsiderusingtheFactoryMethodpattern[Eckel08,page187].TheFactoryMethodcentralizesanobjectcreationandtrackingyourobjectsbecomesmucheasier.NotethatitisabsolutelyfinetocreatemorethanoneFactoryMethod,andthisishowitistypicallydoneinpractice.EachFactoryMethodlogicallygroupsthecreationofobjectsthathavesimilarities.Forexample,oneFactoryMethodmightberesponsibleforconnectingyoutodifferentdatabases(MySQL,SQLite),anotherFactoryMethodmightberesponsibleforcreatingthegeometricalobjectthatyourequest(circle,triangle),andsoon.
TheFactoryMethodisalsousefulwhenyouwanttodecoupleanobjectcreationfromanobjectusage.Wearenotcoupled/boundtoaspecificclasswhencreatinganobject,wejustprovidepartialinformationaboutwhatwewantbycallingafunction.Thismeansthatintroducingchangestothefunctioniseasywithoutrequiringanychangestothecodethatusesit[Zlobin13,page30].
Anotherusecaseworthmentioningisrelatedtoimprovingtheperformanceandmemoryusageofanapplication.AFactoryMethodcanimprovetheperformanceandmemoryusagebycreatingnewobjectsonlyifitisabsolutelynecessary[Zlobin13,page28].Whenwecreateobjectsusingadirectclassinstantiation,extramemoryisallocatedeverytimeanewobjectiscreated(unlesstheclassusescachinginternally,whichisusuallynotthecase).Wecanseethatinpracticeinthefollowingcode(fileid.py),itcreatestwoinstancesofthesameclassAandusestheid()functiontocomparetheirmemoryaddresses.Theaddressesarealsoprintedintheoutputsothatwecaninspectthem.Thefactthatthememoryaddressesare
http://j.mp/djangofacm
-
differentmeansthattwodistinctobjectsarecreatedasfollows:
classA(object):pass
if__name__=='__main__':a=A()b=A()
print(id(a)==id(b))print(a,b)
Executingid.pyonmycomputergivesthefollowingoutput:
>>python3id.pyFalse
NotethattheaddressesthatyouseeifyouexecutethefilearenotthesameasIseebecausetheydependonthecurrentmemorylayoutandallocation.Buttheresultmustbethesame:thetwoaddressesshouldbedifferent.There'soneexceptionthathappensifyouwriteandexecutethecodeinthePythonRead-Eval-PrintLoop(REPL)(interactiveprompt),butthat'saREPL-specificoptimizationwhichisnothappeningnormally.
ImplementationDatacomesinmanyforms.Therearetwomainfilecategoriesforstoring/retrievingdata:human-readablefilesandbinaryfiles.Examplesofhuman-readablefilesareXML,Atom,YAML,andJSON.Examplesofbinaryfilesarethe.sq3fileformatusedbySQLiteandthe.mp3fileformatusedtolistentomusic.
Inthisexample,wewillfocusontwopopularhuman-readableformats:XMLandJSON.Althoughhuman-readablefilesaregenerallyslowertoparsethanbinaryfiles,theymakedataexchange,inspection,andmodificationmucheasier.Forthisreason,itisadvisedtopreferworkingwithhuman-readablefiles,unlessthereareotherrestrictionsthatdonotallowit(mainlyunacceptableperformanceandproprietarybinaryformats).
Inthisproblem,wehavesomeinputdatastoredinanXMLandaJSONfile,andwewanttoparsethemandretrievesomeinformation.Atthesametime,wewanttocentralizetheclient'sconnectiontothose(andallfuture)externalservices.WewillusetheFactoryMethodtosolvethisproblem.TheexamplefocusesonlyonXMLandJSON,butaddingsupportformoreservicesshouldbestraightforward.
First,let'stakealookatthedatafiles.TheXMLfile,person.xml,isbasedontheWikipediaexample[j.mp/wikijson]andcontainsinformationaboutindividuals(firstName,lastName,gender,andsoon)asfollows:
http://j.mp/wikijson
-
JohnSmith25212ndStreetNewYorkNY10021212555-1234646555-4567maleJimyLiar19182ndStreetNewYorkNY10021212555-1234malePattyLiar20182ndStreetNewYorkNY10021212555-1234001452-8819female
TheJSONfile,donut.json,comesfromtheGitHubaccountofAdobe[j.mp/adobejson]andcontainsdonutinformation(type,price/unitthatis,ppu,topping,andsoon)asfollows:
http://j.mp/adobejson
-
[{"id":"0001","type":"donut","name":"Cake","ppu":0.55,"batters":{"batter":[{"id":"1001","type":"Regular"},{"id":"1002","type":"Chocolate"},{"id":"1003","type":"Blueberry"},{"id":"1004","type":"Devil'sFood"}]},"topping":[{"id":"5001","type":"None"},{"id":"5002","type":"Glazed"},{"id":"5005","type":"Sugar"},{"id":"5007","type":"PowderedSugar"},{"id":"5006","type":"ChocolatewithSprinkles"},{"id":"5003","type":"Chocolate"},{"id":"5004","type":"Maple"}]},{"id":"0002","type":"donut","name":"Raised","ppu":0.55,"batters":{"batter":[{"id":"1001","type":"Regular"}]},"topping":[{"id":"5001","type":"None"},{"id":"5002","type":"Glazed"},{"id":"5005","type":"Sugar"},{"id":"5003","type":"Chocolate"},{"id":"5004","type":"Maple"}]},{"id":"0003","type":"donut","name":"OldFashioned","ppu":0.55,"batters":{"batter":[{"id":"1001","type":"Regular"},{"id":"1002","type":"Chocolate"}]},"topping":[{"id":"5001","type":"None"},{"id":"5002","type":"Glazed"},{"id":"5003","type":"Chocolate"},{"id":"5004","type":"Maple"}]}
-
]
WewillusetwolibrariesthatarepartofthePythondistributionforworkingwithXMLandJSON:xml.etree.ElementTreeandjsonasfollows:
importxml.etree.ElementTreeasetreeimportjson
TheJSONConnectorclassparsestheJSONfileandhasaparsed_data()methodthatreturnsalldataasadictionary(dict).Thepropertydecoratorisusedtomakeparsed_data()appearasanormalvariableinsteadofamethodasfollows:
classJSONConnector:
def__init__(self,filepath):self.data=dict()withopen(filepath,mode='r',encoding='utf-8')asf:self.data=json.load(f)
@propertydefparsed_data(self):returnself.data
TheXMLConnectorclassparsestheXMLfileandhasaparsed_data()methodthatreturnsalldataasalistofxml.etree.Elementasfollows:
classXMLConnector:
def__init__(self,filepath):self.tree=etree.parse(filepath)
@propertydefparsed_data(self):returnself.tree
Theconnection_factory()functionisaFactoryMethod.ItreturnsaninstanceofJSONConnectororXMLConnectordependingontheextensionoftheinputfilepathasfollows:
defconnection_factory(filepath):iffilepath.endswith('json'):connector=JSONConnectoreliffilepath.endswith('xml'):connector=XMLConnectorelse:raiseValueError('Cannotconnectto{}'.format(filepath))returnconnector(filepath)
Theconnect_to()functionisawrapperofconnection_factory().Itaddsexceptionhandlingasfollows:
defconnect_to(filepath):
-
factory=Nonetry:factory=connection_factory(filepath)exceptValueErrorasve:print(ve)returnfactory
Themain()functiondemonstrateshowtheFactoryMethoddesignpatterncanbeused.Thefirstpartmakessurethatexceptionhandlingiseffectiveasfollows:
defmain():sqlite_factory=connect_to('data/person.sq3')
ThenextpartshowshowtoworkwiththeXMLfilesusingtheFactoryMethod.XPathisusedtofindallpersonelementsthathavethelastnameLiar.Foreachmatchedperson,thebasicnameandphonenumberinformationareshownasfollows:
xml_factory=connect_to('data/person.xml')xml_data=xml_factory.parsed_data()liars=xml_data.findall(".//{person}[{lastName}='{}']".format('Liar'))print('found:{}persons'.format(len(liars)))forliarinliars:print('firstname:{}'.format(liar.find('firstName').text))print('lastname:{}'.format(liar.find('lastName').text))[print('phonenumber({}):'.format(p.attrib['type']),p.text)forpinliar.find('phoneNumbers')]
ThefinalpartshowshowtoworkwiththeJSONfilesusingtheFactoryMethod.Here,there'snopatternmatching,andthereforethename,price,andtoppingofalldonutsareshownasfollows:
json_factory=connect_to('data/donut.json')json_data=json_factory.parsed_dataprint('found:{}donuts'.format(len(json_data)))fordonutinjson_data:print('name:{}'.format(donut['name']))print('price:${}'.format(donut['ppu']))[print('topping:{}{}'.format(t['id'],t['type']))fortindonut['topping']]
Forcompleteness,hereisthecompletecodeoftheFactoryMethodimplementation(factory_method.py)asfollows:
importxml.etree.ElementTreeasetreeimportjson
classJSONConnector:def__init__(self,filepath):self.data=dict()withopen(filepath,mode='r',encoding='utf-8')asf:self.data=json.load(f)
-
@propertydefparsed_data(self):returnself.data
classXMLConnector:def__init__(self,filepath):self.tree=etree.parse(filepath)
@propertydefparsed_data(self):returnself.tree
defconnection_factory(filepath):iffilepath.endswith('json'):connector=JSONConnectoreliffilepath.endswith('xml'):connector=XMLConnectorelse:raiseValueError('Cannotconnectto{}'.format(filepath))returnconnector(filepath)
defconnect_to(filepath):factory=Nonetry:factory=connection_factory(filepath)exceptValueErrorasve:print(ve)returnfactory
defmain():sqlite_factory=connect_to('data/person.sq3')print()
xml_factory=connect_to('data/person.xml')xml_data=xml_factory.parsed_dataliars=xml_data.findall(".//{}[{}='{}']".format('person','lastName','Liar'))print('found:{}persons'.format(len(liars)))forliarinliars:print('firstname:{}'.format(liar.find('firstName').text))print('lastname:{}'.format(liar.find('lastName').text))[print('phonenumber({}):'.format(p.attrib['type']),p.text)forpinliar.find('phoneNumbers')]print()
json_factory=connect_to('data/donut.json')json_data=json_factory.parsed_dataprint('found:{}donuts'.format(len(json_data)))fordonutinjson_data:print('name:{}'.format(donut['name']))print('price:${}'.format(donut['ppu']))[print('topping:{}{}'.format(t['id'],t['type']))fortindonut['topping']]
if__name__=='__main__':main()
Hereistheoutputofthisprogramasfollows:
-
>>>python3factory_method.pyCannotconnecttodata/person.sq3
found:2personsfirstname:Jimylastname:Liarphonenumber(home):212555-1234firstname:Pattylastname:Liarphonenumber(home):212555-1234phonenumber(mobile):001452-8819
found:3donutsname:Cakeprice:$0.55topping:5001Nonetopping:5002Glazedtopping:5005Sugartopping:5007PowderedSugartopping:5006ChocolatewithSprinklestopping:5003Chocolatetopping:5004Maplename:Raisedprice:$0.55topping:5001Nonetopping:5002Glazedtopping:5005Sugartopping:5003Chocolatetopping:5004Maplename:OldFashionedprice:$0.55topping:5001Nonetopping:5002Glazedtopping:5003Chocolatetopping:5004Maple
NoticethatalthoughJSONConnectorandXMLConnectorhavethesameinterfaces,whatisreturnedbyparsed_data()isnothandledinauniformway.Differentpythoncodemustbeusedtoworkwitheachconnector.Althoughitwouldbenicetobeabletousethesamecodeforallconnectors,thisisatmosttimesnotrealisticunlessweusesomekindofcommonmappingforthedatawhichisveryoftenprovidedbyexternaldataproviders.AssumingthatyoucanuseexactlythesamecodeforhandlingtheXMLandJSONfiles,whatchangesarerequiredtosupportathirdformat,forexample,SQLite?FindanSQLitefileorcreateyourownandtryit.
Asitisnow,thecodedoesnotforbidadirectinstantiationofaconnector.Isitpossibletodothis?Trydoingit.
TipHint:FunctionsinPythoncanhavenestedclasses.
-
AbstractFactoryTheAbstractFactorydesignpatternisageneralizationofFactoryMethod.Basically,anAbstractFactoryisa(logical)groupofFactoryMethods,whereeachFactoryMethodisresponsibleforgeneratingadifferentkindofobject[Eckel08,page193].
Areal-lifeexampleAbstractFactoryisusedincarmanufacturing.Thesamemachineryisusedforstampingtheparts(doors,panels,hoods,fenders,andmirrors)ofdifferentcarmodels.Themodelthatisassembledbythemachineryisconfigurableandeasytochangeatanytime.WecanseeanexampleofthecarmanufacturingAbstractFactoryinthefollowingfigure,whichisprovidedbywww.sourcemaking.com[j.mp/absfpat].
AsoftwareexampleThedjango_factorypackageisanAbstractFactoryimplementationforcreatingDjangomodelsintests.Itisusedforcreatinginstancesofmodelsthatsupporttest-specificattributes.Thisisimportantbecausethetestsbecomereadableandavoidsharingunnecessarycode[j.mp/djangoabs].
http://www.sourcemaking.comhttp://j.mp/absfpathttp://j.mp/djangoabs
-
UsecasesSincetheAbstractFactorypatternisageneralizationoftheFactoryMethodpattern,itoffersthesamebenefits:itmakestrackinganobjectcreationeasier,itdecouplesanobjectcreationfromanobjectusage,anditgivesusthepotentialtoimprovethememoryusageandperformanceofourapplication.
Butaquestionisraised:howdoweknowwhentousetheFactoryMethodversususinganAbstractFactory?TheansweristhatweusuallystartwiththeFactoryMethodwhichissimpler.IfwefindoutthatourapplicationrequiresmanyFactoryMethodswhichitmakessensetocombineforcreatingafamilyofobjects,weendupwithanAbstractFactory.
AbenefitoftheAbstractFactorythatisusuallynotveryvisiblefromauser'spointofviewwhenusingtheFactoryMethodisthatitgivesustheabilitytomodifythebehaviorofourapplicationdynamically(inruntime)bychangingtheactiveFactoryMethod.Theclassicexampleisgivingtheabilitytochangethelookandfeelofanapplication(forexample,Apple-like,Windows-like,andsoon)fortheuserwhiletheapplicationisinuse,withouttheneedtoterminateitandstartitagain[GOF95,page99].
ImplementationTodemonstratetheAbstractFactorypattern,Iwillreuseoneofmyfavoriteexamples,includedinPython3Patterns&Idioms,BruceEckel,[Eckel08,page193].Imaginethatwearecreatingagameorwewanttoincludeamini-gameaspartofourapplicationtoentertainourusers.Wewanttoincludeatleasttwogames,oneforchildrenandoneforadults.Wewilldecidewhichgametocreateandlaunchinruntime,basedonuserinput.AnAbstractFactorytakescareofthegamecreationpart.
Let'sstartwiththekid'sgame.ItiscalledFrogWorld.Themainheroisafrogwhoenjoyseatingbugs.Everyheroneedsagoodname,andinourcasethenameisgivenbytheuserinruntime.Theinteract_with()methodisusedtodescribetheinteractionofthefrogwithanobstacle(forexample,bug,puzzle,andotherfrog)asfollows:
classFrog:def__init__(self,name):self.name=name
def__str__(self):returnself.name
definteract_with(self,obstacle):print('{}theFrogencounters{}and{}!'.format(self,obstacle,obstacle.action()))
TherecanbemanydifferentkindsofobstaclesbutforourexampleanobstaclecanonlybeaBug.Whenthefrogencountersabug,onlyoneactionissupported:iteatsit!
classBug:
-
def__str__(self):return'abug'
defaction(self):return'eatsit'
TheFrogWorldclassisanAbstractFactory.Itsmainresponsibilitiesarecreatingthemaincharacterandtheobstacle(s)ofthegame.Keepingthecreationmethodsseparateandtheirnamesgeneric(forexample,make_character(),make_obstacle())allowsustodynamicallychangetheactivefactory(andthereforetheactivegame)withoutanycodechanges.Inastaticallytypedlanguage,theAbstractFactorywouldbeanabstractclass/interfacewithemptymethods,butinPythonthisisnotrequiredbecausethetypesarecheckedinruntime[Eckel08,page195],[j.mp/ginstromdp]asfollows:
classFrogWorld:def__init__(self,name):print(self)self.player_name=namedef__str__(self):return'\n\n\t------FrogWorld-------'
defmake_character(self):returnFrog(self.player_name)
defmake_obstacle(self):returnBug()
TheWizardWorldgameissimilar.Theonlydifferencesarethatthewizardbattlesagainstmonsterslikeorksinsteadofeatingbugs!
classWizard:def__init__(self,name):self.name=name
def__str__(self):returnself.name
definteract_with(self,obstacle):print('{}theWizardbattlesagainst{}and{}!'.format(self,obstacle,obstacle.action()))
classOrk:def__str__(self):return'anevilork'
defaction(self):return'killsit'
classWizardWorld:def__init__(self,name):print(self)self.player_name=name
def__str__(self):
http://j.mp/ginstromdp
-
return'\n\n\t------WizardWorld-------'
defmake_character(self):returnWizard(self.player_name)
defmake_obstacle(self):returnOrk()
TheGameEnvironmentisthemainentrypointofourgame.Itacceptsfactoryasaninput,andusesittocreatetheworldofthegame.Theplay()methodinitiatestheinteractionbetweenthecreatedheroandtheobstacleasfollows:
classGameEnvironment:def__init__(self,factory):self.hero=factory.make_character()self.obstacle=factory.make_obstacle()
defplay(self):self.hero.interact_with(self.obstacle)
Thevalidate_age()functionpromptstheusertogiveavalidage.Iftheageisnotvalid,itreturnsatuplewiththefirstelementsettoFalse.Iftheageisfine,thefirstelementofthetupleissettoTrueandthat'sthecasewhereweactuallycareaboutthesecondelementofthetuple,whichistheagegivenbytheuserasfollows:
defvalidate_age(name):try:age=input('Welcome{}.Howoldareyou?'.format(name))age=int(age)exceptValueErroraserr:print("Age{}isinvalid,pleasetryagain...".format(age))return(False,age)return(True,age)
Lastbutnotleastcomesthemain()function.Itasksfortheuser'snameandage,anddecideswhichgameshouldbeplayedbytheageoftheuserasfollows:
defmain():name=input("Hello.What'syourname?")valid_input=Falsewhilenotvalid_input:valid_input,age=validate_age(name)game=FrogWorldifage<18elseWizardWorldenvironment=GameEnvironment(game(name))environment.play()
AndthecompletecodeoftheAbstractFactoryimplementation(abstract_factory.py)isgivenasfollows:
classFrog:def__init__(self,name):self.name=name
-
def__str__(self):returnself.name
definteract_with(self,obstacle):print('{}theFrogencounters{}and{}!'.format(self,obstacle,obstacle.action()))
classBug:def__str__(self):return'abug'
defaction(self):return'eatsit'
classFrogWorld:def__init__(self,name):print(self)self.player_name=namedef__str__(self):return'\n\n\t------FrogWorld-------'
defmake_character(self):returnFrog(self.player_name)
defmake_obstacle(self):returnBug()
classWizard:def__init__(self,name):self.name=name
def__str__(self):returnself.namedefinteract_with(self,obstacle):print('{}theWizardbattlesagainst{}and{}!'.format(self,obstacle,obstacle.action()))
classOrk:def__str__(self):return'anevilork'
defaction(self):return'killsit'
classWizardWorld:def__init__(self,name):print(self)self.player_name=name
def__str__(self):return'\n\n\t------WizardWorld-------'
defmake_character(self):returnWizard(self.player_name)
defmake_obstacle(self):returnOrk()
-
classGameEnvironment:def__init__(self,factory):self.hero=factory.make_character()self.obstacle=factory.make_obstacle()
defplay(self):self.hero.interact_with(self.obstacle)
defvalidate_age(name):try:age=input('Welcome{}.Howoldareyou?'.format(name))age=int(age)exceptValueErroraserr:print("Age{}isinvalid,pleasetryagain...".format(age))return(False,age)return(True,age)
defmain():name=input("Hello.What'syourname?")valid_input=Falsewhilenotvalid_input:valid_input,age=validate_age(name)game=FrogWorldifage<18elseWizardWorldenvironment=GameEnvironment(game(name))environment.play()
if__name__=='__main__':main()
Asampleoutputofthisprogramisasfollows:
>>>python3abstract_factory.pyHello.What'syourname?NickWelcomeNick.Howoldareyou?17------FrogWorld-------NicktheFrogencountersabugandeatsit!
Tryextendingthegametomakeitmorecomplete.Youcangoasfarasyouwant:manyobstacles,manyenemies,andwhateverelseyoulike.
-
SummaryInthischapter,wehaveseenhowtousetheFactoryMethodandtheAbstractFactorydesignpatterns.Bothpatternsareusedwhenwewantto(a)trackanobjectcreation,(b)decoupleanobjectcreationfromanobjectusage,oreven(c)improvetheperformanceandresourceusageofanapplication.Case(c)wasnotdemonstratedinthechapter.Youmightconsideritasagoodexercise.
TheFactoryMethoddesignpatternisimplementedasasinglefunctionthatdoesn'tbelongtoanyclass,andisresponsibleforthecreationofasinglekindofobject(ashape,aconnectionpoint,andsoon).WesawhowtheFactoryMethodrelatestotoyconstruction,mentionedhowitisusedbyDjangoforcreatingdifferentformfields,anddiscussedotherpossibleusecasesforit.Asanexample,weimplementedaFactoryMethodthatprovidesaccesstotheXMLandJSONfiles.
TheAbstractFactorydesignpatternisimplementedasanumberofFactoryMethodsthatbelongtoasingleclassandareusedtocreateafamilyofrelatedobjects(thepartsofacar,theenvironmentofagame,andsoforth).WementionedhowtheAbstractFactoryisrelatedwithcarmanufacturing,howthedjango_factoryDjangopackagemakesuseofittocreatecleantests,andcoveredtheusecasesofit.TheimplementationoftheAbstractFactoryisamini-gamethatshowshowwecanusemanyrelatedfactoriesinasingleclass.
Inthenextchapter,wewilltalkabouttheBuilderpattern,whichisanothercreationalpatternthatcanbeusedforfine-controllingthecreationofcomplexobjects.
-
Chapter2.TheBuilderPatternImaginethatwewanttocreateanobjectthatiscomposedofmultiplepartsandthecompositionneedstobedonestepbystep.Theobjectisnotcompleteunlessallitspartsarefullycreated.That'swheretheBuilderdesignpatterncanhelpus.TheBuilderpatternseparatestheconstructionofacomplexobjectfromitsrepresentation.Bykeepingtheconstructionseparatefromtherepresentation,thesameconstructioncanbeusedtocreateseveraldifferentrepresentations[GOF95,page110],[j.mp/builderpat].
ApracticalexamplecanhelpusunderstandwhatthepurposeoftheBuilderpatternis.SupposethatwewanttocreateanHTMLpagegenerator,thebasicstructure(constructionpart)ofanHTMLpageisalwaysthesame:itbeginswithandfinisheswith;insidetheHTMLsectionaretheandelements,insidetheheadsectionaretheandelements,andsoforth.Buttherepresentationofthepagecandiffer.Eachpagehasitsowntitle,itsownheadings,anddifferentcontents.Moreover,thepageisusuallybuiltinsteps:onefunctionaddsthetitle,anotheraddsthemainheading,anotherthefooter,andsoon.Onlyafterthewholestructureofapageiscompletecanitbeshowntotheclientusingafinalrenderfunction.WecantakeitevenfurtherandextendtheHTMLgeneratorsothatitcangeneratetotallydifferentHTMLpages.Onepagemightcontaintables,anotherpagemightcontainimagegalleries,yetanotherpagecontainsthecontactform,andsoon.
TheHTMLpagegenerationproblemcanbesolvedusingtheBuilderpattern.Inthispattern,therearetwomainparticipants:thebuilderandthedirector.Thebuilderisresponsibleforcreatingthevariouspartsofthecomplexobject.IntheHTMLexample,thesepartsarethetitle,heading,body,andthefooterofthepage.Thedirectorcontrolsthebuildingprocessusingabuilderinstance.TheHTMLexamplemeansforcallingthebuilder'sfunctionsforsettingthetitle,theheading,andsoon.UsingadifferentbuilderinstanceallowsustocreateadifferentHTMLpagewithouttouchinganycodeofthedirector.
Areal-lifeexampleTheBuilderdesignpatternisusedinfast-foodrestaurants.Thesameprocedureisalwaysusedtoprepareaburgerandthepackaging(boxandpaperbag),eveniftherearemanydifferentkindsofburgers(classic,cheeseburger,andmore)anddifferentpackages(small-sizedbox,medium-sizedbox,andsoforth).Thedifferencebetweenaclassicburgerandacheeseburgerisintherepresentation,andnotintheconstructionprocedure.Thedirectoristhecashierwhogivesinstructionsaboutwhatneedstobepreparedtothecrew,andthebuilderisthepersonfromthecrewthattakescareofthespecificorder.Thefollowingfigureprovidedbywww.sourcemaking.comshowsaUnifiedModelingLanguage(UML)sequencediagramofthecommunicationthattakesplacebetweenthecustomer(client),thecashier(director),andthecrew(builder)whenakid'smenuisordered[j.mp/builderpat].
http://j.mp/builderpathttp://www.sourcemaking.comhttp://j.mp/builderpat
-
AsoftwareexampleTheHTMLexamplethatwasmentionedatthebeginningofthechapterisactuallyusedbydjango-widgy,athird-partytreeeditorforDjangothatcanbeusedasaContentManagementSystem(CMS).Thedjango-widgyeditorcontainsapagebuilderthatcanbeusedforcreatingHTMLpageswithdifferentlayouts[j.mp/widgypb].
Thedjango-query-builderlibraryisanotherthird-partyDjangolibrarythatreliesontheBuilderpattern.Thedjango-query-builderlibrarycanbeusedforbuildingSQLqueriesdynamically.Usingthis,wecancontrolallaspectsofaqueryandcreateadifferentrangeofqueries,fromsimpletoverycomplex[j.mp/djangowidgy].
http://j.mp/widgypbhttp://j.mp/djangowidgy
-
UsecasesWeusetheBuilderpatternwhenweknowthatanobjectmustbecreatedinmultiplesteps,anddifferentrepresentationsofthesameconstructionarerequired.Theserequirementsexistinmanyapplicationssuchaspagegenerators(liketheHTMLpagegeneratormentionedinthischapter),documentconverters[GOF95,page110],andUserInterface(UI)formcreators[j.mp/pipbuild].
SomeresourcesmentionthattheBuilderpatterncanalsobeusedasasolutiontothetelescopicconstructorproblem[j.mp/wikibuilder].Thetelescopicconstructorproblemoccurswhenweareforcedtocreateanewconstructorforsupportingdifferentwaysofcreatinganobject.Theproblemisthatweendupwithmanyconstructorsandlongparameterlists,whicharehardtomanage.Anexampleofthetelescopicconstructorislistedatthestackoverflowwebsite[j.mp/sobuilder].Fortunately,thisproblemdoesnotexistinPython,becauseitcanbesolvedinatleasttwoways:
Withnamedparameters[j.mp/sobuipython]Withargumentlistunpacking[j.mp/arglistpy]
Atthispoint,thedistinctionbetweentheBuilderpatternandtheFactorypatternmightnotbeveryclear.ThemaindifferenceisthataFactorypatterncreatesanobjectinasinglestep,whereasaBuilderpatterncreatesanobjectinmultiplesteps,andalmostalwaysthroughtheuseofadirector.SometargetedimplementationsoftheBuilderpatternlikeJava'sStringBuilderbypasstheuseofadirector,butthat'stheexceptiontotherule.
AnotherdifferenceisthatwhileaFactorypatternreturnsacreatedobjectimmediately,intheBuilderpatterntheclientcodeexplicitlyasksthedirectortoreturnthefinalobjectwhenitneedsit[GOF95,page113],[j.mp/builderpat].
ThenewcomputeranalogymighthelptodistinguishbetweenaBuilderpatternandaFactorypattern.Assumethatyouwanttobuyanewcomputer.Ifyoudecidetobuyaspecificpreconfiguredcomputermodel,forexample,thelatestApple1.4GHzMacmini,youusetheFactorypattern.Allthehardwarespecificationsarealreadypredefinedbythemanufacturer,whoknowswhattodowithoutconsultingyou.Themanufacturertypicallyreceivesjustasingleinstruction.Code-wise,thiswouldlooklikethefollowing(apple-factory.py):
MINI14='1.4GHzMacmini'
classAppleFactory:classMacMini14:def__init__(self):self.memory=4#ingigabytesself.hdd=500#ingigabytesself.gpu='IntelHDGraphics5000'
def__str__(self):info=('Model:{}'.format(MINI14),'Memory:{}GB'.format(self.memory),'HardDisk:{}GB'.format(self.hdd),
http://j.mp/pipbuildhttp://j.mp/wikibuilderhttp://j.mp/sobuilderhttp://j.mp/sobuipythonhttp://j.mp/arglistpyhttp://j.mp/builderpat
-
'GraphicsCard:{}'.format(self.gpu))return'\n'.join(info)
defbuild_computer(self,model):if(model==MINI14):returnself.MacMini14()else:print("Idon'tknowhowtobuild{}".format(model))
if__name__=='__main__':afac=AppleFactory()mac_mini=afac.build_computer(MINI14)print(mac_mini)
NoteNoticethenestedMacMini14class.Thisisaneatwayofforbiddingthedirectinstantiationofaclass.
AnotheroptionisbuyingacustomPC.Inthiscase,youusetheBuilderpattern.Youarethedirectorthatgivesorderstothemanufacturer(builder)aboutyouridealcomputerspecifications.Code-wise,thislookslikethefollowing(computer-builder.py):
classComputer:def__init__(self,serial_number):self.serial=serial_numberself.memory=None#ingigabytesself.hdd=None#ingigabytesself.gpu=None
def__str__(self):info=('Memory:{}GB'.format(self.memory),'HardDisk:{}GB'.format(self.hdd),'GraphicsCard:{}'.format(self.gpu))return'\n'.join(info)
classComputerBuilder:def__init__(self):self.computer=Computer('AG23385193')
defconfigure_memory(self,amount):self.computer.memory=amount
defconfigure_hdd(self,amount):self.computer.hdd=amount
defconfigure_gpu(self,gpu_model):self.computer.gpu=gpu_model
classHardwareEngineer:def__init__(self):self.builder=None
defconstruct_computer(self,memory,hdd,gpu):self.builder=ComputerBuilder()
-
[stepforstepin(self.builder.configure_memory(memory),self.builder.configure_hdd(hdd),self.builder.configure_gpu(gpu))]
@propertydefcomputer(self):returnself.builder.computer
defmain():engineer=HardwareEngineer()engineer.construct_computer(hdd=500,memory=8,gpu='GeForceGTX650Ti')computer=engineer.computerprint(computer)
if__name__=='__main__':main()
ThebasicchangesaretheintroductionofabuilderComputerBuilder,adirectorHardwareEngineer,andthestep-by-stepconstructionofacomputer,whichnowsupportsdifferentconfigurations(noticethatmemory,hdd,andgpuareparametersandnotpreconfigured).Whatdoweneedtodoifwewanttosupporttheconstructionoftablets?Implementthisasanexercise.
Youmightalsowanttochangethecomputerserial_numberintosomethingthatisdifferentforeachcomputer,becauseasitisnowitmeansthatallcomputerswillhavethesameserialnumber(whichisimpractical).
-
ImplementationLet'sseehowwecanusetheBuilderdesignpatterntomakeapizzaorderingapplication.Thepizzaexampleisparticularlyinterestingbecauseapizzaispreparedinstepsthatshouldfollowaspecificorder.Toaddthesauce,youfirstneedtopreparethedough.Toaddthetopping,youfirstneedtoaddthesauce.Andyoucan'tstartbakingthepizzaunlessboththesauceandthetoppingareplacedonthedough.Moreover,eachpizzausuallyrequiresadifferentbakingtime,dependingonthethicknessofitsdoughandthetoppingused.
WestartwithimportingtherequiredmodulesanddeclaringafewEnumparameters[j.mp/pytenum]plusaconstantthatareusedmanytimesintheapplication.TheSTEP_DELAYconstantisusedtoaddatimedelaybetweenthedifferentstepsofpreparingapizza(preparethedough,addthesauce,andsoon)asfollows:
fromenumimportEnum
PizzaProgress=Enum('PizzaProgress','queuedpreparationbakingready')PizzaDough=Enum('PizzaDough','thinthick')PizzaSauce=Enum('PizzaSauce','tomatocreme_fraiche')PizzaTopping=Enum('PizzaTopping','mozzarelladouble_mozzarellabaconhammushroomsred_onionoregano')STEP_DELAY=3#insecondsforthesakeoftheexample
Ourendproductisapizza,whichisdescribedbythePizzaclass.WhenusingtheBuilderpattern,theendproductdoesnothavemanyresponsibilities,sinceitisnotsupposedtobeinstantiateddirectly.Abuildercreatesaninstanceoftheendproductandmakessurethatitisproperlyprepared.That'swhythePizzaclassissominimal.Itbasicallyinitializesalldatatosanedefaultvalues.Anexceptionistheprepare_dough()method.Theprepare_dough()methodisdefinedinthePizzaclassinsteadofabuilderfortworeasons:
ToclarifythefactthattheendproductistypicallyminimaldoesnotmeanthatyoushouldneverassignitanyresponsibilitiesTopromotecodereusethroughcomposition[GOF95,page32]
classPizza:def__init__(self,name):self.name=nameself.dough=Noneself.sauce=Noneself.topping=[]
def__str__(self):returnself.name
defprepare_dough(self,dough):self.dough=doughprint('preparingthe{}doughofyour{}...'.format(self.dough.name,self))time.sleep(STEP_DELAY)
http://j.mp/pytenum
-
print('donewiththe{}dough'.format(self.dough.name))
Therearetwobuilders:oneforcreatingamargaritapizza(MargaritaBuilder)andanotherforcreatingacreamybaconpizza(CreamyBaconBuilder).EachbuildercreatesaPizzainstanceandcontainsmethodsthatfollowthepizza-makingprocedure:prepare_dough(),add_sauce(),add_topping(),andbake().Tobeprecise,prepare_dough()isjustawrappertotheprepare_dough()methodofthePizzaclass.Noticehoweachbuildertakescareofallthepizza-specificdetails.Forexample,thetoppingofthemargaritapizzaisdoublemozzarellaandoregano,whilethetoppingofthecreamybaconpizzaismozzarella,bacon,ham,mushrooms,redonion,andoreganoasfollows:
classMargaritaBuilder:def__init__(self):self.pizza=Pizza('margarita')self.progress=PizzaProgress.queuedself.baking_time=5#insecondsforthesakeoftheexample
defprepare_dough(self):self.progress=PizzaProgress.preparationself.pizza.prepare_dough(PizzaDough.thin)
defadd_sauce(self):print('addingthetomatosaucetoyourmargarita...')self.pizza.sauce=PizzaSauce.tomatotime.sleep(STEP_DELAY)print('donewiththetomatosauce')
defadd_topping(self):print('addingthetopping(doublemozzarella,oregano)toyourmargarita')self.pizza.topping.append([iforiin(PizzaTopping.double_mozzarella,PizzaTopping.oregano)])time.sleep(STEP_DELAY)print('donewiththetopping(doublemozzarella,oregano)')
defbake(self):self.progress=PizzaProgress.bakingprint('bakingyourmargaritafor{}seconds'.format(self.baking_time))time.sleep(self.baking_time)self.progress=PizzaProgress.readyprint('yourmargaritaisready')
classCreamyBaconBuilder:def__init__(self):self.pizza=Pizza('creamybacon')self.progress=PizzaProgress.queuedself.baking_time=7#insecondsforthesakeoftheexample
defprepare_dough(self):self.progress=PizzaProgress.preparationself.pizza.prepare_dough(PizzaDough.thick)
defadd_sauce(self):print('addingthecrèmefraîchesaucetoyourcreamybacon')
-
self.pizza.sauce=PizzaSauce.creme_fraichetime.sleep(STEP_DELAY)print('donewiththecrèmefraîchesauce')
defadd_topping(self):print('addingthetopping(mozzarella,bacon,ham,mushrooms,redonion,oregano)toyourcreamybacon')self.pizza.topping.append([tfortin(PizzaTopping.mozzarella,PizzaTopping.bacon,PizzaTopping.ham,PizzaTopping.mushrooms,PizzaTopping.red_onion,PizzaTopping.oregano)])time.sleep(STEP_DELAY)print('donewiththetopping(mozzarella,bacon,ham,mushrooms,redonion,oregano)')
defbake(self):self.progress=PizzaProgress.bakingprint('bakingyourcreamybaconfor{}seconds'.format(self.baking_time))time.sleep(self.baking_time)self.progress=PizzaProgress.readyprint('yourcreamybaconisready')
Thedirectorinthisexampleisthewaiter.ThecoreoftheWaiterclassistheconstruct_pizza()method,whichacceptsabuilderasaparameterandexecutesallthepizzapreparationstepsintherightorder.Choosingtheappropriatebuilder,whichcanevenbedoneinruntime,givesustheabilitytocreatedifferentpizzastyleswithoutmodifyinganycodeofthedirector(Waiter).TheWaiterclassalsocontainsthepizza()method,whichreturnstheendproduct(preparedpizza)asavariabletothecallerasfollows:
classWaiter:def__init__(self):self.builder=None
defconstruct_pizza(self,builder):self.builder=builder[step()forstepin(builder.prepare_dough,builder.add_sauce,builder.add_topping,builder.bake)]
@propertydefpizza(self):returnself.builder.pizza
Thevalidate_style()functionissimilartothevalidate_age()functionasdescribedinChapter1,TheFactoryPattern.Itisusedtomakesurethattheusergivesvalidinput,whichinthiscaseisacharacterthatismappedtoapizzabuilder.ThemcharacterusestheMargaritaBuilderclassandtheccharacterusestheCreamyBaconBuilderclass.Thesemappingsareinthebuilderparameter.Atupleisreturned,withthefirstelementsettoTrueiftheinputisvalid,orFalseifitisinvalidasfollows:
defvalidate_style(builders):try:
-
pizza_style=input('Whatpizzawouldyoulike,[m]argaritaor[c]reamybacon?')builder=builders[pizza_style]()valid_input=TrueexceptKeyErroraserr:print('Sorry,onlymargarita(keym)andcreamybacon(keyc)areavailable')return(False,None)return(True,builder)
Thelastpartisthemain()function.Themain()functioncontainsacodeforinstantiatingapizzabuilder.ThepizzabuilderisthenusedbytheWaiterdirectorforpreparingthepizza.Thecreatedpizzacanbedeliveredtotheclientatanylaterpoint:
defmain():builders=dict(m=MargaritaBuilder,c=CreamyBaconBuilder)valid_input=Falsewhilenotvalid_input:valid_input,builder=validate_style(builders)print()waiter=Waiter()waiter.construct_pizza(builder)pizza=waiter.pizzaprint()print('Enjoyyour{}!'.format(pizza))
Toputallthesethingstogether,here'sthecompletecodeofthisexample(builder.py):
fromenumimportEnumimporttime
PizzaProgress=Enum('PizzaProgress','queuedpreparationbakingready')PizzaDough=Enum('PizzaDough','thinthick')PizzaSauce=Enum('PizzaSauce','tomatocreme_fraiche')PizzaTopping=Enum('PizzaTopping','mozzarelladouble_mozzarellabaconhammushroomsred_onionoregano')STEP_DELAY=3#insecondsforthesakeoftheexample
classPizza:def__init__(self,name):self.name=nameself.dough=Noneself.sauce=Noneself.topping=[]
def__str__(self):returnself.name
defprepare_dough(self,dough):self.dough=doughprint('preparingthe{}doughofyour{}...'.format(self.dough.name,self))time.sleep(STEP_DELAY)print('donewiththe{}dough'.format(self.dough.name))
-
classMargaritaBuilder:def__init__(self):self.pizza=Pizza('margarita')self.progress=PizzaProgress.queuedself.baking_time=5#insecondsforthesakeoftheexample
defprepare_dough(self):self.progress=PizzaProgress.preparationself.pizza.prepare_dough(PizzaDough.thin)
defadd_sauce(self):print('addingthetomatosaucetoyourmargarita...')self.pizza.sauce=PizzaSauce.tomatotime.sleep(STEP_DELAY)print('donewiththetomatosauce')
defadd_topping(self):print('addingthetopping(doublemozzarella,oregano)toyourmargarita')self.pizza.topping.append([iforiin(PizzaTopping.double_mozzarella,PizzaTopping.oregano)])time.sleep(STEP_DELAY)print('donewiththetopping(doublemozzarrella,oregano)')
defbake(self):self.progress=PizzaProgress.bakingprint('bakingyourmargaritafor{}seconds'.format(self.baking_time))time.sleep(self.baking_time)self.progress=PizzaProgress.readyprint('yourmargaritaisready')
classCreamyBaconBuilder:def__init__(self):self.pizza=Pizza('creamybacon')self.progress=PizzaProgress.queuedself.baking_time=7#insecondsforthesakeoftheexample
defprepare_dough(self):self.progress=PizzaProgress.preparationself.pizza.prepare_dough(PizzaDough.thick)
defadd_sauce(self):print('addingthecrèmefraîchesaucetoyourcreamybacon')self.pizza.sauce=PizzaSauce.creme_fraichetime.sleep(STEP_DELAY)print('donewiththecrèmefraîchesauce')
defadd_topping(self):print('addingthetopping(mozzarella,bacon,ham,mushrooms,redonion,oregano)toyourcreamybacon')self.pizza.topping.append([tfortin(PizzaTopping.mozzarella,PizzaTopping.bacon,PizzaTopping.ham,PizzaTopping.mushrooms,PizzaTopping.red_onion,PizzaTopping.oregano)])time.sleep(STEP_DELAY)print('donewiththetopping(mozzarella,bacon,ham,mushrooms,redonion,oregano)')
-
defbake(self):self.progress=PizzaProgress.bakingprint('bakingyourcreamybaconfor{}seconds'.format(self.baking_time))time.sleep(self.baking_time)self.progress=PizzaProgress.readyprint('yourcreamybaconisready')
classWaiter:def__init__(self):self.builder=None
defconstruct_pizza(self,builder):self.builder=builder[step()forstepin(builder.prepare_dough,builder.add_sauce,builder.add_topping,builder.bake)]
@propertydefpizza(self):returnself.builder.pizza
defvalidate_style(builders):try:pizza_style=input('Whatpizzawouldyoulike,[m]argaritaor[c]reamybacon?')builder=builders[pizza_style]()valid_input=TrueexceptKeyErroraserr:print('Sorry,onlymargarita(keym)andcreamybacon(keyc)areavailable')return(False,None)return(True,builder)
defmain():builders=dict(m=MargaritaBuilder,c=CreamyBaconBuilder)valid_input=Falsewhilenotvalid_input:valid_input,builder=validate_style(builders)print()waiter=Waiter()waiter.construct_pizza(builder)pizza=waiter.pizzaprint()print('Enjoyyour{}!'.format(pizza))
if__name__=='__main__':main()
Asampleoutputofthisexampleisasfollows:
>>>python3builder.pyWhatpizzawouldyoulike,[m]argaritaor[c]reamybacon?rSorry,onlymargarita(keym)andcreamybacon(keyc)areavailableWhatpizzawouldyoulike,[m]argaritaor[c]reamybacon?m
preparingthethindoughofyourmargarita...donewiththethindoughaddingthetomatosaucetoyourmargarita...
-
donewiththetomatosauceaddingthetopping(doublemozzarella,oregano)toyourmargaritadonewiththetopping(doublemozzarella,oregano)bakingyourmargaritafor5secondsyourmargaritaisready
Enjoyyourmargarita!
Supportingonlytwopizzatypesisashame.ImplementaHawaiianpizzabuilder.Considerusinginheritanceafterthinkingabouttheadvantagesanddisadvantages.ChecktheingredientsofatypicalHawaiianpizzaanddecidewhichclassyouneedtoextend:MargaritaBuilderorCreamyBaconBuilder?Perhapsboth[j.mp/pymulti]?
Inthebook,EffectiveJava(2ndedition),JoshuaBlochdescribesaninterestingvariationoftheBuilderpatternwherecallstobuildermethodsarechained.Thisisaccomplishedbydefiningthebuilderitselfasaninnerclassandreturningitselffromeachofthesetter-likemethodsonit.Thebuild()methodreturnsthefinalobject.ThispatterniscalledtheFluentBuilder.Here'saPythonimplementation,whichwaskindlyprovidedbyareviewerofthebook:
classPizza:def__init__(self,builder):self.garlic=builder.garlicself.extra_cheese=builder.extra_cheese
def__str__(self):garlic='yes'ifself.garlicelse'no'cheese='yes'ifself.extra_cheeseelse'no'info=('Garlic:{}'.format(garlic),'Extracheese:{}'.format(cheese))return'\n'.join(info)
classPizzaBuilder:def__init__(self):self.extra_cheese=Falseself.garlic=False
defadd_garlic(self):self.garlic=Truereturnself
defadd_extra_cheese(self):self.extra_cheese=Truereturnself
defbuild(self):returnPizza(self)
if__name__=='__main__':pizza=Pizza.PizzaBuilder().add_garlic().add_extra_cheese().build()print(pizza)
AdaptthepizzaexampletomakeuseoftheFluentBuilderpattern.Whichversionofthetwodoyouprefer?Whataretheprosandconsofeachversion?
http://j.mp/pymulti
-
SummaryInthischapter,wehaveseenhowtousetheBuilderdesignpattern.WeusetheBuilderpatternforcreatinganobjectinsituationswhereusingtheFactorypattern(eitheraFactoryMethodoranAbstractFactory)isnotagoodoption.ABuilderpatternisusuallyabettercandidatethanaFactorypatternwhen:
Wewanttocreateacomplexobject(anobjectcomposedofmanypartsandcreatedindifferentstepsthatmightneedtofollowaspecificorder).Differentrepresentationsofanobjectarerequired,andwewanttokeeptheconstructionofanobjectdecoupledfromitsrepresentationWewanttocreateanobjectatonepointintimebutaccessitatalaterpoint
WesawhowtheBuilderpatternisusedinfast-foodrestaurantsforpreparingmeals,andhowtwothird-partyDjangopackages,django-widgyanddjango-query-builder,useitforgeneratingHTMLpagesanddynamicSQLqueries,respectively.WefocusedonthedifferencesbetweenaBuilderpatternandaFactorypattern,andgaveapreconfigured(Factory)versuscustomer(Builder)computerorderanalogytoclarifythem.
Intheimplementationpart,wehaveseenhowtocreateapizzaorderingapplication,whichhaspreparationdependencies.Therearemanyrecommendedinterestingexercisesinthischapter,includingimplementingaFluentBuilder.
Inthenextchapter,youwilllearnaboutthelastcreationaldesignpatterncoveredinthisbook:thePrototypepattern,whichisusedforcloninganobject.
-
Chapter3.ThePrototypePatternSometimes,weneedtocreateanexactcopyofanobject.Forinstance,assumethatyouwanttocreateanapplicationforstoring,sharing,andediting(suchasmodifying,addingnotes,andremoving)culinaryrecipes.UserBobfindsacakerecipeandaftermakingafewmodificationshethinksthathiscakeisdelicious,andhewantstoshareitwithhisfriend,Alice.Butwhatdoessharingarecipemean?IfBobwantstodosomefurtherexperimentationwithhisrecipeaftersharingitwithAlice,willthenewchangesalsobevisibleinAlice'srecipe?CanBobkeeptwocopiesofthecakerecipe?Hisdeliciouscakerecipeshouldremainunaffectedbyanychangesmadeintheexperimentalcakerecipe.
Suchproblemscanbesolvedbyallowingtheuserstohavemorethanoneindependentcopyofthesamerecipe.Eachcopyiscalledaclone,becauseitisanexactcopyoftheoriginalobjectataspecificpointintime.Thetimeaspectisimportant,sinceitaffectswhattheclonecontains.Forexample,ifBobsharesthecakerecipewithAlicebeforemakinghisownimprovementstoachieveperfection,AlicewillneverbeabletobakeherownversionofthedeliciouscakethatBobcreated!ShewillonlybeabletobaketheoriginalcakerecipefoundbyBob.
Notethedifferencebetweenacopyandareference.Ifwehavetworeferencestothesamecakerecipe,whateverchangesBobmakestotherecipewillbevisibletoAlice'sversionoftherecipe,andviceversa.WhatwewantisbothBobandAlicetohavetheirowncopy,sothattheycanmakeindependentchangeswithoutaffectingeachother'srecipe.Bobactuallyneedstwocopiesofthecakerecipe:thedeliciousversionandtheexperimentalversion.
Thedifferencebetweenareferenceandacopyisshowninthefollowingfigure:
Ontheleftpart,wecanseetworeferences.BothAliceandBobrefertothesamerecipe,whichessentiallymeansthattheyshareitandallmodificationsarevisiblebyboth.Ontherightpart,wecanseetwodifferentcopiesofthesamerecipe.Inthiscase,independentmodificationsareallowedandthechangesofAlicedonotaffectthechangesofBob,andviceversa.
ThePrototypedesignpatternhelpsuswithcreatingobjectclones.Initssimplestversion,thePrototypepatternisjustaclone()functionthatacceptsanobjectasaninputparameterand
-
returnsacloneofit.InPython,thiscanbedoneusingthecopy.deepcopy()function.Let'sseeanexample.Inthefollowingcode(fileclone.py),therearetwoclasses,AandB.AistheparentclassandBisthederivedclass.Inthemainpart,wecreateaninstanceofclassBb,andusedeepcopy()tocreateacloneofbnamedc.Theresultisthatallthemembersofthehierarchy(atthepointoftimethecloninghappens)arecopiedintheclonec.Asaninterestingexercise,youcantryusingdeepcopy()withcompositioninsteadofinheritancewhichisshowninthefollowingcode:
importcopy
classA:def__init__(self):self.x=18self.msg='Hello'
classB(A):def__init__(self):A.__init__(self)self.y=34
def__str__(self):return'{},{},{}'.format(self.x,self.msg,self.y)
if__name__=='__main__':b=B()c=copy.deepcopy(b)print([str(i)foriin(b,c)])print([iforiin(b,c)])
Whenexecutingclone.pyonmycomputer,Igetthefollowing:
>>>python3clone.py['18,Hello,34','18,Hello,34'][,]
Althoughyouroutputofthesecondlinewillmostlikelynotbethesameasmine,what'simportantistonoticethatthetwoobjectsresideintwodifferentmemoryaddresses(the0x...part).Thismeansthatthetwoobjectsaretwoindependentcopies.
IntheImplementationsection,laterinthischapter,wewillseehowtousecopy.deepcopy()withsomeextraboilerplatecodewrappedinaclass,forkeepingaregistryoftheobjectsthatarecloned.
Areal-lifeexampleThePrototypedesignpatternisallaboutcloninganobject.Mitosis,theprocessinacelldivisionbywhichthenucleusdividesresultingintwonewnuclei,eachofwhichhasexactlythesamechromosomeandDNAcontentastheoriginalcell,isanexampleofbiologicalcloning[j.mp/mmitosis].
http://j.mp/mmitosis
-
Thefollowingfigure,providedbywww.sourcemaking.com,showsanexampleofthemitoticdivisionofacell[j.mp/pprotpat]:
Anotherpopularexampleof(artificial)cloningisDolly,thesheep[j.mp/wikidolly].
http://www.sourcemaking.comhttp://j.mp/pprotpathttp://j.mp/wikidolly
-
AsoftwareexampleTherearemanyPythonapplicationsthatmakeuseofthePrototypepattern[j.mp/pythonprot],butitisalmostneverreferredtoasPrototypesincecloningobjectsisabuilt-infeatureofthelanguage.
OneapplicationthatusesPrototypeistheVisualizationToolkit(VTK)[j.mp/pyvto].VTKisanopensourcecross-platformsystemfor3Dcomputergraphics,imageprocessing,andvisualization.VTKusesPrototypeforcreatingclonesofgeometricalelementssuchaspoints,lines,hexahedrons,andsoforth[j.mp/vtkcell].
AnotherprojectthatusesPrototypeismusic21.Accordingtotheproject'spage,"music21isasetoftoolsforhelpingscholarsandotheractivelistenersanswerquestionsaboutmusicquicklyandsimply"[j.mp/pmusic21].Themusic21toolkitusesPrototypeforcopyingmusicalnotesandscores[j.mp/py21code].
http://j.mp/pythonprothttp://j.mp/pyvtohttp://j.mp/vtkcellhttp://j.mp/pmusic21http://j.mp/py21code
-
UsecasesThePrototypepatternisusefulwhenwehaveanexistingobjectandwewanttocreateanexactcopyofit.Acopyofanobjectisusuallyrequiredwhenweknowthatpartsoftheobjectwillbemodifiedbutwewanttokeeptheoriginalobjectuntouched.Insuchcases,itdoesn'tmakesensetorecreatetheoriginalobjectfromscratch[j.mp/protpat].
AnothercasewherePrototypecomesinhandyiswhenwewanttoduplicateacomplexobject.Byduplicatingacomplexobject,wecanthinkofanobjectthatispopulatedfromadatabaseandhasreferencestootherobjectsthatarealsopopulatedfromadatabase.Itisalotofefforttocreateanobjectclonebyqueryingthedatabase(s)multipletimesagain.UsingPrototypeforsuchcasesismoreconvenient.
Sofar,wehavecoveredonlythereferenceversuscopyissue,butacopycanbefurtherdividedintoadeepcopyversusashallowcopy.Adeepcopyiswhatwehaveseensofar:alldataoftheoriginalobjectaresimplycopiedintheclone,withoutmakinganyexceptions.Ashallowcopyreliesonreferences.Wecanintroducedatasharing,andtechniqueslikecopy-on-writetoimprovetheperformance(suchasclonecreationtime)andthememoryusage.Usingshallowcopiesmightbeworthwhileiftheavailableresourcesarelimited(suchasembeddedsystems)orperformanceiscritical(suchashigh-performancecomputing).
InPython,wecandoshallowcopiesusingthecopy.copy()function.QuotingtheofficialPythondocumentation,thedifferencesbetweenashallowcopy(copy.copy())andadeepcopy(copy.deepcopy())inPythonare[j.mp/py3copy]asfollows:
"Ashallowcopyconstructsanewcompoundobjectandthen(totheextentpossible)insertsreferencesintoittotheobjectsfoundintheoriginal.Adeepcopyconstructsanewcompoundobjectandthen,recursively,insertscopiesintoitoftheobjectsfoundintheoriginal."
Canyouthinkofanyexampleswhereusingshallowcopiesisbetterthanusingdeepcopies?
http://j.mp/protpathttp://j.mp/py3copy
-
ImplementationInprogramming,itisnotuncommonforabooktobeavailableinmultipleeditions.Forexample,theclassictextbookonCprogrammingTheCProgrammingLanguagebyKernighanandRitchieisavailableintwoeditions.Thefirsteditionwaspublishedin1978.Atthattime,Cwasnotstandardized.Thesecondeditionofthebookwaspublished10yearslaterandcoversthestandard(ANSI)versionofC.Whatarethedifferencesbetweenthetwoeditions?Tomentionafew,theprice,thelength(numberofpages),andthepublicationdate.Buttherearealsomanysimilarities:theauthors,thepublishers,andthetags/keywordsthatdescribethebookareexactlythesame.Thisindicatesthatcreatinganewbookfromscratchisnotalwaysthebestapproach.Ifweknowthattherearemanysimilaritiesbetweentwobookeditions,wecanusecloningandmodifyonlythedifferentpartsofthenewedition.
Let'sseehowwecanusethePrototypepatternforcreatinganapplicationthatshowsbookinformation.Webeginwiththerepresentationofabook.Apartfromtheusualinitialization,theBookclassdemonstratesaninterestingtechnique.Itshowshowwecanavoidthetelescopicconstructorproblem.Inthe__init__()method,onlythreeparametersarefixed:name,authors,andprice.Butclientscanpassmoreparametersintheformofkeywords(name=value)usingtherestvariable-lengthlist.Thelineself.__dict__.update(rest)addsthecontentsofresttotheinternaldictionaryoftheBookclasstomakethempartofit.
Butthere'sacatch.Sincewedon'tknowallthenamesoftheaddedparameters,weneedtoaccesstheinternaldictformakinguseofthemin__str__().Andsincethecontentsofadictionarydonotfollowanyspecificorder,weuseanOrderedDicttoforceanorder;otherwise,everytimetheprogramisexecuted,differentoutputswillbeshown.Ofcourse,youshouldnottakemywordsforgranted.Asanexercise,removetheusageofOrderedDictandsorted()andruntheexampletoseeifI'mright:
classBook:def__init__(self,name,authors,price,**rest):'''Examplesofrest:publisher,length,tags,publicationdate'''self.name=nameself.authors=authorsself.price=price#inUSdollarsself.__dict__.update(rest)
def__str__(self):mylist=[]ordered=OrderedDict(sorted(self.__dict__.items()))foriinordered.keys():mylist.append('{}:{}'.format(i,ordered[i]))ifi=='price':mylist.append('$')mylist.append('\n')return''.join(mylist)
ThePrototypeclassimplementsthePrototypedesignpattern.TheheartofthePrototypeclassistheclone()method,whichdoestheactualcloningusingthefamiliarcopy.deepcopy()
-
function.ButthePrototypeclassdoesabitmorethansupportingcloning.Itcontainstheregister()andunregister()methods,whichcanbeusedtokeeptrackoftheobjectsthatareclonedinadictionary.Notethatthisisjustaconvenience,andnotanecessity.
Moreover,theclone()methodusesthesametrickthat__str__()usesintheBookclass,butthistimeforadifferentreason.Usingthevariable-lengthlistattr,wecanpassonlythevariablesthatreallyneedtobemodifiedwhencloninganobjectasfollows:
classPrototype:def__init__(self):self.objects=dict()
defregister(self,identifier,obj):self.objects[identifier]=obj
defunregister(self,identifier):delself.objects[identifier]
defclone(self,identifier,**attr):found=self.objects.get(identifier)ifnotfound:raiseValueError('Incorrectobjectidentifier:{}'.format(identifier))obj=copy.deepcopy(found)obj.__dict__.update(attr)returnobj
Themain()functionshowsTheCProgrammingLanguagebookcloningexamplementionedatthebeginningofthissectioninpractice.Whencloningthefirsteditionofthebooktocreatethesecondedition,weonlyneedtopassthemodifiedvaluesoftheexistingparameters.Butwecanalsopassextraparameters.Inthiscase,editionisanewparameterthatwasnotneededinthefirstbookbutisusefulinformationfortheclone:
defmain():b1=Book('TheCProgrammingLanguage',('BrianW.Kernighan','DennisM.Ritchie'),price=118,publisher='PrenticeHall',length=228,publication_date='1978-02-22',tags=('C','programming','algorithms','datastructures'))
prototype=Prototype()cid='k&r-first'prototype.register(cid,b1)b2=prototype.clone(cid,name='TheCProgrammingLanguage(ANSI)',price=48.99,length=274,publication_date='1988-04-01',edition=2)
foriin(b1,b2):print(i)print("IDb1:{}!=IDb2:{}".format(id(b1),id(b2)))
Noticetheusageoftheid()functionwhichreturnsthememoryaddressofanobject.Whenwecloneanobjectusingadeepcopy,thememoryaddressesoftheclonemustbedifferentfromthememoryaddressesoftheoriginalobject.
-
Theprototype.pyfileisasfollows:
importcopyfromcollectionsimportOrderedDict
classBook:def__init__(self,name,authors,price,**rest):'''Examplesofrest:publisher,length,tags,publicationdate'''self.name=nameself.authors=authorsself.price=price#inUSdollarsself.__dict__.update(rest)
def__str__(self):mylist=[]ordered=OrderedDict(sorted(self.__dict__.items()))foriinordered.keys():mylist.append('{}:{}'.format(i,ordered[i]))ifi=='price':mylist.append('$')mylist.append('\n')return''.join(mylist)
classPrototype:def__init__(self):self.objects=dict()
defregister(self,identifier,obj):self.objects[identifier]=obj
defunregister(self,identifier):delself.objects[identifier]
defclone(self,identifier,**attr):found=self.objects.get(identifier)ifnotfound:raiseValueError('Incorrectobjectidentifier:{}'.format(identifier))obj=copy.deepcopy(found)obj.__dict__.update(attr)returnobj
defmain():b1=Book('TheCProgrammingLanguage',('BrianW.Kernighan','DennisM.Ritchie'),price=118,publisher='PrenticeHall',length=228,publication_date='1978-02-22',tags=('C','programming','algorithms','datastructures'))
prototype=Prototype()cid='k&r-first'prototype.register(cid,b1)b2=prototype.clone(cid,name='TheCProgrammingLanguage(ANSI)',price=48.99,length=274,publication_date='1988-04-01',edition=2)
foriin(b1,b2):print(i)print("IDb1:{}!=IDb2:{}".format(id(b1),id(b2)))
-
if__name__=='__main__':main()
Theoutputofid()dependsonthecurrentmemoryallocationofthecomputerandyoushouldexpectittodifferoneveryexecutionofthisprogram.Butnomatterwhattheactualaddressesare,theyshouldnotbethesameinanychance.
AsampleoutputwhenIexecutethisprogramonmymachineisasfollows:
>>>python3prototype.pyauthors:('BrianW.Kernighan','DennisM.Ritchie')length:228name:TheCProgrammingLanguageprice:118$publication_date:1978-02-22publisher:PrenticeHalltags:('C','programming','algorithms','datastructures')
authors:('BrianW.Kernighan','DennisM.Ritchie')edition:2length:274name:TheCProgrammingLanguage(ANSI)price:48.99$publication_date:1988-04-01publisher:PrenticeHalltags:('C','programming','algorithms','datastructures')
IDb1:140004970829304!=IDb2:140004970829472
Indeed,Prototypeworksasexpected.ThesecondeditionofTheCProgrammingLanguagebookreusesalltheinformationthatwassetinthefirstedition,andallthedifferencesthatwedefinedareonlyappliedtothesecondedition.Thefirsteditionremainsunaffected.Ourconfidencecanbeincreasedbylookingattheoutputoftheid()function:thetwoaddressesaredifferent.
Asanexercise,youcancomeupwithyourownexampleofPrototype.Afewideasareasfollows:
TherecipeexamplethatwasmentionedinthischapterThedatabase-populatedobjectthatwasmentionedinthischapterCopyinganimagesothatyoucanaddyourownmodificationswithouttouchingtheoriginal
-
SummaryInthischapter,wehaveseenhowtousethePrototypedesignpattern.Prototypeisusedforcreatingexactcopiesofobjects.Creatingacopyofanobjectcanactuallymeantwothings:
Relyingonreferences,whichhappenswhenashallowcopyiscreatedDuplicatingeverything,whichhappenswhenadeepcopyiscreated
Inthefirstcase,wewanttofocusonimprovingtheperformanceandthememoryusageofourapplicationbyintroducingdatasharingbetweenobjects.Butweneedtobecarefulaboutmodifyingdata,becauseallmodificationsarevisibletoallcopies.Shallowcopieswerenotintroducedinthischapter,butyoumightwanttoexperimentwiththem.
Inthesecondcase,wewanttobeabletomakemodificationstoonecopywithoutaffectingtherest.That'susefulforcaseslikethecake-recipeexamplethatwehaveseen.Here,nodatasharingisdoneandsoweneedtobecarefulabouttheresourceconsumptionandtheoverheadthatisintroducedbyourclones.
WeshowedasimpleexampleofadeepcopyingwhichinPythonisdoneusingthecopy.deepcopy()function.Wealsomentionedexamplesofcloningfoundinreallife,focusingonmitosis.
ManysoftwareprojectsusePrototype,butinPythonitisnotmentionedassuchbecauseitisabuilt-infeature.AmongthemaretheVTK,whichusesPrototypeforcreatingclonesofgeometricalelements,andmusic21,whichusesitforduplicatingmusicalscoresandnotes.
Finally,wediscussedtheusecasesofPrototypeandimplementedaprogramthatsupportscloningbookssothatallinformationthatdoesnotchangeinaneweditioncanbereused,butatthesametimemodifiedinformationcanbeupdatedandnewinformationcanbeadded.
Prototypeisthelastcreationaldesignpatterncoveredinthisbook.ThenextchapterbeginswithAdapter,astructuraldesignpatternthatcanbeusedtomaketwoincompatiblesoftwareinterfacescompatible.
-
Chapter4.TheAdapterPatternStructuraldesignpatternsdealwiththerelationshipsbetweentheentities(suchasclassesandobjects)ofasystem.Astructuraldesignpatternfocusesonprovidingasimplewayofcomposingobjectsforcreatingnewfunctionality[GOF95,page155],[j.mp/structpat].
Adapterisastructuraldesignpatternthathelpsusmaketwoincompatibleinterfacescompatible.First,let'sanswerwhatincompatibleinterfacesreallymean.Ifwehaveanoldcomponentandwewanttouseitinanewsystem,oranewcomponentthatwewanttouseinanoldsystem,thetwocanrarelycommunicatewithoutrequiringanycodechanges.Butchangingthecodeisnotalwayspossible,eitherbecausewedon'thaveaccesstoit(forexample,thecomponentisprovidedasanexternallibrary)orbecauseitisimpractical.Insuchcases,wecanwriteanextralayerthatmakesalltherequiredmodificationsforenablingthecommunicationbetweenthetwointerfaces.ThislayeriscalledtheAdapter.
E-commercesystemsareknownexamples.Assumethatweuseane-commercesystemthatcontainsacalculate_total(order)function.Thefunctioncalculatesthetotalamountofanorder,butonlyinDanishKroner(DKK).Itisreasonableforourcustomerstoaskustoaddsupportformorepopularcurrencies,suchasUnitedStatesDollars(USD)andEuros(EUR).IfweownthesourcecodeofthesystemwecanextenditbyaddingnewfunctionsfordoingtheconversionsfromDKKtoUSDandfromDKKtoEUR.Butwhatifwedon'thaveaccesstothesourcecodeoftheapplicationbecauseitisprovidedtousonlyasanexternallibrary?Inthiscase,wecanstillusethelibrary(forexample,callitsmethods),butwecannotmodify/extendit.Thesolutionistowriteawrapper(alsoknownasAdapter)thatconvertsthedatafromthegivenDKKformattotheexpectedUSDorEURformat.
TheAdapterpatternisnotusefulonlyfordataconversions.Ingeneral,ifyouwanttouseaninterfacethatexpectsfunction_a()butyouonlyhavefunction_b(),youcanuseanAdaptertoconvert(adapt)function_b()tofunction_a()[Eckel08,page207],[j.mp/adapterpat].Thisisnotonlytrueforfunctionsbutalsoforfunctionparameters.Anexampleisafunctionthatexpectstheparametersx,y,andzbutyouonlyhaveafunctionthatworkswiththeparametersxandyathand.WewillseehowtousetheAdapterpatternintheimplementationsection.
Areal-lifeexampleProbablyallofususetheAdapterpatterneveryday,butinhardwareinsteadofsoftware.Ifyouhaveasmartphoneoratablet,youneedtousesomething(forexample,thelightningconnectorofaniPhone)withaUSBadapterforconnectingittoyourcomputer.IfyouaretravelingfrommostEuropeancountriestotheUK,youneedtouseaplugadapterforchargingyourlaptop.ThesameistrueifyouaretravelingfromEuropetoUSA,ortheotherwayaround.Adaptersareeverywhere!
Thefollowingimage,courtesyofsourcemaking.com,showsseveralexamplesofhardwareadapters[j.mp/adapterpat]:
http://j.mp/structpathttp://j.mp/adapterpathttp://sourcemaking.comhttp://j.mp/adapterpat
-
AsoftwareexampleGrokisaPythonframeworkthatrunsontopofZope3andfocusesonagiledevelopment.TheGrokframeworkusesAdaptersformakingitpossibleforexistingobjectstoconformtospecificAPIswithouttheneedtomodifythem[j.mp/grokada].
ThePythonTraitspackagealsousestheAdapterpatternfortransforminganobjectthatdoesnotimplementofaspecificinterface(orsetofinterfaces)toanobjectthatdoes[j.mp/pytraitsad].
http://j.mp/grokadahttp://j.mp/pytraitsad
-
UsecasesTheAdapterpatternisusedformakingthingsworkaftertheyhavebeenimplemented[j.mp/adapterpat].Usuallyoneofthetwoincompatibleinterfacesiseitherforeignorold/legacy.Iftheinterfaceisforeign,itmeansthatwehavenoaccesstothesourcecode.Ifitisolditisusuallyimpracticaltorefactorit.Wecantakeitevenfurtherandarguethatalteringtheimplementationofalegacycomponenttomeetourneedsisnotonlyimpractical,butitalsoviolatestheopen/closeprinciple[j.mp/adaptsimp].Theopen/closeprincipleisoneofthefundamentalprinciplesofObject-Orienteddesign(theOofSOLID).Itstatesthatasoftwareentityshouldbeopenforextension,butclosedformodification.Thatbasicallymeansthatweshouldbeabletoextendthebehaviorofanentitywithoutmakingsourcecodemodifications.Adapterrespectstheopen/closedprinciple[j.mp/openclosedp].
Therefore,usinganAdapterformakingthingsworkaftertheyhavebeenimplementedisabetterapproachbecauseit:
DoesnotrequireaccesstothesourcecodeoftheforeigninterfaceDoesnotviolatetheopen/closedprinciple
http://j.mp/adapterpathttp://j.mp/adaptsimphttp://j.mp/openclosedp
-
ImplementationTherearemanywaysofimplementingtheAdapterdesignpatterninPython[Eckel08,page207].AllthetechniquesdemonstratedbyBruceEckeluseinheritance,butPythonprovidesanalternative,andinmyopinion,amoreidiomaticwayofimplementinganAdapter.Thealternativetechniqueshouldbefamiliartoyou,sinceitusestheinternaldictionaryofaclass,andwehaveseenhowtodothatinChapter3,ThePrototypePattern.
Let'sbeginwiththewhatwehavepart.OurapplicationhasaComputerclassthatshowsbasicinformationaboutacomputer.Alltheclassesofthisexample,includingtheComputerclassareveryprimitive,becausewewanttofocusontheAdapterpatternandnotonhowtomakeaclassascompleteaspossible.
classComputer:def__init__(self,name):self.name=name
def__str__(self):return'the{}computer'.format(self.name)
defexecute(self):return'executesaprogram'
Inthiscase,theexecute()methodisthemainactionthatthecomputercanperform.Thismethodiscalledbytheclientcode.
Nowwemovetothewhatwewantpart.Wedecidetoenrichourapplicationwithmorefunctionality,andluckily,wefindtwointerestingclassesimplementedintwodifferentlibrariesthatareunrelatedwithourapplication:SynthesizerandHuman.IntheSynthesizerclass,themainactionisperformedbytheplay()method.IntheHumanclass,itisperformedbythespeak()method.Toindicatethatthetwoclassesareexternal,weplacetheminaseparatemodule,asshown:
classSynthesizer:def__init__(self,name):self.name=name
def__str__(self):return'the{}synthesizer'.format(self.name)
defplay(self):return'isplayinganelectronicsong'
classHuman:def__init__(self,name):self.name=name
def__str__(self):return'{}thehuman'.format(self.name)
defspeak(self):
-
return'sayshello'
Sofarsogood.But,wehaveaproblem.Theclientonlyknowshowtocalltheexecute()method,andithasnoideaaboutplay()orspeak().HowcanwemakethecodeworkwithoutchangingtheSynthesizerandHumanclasses?Adapterstotherescue!WecreateagenericAdapterclassthatallowsustoadaptanumberofobjectswithdifferentinterfaces,intooneunifiedinterface.Theobjargumentofthe__init__()methodistheobjectthatwewanttoadapt,andadapted_methodsisadictionarycontainingkey/valuepairsofmethodtheclientcalls/methodthatshouldbecalled.
classAdapter:def__init__(self,obj,adapted_methods):self.obj=objself.__dict__.update(adapted_methods)
def__str__(self):returnstr(self.obj)
Let'sseehowwecanusetheAdapterpattern.Anobjectslistholdsalltheobjects.ThecompatibleobjectsthatbelongtotheComputerclassneednoadaptation.Wecanaddthemdirectlytothelist.Theincompatibleobjectsarenotaddeddirectly.TheyareadaptedusingtheAdapterclass.Theresultisthattheclientcodecancontinueusingtheknownexecute()methodonallobjectswithouttheneedtobeawareofanyinterfacedifferencesbetweentheusedclasses.
defmain():objects=[Computer('Asus')]synth=Synthesizer('moog')objects.append(Adapter(synth,dict(execute=synth.play)))human=Human('Bob')objects.append(Adapter(human,dict(execute=human.speak)))
foriinobjects:print('{}{}'.format(str(i),i.execute()))
Let'sseethecompletecodeoftheAdapterpatternexample(filesexternal.pyandadapter.py)asfollows:
classSynthesizer:def__init__(self,name):self.name=name
def__str__(self):return'the{}synthesizer'.format(self.name)
defplay(self):return'isplayinganelectronicsong'
classHuman:def__init__(self,name):self.name=name
-
def__str__(self):return'{}thehuman'.format(self.name)
defspeak(self):return'sayshello'
fromexternalimportSynthesizer,Human
classComputer:def__init__(self,name):self.name=name
def__str__(self):return'the{}computer'.format(self.name)
defexecute(self):return'executesaprogram'
classAdapter:def__init__(self,obj,adapted_methods):self.obj=objself.__dict__.update(adapted_methods)
def__str__(self):returnstr(self.obj)
defmain():objects=[Computer('Asus')]synth=Synthesizer('moog')objects.append(Adapter(synth,dict(execute=synth.play)))human=Human('Bob')objects.append(Adapter(human,dict(execute=human.speak)))
foriinobjects:print('{}{}'.format(str(i),i.execute()))
if__name__=="__main__":main()
Theoutputwhenexecutingtheexampleis:
>>>python3adapter.pytheAsuscomputerexecutesaprogramthemoogsynthesizerisplayinganelectronicsongBobthehumansayshello
WemanagedtomaketheHumanandSynthesizerclassescompatiblewiththeinterfaceexpectedbytheclient,withoutchangingtheirsourcecode.Thisisnice.
Here'sachallengingexerciseforyou.Thereisaproblemwiththisimplementation.Whileallclasseshaveanameattribute,thefollowingcodefails:
foriinobjects:print(i.name)
-
Firstofall,whydoesthiscodefail?Althoughthismakessensefromacodingpointofview,itdoesnotmakesenseatallfortheclientcodewhichshouldnotbeawareofdetailssuchaswhatisadaptedandwhatisnotadapted.Wejustwanttoprovideauniforminterface.Howcanwemakethiscodework?
TipHint:Thinkofhowyoucandelegatethenon-adaptedpartstotheobjectcontainedintheAdapterclass.
-
SummaryThischaptercoveredtheAdapterdesignpattern.WeusetheAdapterpatternformakingtwo(ormore)incompatibleinterfacescompatible.Asamotivation,ane-commercesystemthatshouldsupportmultiplecurrencieswasmentioned.Weuseadapterseverydayforinterconnectingdevices,chargingthem,andsoon.
Adaptermakesthingsworkaftertheyhavebeenimplemented.TheGrokPythonframeworkandtheTraitspackageusetheAdapterpatternforachievingAPIconformanceandinterfacecompatibility,respectively.Theopen/closeprincipleisstronglyconnectedwiththeseaspects.
Intheimplementationsection,wesawhowtoachieveinterfaceconformanceusingtheAdapterpatternwithoutmodifyingthesourcecodeoftheincompatiblemodel.ThisisachievedthroughagenericAdapterclassthatdoestheworkforus.Althoughwecouldusesub-classing(inheritance)toimplementtheAdapterpatterninthetraditionalwayinPython,thistechniqueisagreatalternative.
Inthenextchapter,wewillseehowwecanusetheDecoratorpatterntoextendthebehaviorofanobjectwithoutusingsub-classing.
-
Chapter5.TheDecoratorPatternWheneverwewanttoaddextrafunctionalitytoanobject,wehaveanumberofdifferentoptions.Wecan:
Addthefunctionalitydirectlytotheclasstheobjectbelongsto,ifitmakessense(forexample,addanewmethod)UsecompositionUseinheritance
Compositionshouldgenerallybepreferredoverinheritance,becauseinheritancemakescodereuseharder,it'sstatic,andappliestoanentireclassandallinstancesofit[GOF95,page31],[j.mp/decopat].
Designpatternsofferusafourthoptionthatsupportsextendingthefunctionalityofanobjectdynamically(inruntime):Decorators.ADecoratorpatterncanaddresponsibilitiestoanobjectdynamically,andinatransparentmanner(withoutaffectingotherobjects)[GOF95,page196].
Inmanyprogramminglanguages,theDecoratorpatternisimplementedusingsub-classing(inheritance)[GOF95,page198].InPython,wecan(andshould)usethebuilt-indecoratorfeature.APythondecoratorisaspecificchangetothesyntaxofPythonthatisusedforextendingthebehaviorofaclass,method,orfunctionwithoutusinginheritance.Intermsofimplementation,aPythondecoratorisacallable(function,method,class)thatacceptsafunctionobjectfinasinput,andreturnsanotherfunctionobjectfout[j.mp/conqdec