Grundlæggende objektdesignmønstre i JavaScript

Effektiv objektdesign på fire måder

Foto af Dominik Scythe på Unsplash

Som JavaScript-udvikler handler meget af koden, du skriver, om objekter. Vi opretter objekter til at hjælpe med at organisere kode, reducere redundans og resonere om problemer ved hjælp af objektorienterede teknikker. Fordelene ved objektorienteret design er let synlige, men at genkende genstanden af ​​objekter er bare det første trin. Når du har besluttet at bruge en objektorienteret struktur i din kode, er det næste trin at beslutte, hvordan du gør det. I JavaScript er dette ikke så simpelt som at designe en klasse og instantisere objekter ud fra det (da JavaScript ikke har ægte klasser, men det er et spørgsmål til et andet blogindlæg.) Der er mange forskellige designmønstre til at skabe lignende objekter, og i dag er vi vil udforske nogle af de mest almindelige. Hvert mønster har sine egne fordele og ulemper, og forhåbentlig er du ved udgangen af ​​dette blogindlæg klar til at beslutte, hvilke af disse indstillinger der er rigtige for dig.

Hver udvikler har deres egne præferencer, men jeg vil tilbyde følgende kriterier, der skal overvejes, når jeg beslutter et passende objektdesignmønster til din kode.

  1. Læsbarhed: Som al god kode skal objektorienteret kode ikke kun læses for dig, men også for andre udviklere. Nogle designmønstre er lettere at fortolke end andre, og du skal altid holde læsbarheden i tankerne. Hvis du har svært ved at forstå, hvad din kode laver, vil andre udviklere næsten helt sikkert ikke have nogen anelse om det.
  2. Gentagelse: En af de største fordele ved objektorienteret kode er, at den reducerer redundans. Hvis din kode sandsynligvis har mange objekter af samme type, er et objektorienteret design næsten helt sikkert passende. Nogle mønstre reducerer dog overflødigheden mere end andre. Husk dette, mens du samtidig overvejer, at større reduktion af redundans kan resultere i tab (eller i det mindste vanskeligere implementering) af visse tilpasningsmuligheder.
  3. Hierarkisk struktur: Som vi nævnte før, har JavaScript ingen ægte klasser, og det er bedst ikke at tænke på dine objekter på denne måde; der er dog muligheder for at delegere lignende opførsel til forskellige sæt og undergrupper af objekter. Dette gøres ved hjælp af prototygdelegation, hvor et objekt søger i hele prototypekæden efter en given egenskab. På denne måde er det muligt at oprette en hierarkisk struktur af objekter, hvor et objekt af en lavere type i strukturen kan delegere adfærd opad i sin prototypekæde (for eksempel et kyllingobjekt, der delegerer en layEgg-adfærd til en højere op-prototype Fugleobjekt .) Inden du vælger et designmønster, skal du tage et øjeblik til at overveje, om du forventer, at en hierarkisk struktur er nødvendig, og i bekræftende fald hvilken adfærd, der skal placeres på hvilke objekttyper.

Og med disse få korte anbefalinger fuldført, så lad os se vores gennemgang af de mest almindelige designmønstre, som du sandsynligvis vil støde på.

Fabriksoprettelsesmønster

Factory Object Creation Pattern, eller simpelthen Factory Pattern, bruger såkaldte “fabrikkens funktioner” til at oprette objekter af en lignende type. Hvert objekt oprettet af en sådan funktion har de samme egenskaber til at omfatte både tilstand og adfærd. Tag for eksempel følgende:

Her har vi en funktion, makeRobot (), der tager to parametre (navn og job) og bruger dem til at tildele tilstand til et objekt bogstaveligt inde i funktionen, som det derefter returnerer. Derudover definerer funktionen en metode, introducere () på det samme objekt. I dette eksempel instantierer vi to robotobjekter, som begge har de samme egenskaber (omend med forskellige værdier.) Hvis vi ville, kunne vi oprette tusinder flere robotter på nøjagtig den samme måde og pålideligt forudsige, hvad deres egenskaber ville være hver gang.

Selvom fabriksmønsteret er nyttigt til at skabe lignende objekter, har det to store ulemper. For det første er der ingen måde at kontrollere, om et givet objekt blev oprettet af en bestemt fabrik. Vi kan for eksempel ikke sige noget som bender-instans af makeRobot for at finde ud af, hvordan bender blev oprettet. For det andet deler fabriksmønsteret ikke adfærd, snarere, det skaber blot nye versioner af en adfærd hver gang det kaldes og føjer dem til det objekt, der oprettes. Som et resultat gentages metoder på ny på ethvert objekt oprettet af fabriksfunktionen, hvilket optager værdifuld plads. I et stort program kan dette vise sig at være ekstremt langsomt og spildt.

Konstruktørmønster

En måde at tackle nogle af svaghederne ved fabriksmønsteret er at bruge det såkaldte konstruktørmønster. I dette mønster bruger vi en "konstruktorfunktion", som egentlig bare er en regelmæssig funktion, der kaldes ved hjælp af det nye nøgleord. Ved at bruge det nye nøgleord fortæller vi JavaScript om at udføre funktionen på en speciel måde, og fire vigtige ting vil ske:

  1. Funktionen opretter straks et nyt objekt.
  2. Funktionens eksekveringskontekst (dette) indstilles som det nye objekt.
  3. Funktionskoden udføres inden for det nye objekts eksekveringskontekst.
  4. Funktionen vil implicit returnere det nye objekt, fraværende noget andet eksplicit returnering.

Lad os ændre vores tidligere eksempel og prøv at lave nogle robotter ved hjælp af konstruktørmønsteret.

Dette uddrag ligner meget det foregående, bortset fra denne gang bruger vi dette nøgleord inden i funktionen til at henvise til et nyt objekt, indstille nogle tilstand og egenskaber på det og derefter implicit returnere, når funktionen er færdig med at udføre. For konventionens skyld (ikke nogen faktisk syntaktisk grund) har vi kaldt vores funktion simpelthen robot med en kapital "R". Og i modsætning til fabriksmønsteret, kan vi endda kontrollere, om et givet objekt blev konstrueret af robotfunktionen inden for.

Du kan blive fristet til at tænke på dette, som om vi havde oprettet en robot “klasse”, men det er vigtigt at huske, at vi ikke opretter kopier af robot, som vi måske er på et ægte klassesprog. Snarere udnytter vi et link, der oprettes mellem det nyligt instantierede objekts prototype og dets tilhørende konstruktorfunktions prototype, som letter prototypedelegation. Vi har ikke rigtig draget fordel af denne funktionalitet i ovenstående uddrag, da vi stadig opretter en ny introducer () -metode på hver eneste nye robot. Lad os se, om vi kan løse det.

Pseudoklassisk mønster

Indtil videre har vi ikke rigtig undersøgt prototypedelegation, bortset fra at kort nævne, at den eksisterer. Nu er det tid til at se det i aktion og eliminere en vis kodeadundans på samme tid. Objektprototyper og deres delegationsadfærd er et helt blogindlæg værdigt, men vi kan få mindst et grundlæggende billede her. I det væsentlige, når en bestemt egenskab kaldes på et bestemt objekt, f.eks. NogleRobot.introduce (), går det først efter sig selv efter denne egenskab. Hvis der ikke findes en sådan egenskab, ser den på de egenskaber, der er tilgængelige for dets prototypeobjekt, som igen ser på dets prototypeobjekt om nødvendigt, og så videre helt op til det øverste niveau Object.prototype. Prototypekæden tillader delegering af adfærd, hvor vi ikke behøver at definere nogen delt metode på objekter på lavere niveau af samme type. I stedet kan vi definere adfærden på hvilken prototype de alle deler, og således eliminere overflødighed ved kun at definere koden én gang. Her er det i aktion med vores robotter.

Som i konstruktørmønsteret bruger vi det nye nøgleord til at oprette et nyt objekt, tildele en eller anden tilstand og derefter implicit returnere det objekt. I dette tilfælde definerer vi dog ikke introduktionsmetoden () på hver af vores robotter. Snarere definerer vi det på Robot.prototype-objektet, som som vi har set, fungerer som prototypen for hvert nyt objekt, der er oprettet af Robotkonstruktorfunktionen. Når vi forsøger at kalde for eksempel wallE.introduce (), ser wallE-objektet, at det ikke har nogen sådan metode og fortsætter med at søge efter sin prototype-kæde og hurtigt finde en metode med det navn på Robot.prototype. Faktisk, hvis vi kontrollerer wallEs prototype ved hjælp af Object.getPrototypeOf (), kan vi se, at det faktisk er Robot.prototype.

Dette designmønster, kendt som det pseudoklassiske mønster, løser begge de problemer, vi oprindeligt så i fabriksmønsteret; dog giver det os stadig den lidt ubehagelige illusion af et klassebaseret system. Dette kan føre til nogle uheldige omveje i vores mentale model for, hvordan JavaScript virkelig fungerer, og nogle uventede gotchas i selve programudførelsen. En løsning på dette problem, populariseret af Kyle Simpson, forfatter af You Don't Know JS, er det objekt, der er knyttet til andet objekt (OLOO) -mønster, som vi skal undersøge næste.

Objekt knyttet til andet objektmønster

Hvis det pseudoklassiske mønster er en foreløbig kombination af konstruktørmønsteret og prototype delegation, kan OLOO måske betragtes som en fuldstændig omfavnelse af JavaScript's prototypesystem. I dette mønster bruger vi overhovedet ikke en funktion til at oprette objekter. I stedet definerer vi en slags plan, som vi derefter eksplicit bruger som prototype til alle individuelle objekter, vi har brug for. Vi kan se dette i aktion med et sidste sæt robotter.

I dette uddrag definerer vi først et robotobjekt, der vil fungere som prototypen for alle fremtidige robotter. Robotobjektet indeholder al den opførsel, vi forventer af vores robotter; det indstiller dog ikke nogen tilstand. Snarere definerer vi en init () -metode på Robot, som vi vil bruge til at indstille tilstand på eventuelle fremtidige robotter. Når vi taler om fremtidige robotter, i stedet for at oprette dem med en funktion, gør vi det ved hjælp af metoden Object.create (), som accepterer en prototype som et argument. Ved at videregive Robot til metoden Object.create () sikrer vi, at det resulterende objekt har Robot til sin prototype. Vi kalder derefter init () -metoden på vores individuelle robotter for at indstille den nødvendige tilstand. Vi kan endda kontrollere, om et givet objekt er af en bestemt type ved hjælp af metoden Object.getPrototypeOf (), som vi gjorde i tidligere kodestykker.

OLOO giver os mulighed for at dele lignende opførsel og kontrollere typen af ​​individuelle objekter, alt sammen mens vi sidestolder klassens illusioner i konstruktøren og de pseudoklassiske mønstre. For mange udviklere foretrækkes denne metode, fordi den giver mulighed for let at forstå kode, der også er effektiv og ren.

Hvilke objektoprettelsesmønstre du i sidste ende vælger er op til dig, men forhåbentlig har dette været en god introduktion til nogle af de tilgængelige indstillinger. Objekter i JavaScript er utroligt magtfulde, især når de kombineres med effektiv anvendelse af objektprototyper - og vi er ikke engang begyndt at udforske de muligheder, der stilles til rådighed for os ved fuldt ud at udnytte flere trin i prototypekæden. Dette er dog et emne for en anden dag. Indtil da glad kodning!