Fundamentele ontwerppatronen van objecten in JavaScript

Effectief objectontwerp op vier manieren

Foto door Dominik Scythe op Unsplash

Als JavaScript-ontwikkelaar gaat een groot deel van de code die u gaat schrijven, over objecten. We maken objecten om code te organiseren, redundantie te verminderen en redeneren over problemen met behulp van objectgeoriënteerde technieken. De voordelen van objectgeoriënteerd ontwerp zijn meteen duidelijk, maar het nut van objecten herkennen is slechts de eerste stap. Nadat u hebt besloten om een ​​objectgeoriënteerde structuur in uw code te gebruiken, is de volgende stap om te beslissen hoe u dit doet. In JavaScript is dit niet zo eenvoudig als het ontwerpen van een klasse en het instantiëren van objecten (omdat JavaScript geen echte klassen heeft, maar dat is een vraag voor een ander blogbericht.) Er zijn veel verschillende ontwerppatronen voor het maken van soortgelijke objecten, en vandaag zijn we enkele van de meest voorkomende onderzoeken. Elk patroon heeft zijn eigen voor- en nadelen, en hopelijk ben je aan het einde van deze blogpost klaar om te beslissen welke van deze opties geschikt zijn voor jou.

Elke ontwikkelaar heeft zijn eigen voorkeuren, maar ik zou de volgende criteria willen aanbieden om te overwegen bij het kiezen van een geschikt objectontwerppatroon voor uw code.

  1. Leesbaarheid: Zoals alle goede code, moet objectgeoriënteerde code niet alleen voor u leesbaar zijn, maar ook voor andere ontwikkelaars. Sommige ontwerppatronen zijn eenvoudiger te interpreteren dan andere en u moet altijd de leesbaarheid in gedachten houden. Als je moeite hebt om te begrijpen wat je code doet, hebben andere ontwikkelaars vrijwel zeker geen idee.
  2. Herhaling: een van de grote voordelen van objectgeoriënteerde code is dat het redundantie vermindert. Als uw code waarschijnlijk veel objecten van hetzelfde type heeft, is een objectgeoriënteerd ontwerp vrijwel zeker geschikt. Sommige patronen verminderen redundantie echter meer dan andere. Houd dit in gedachten, terwijl u tegelijkertijd in overweging neemt dat een grotere reductie van redundantie kan leiden tot verlies (of op zijn minst moeilijkere implementatie) van bepaalde aanpassingsopties.
  3. Hiërarchische structuur: zoals we eerder hebben vermeld, heeft JavaScript geen echte klassen, en het is het beste om niet op deze manier aan uw objecten te denken; er zijn echter opties voor het delegeren van soortgelijk gedrag aan verschillende sets en subsets van objecten. Dit wordt gedaan met behulp van prototype-delegatie, waarbij een object de hele prototypeketen doorzoekt naar een bepaalde eigenschap. Op deze manier is het mogelijk om een ​​hiërarchische structuur van objecten te maken waarbij een object van een lager type in de structuur gedrag kan overdragen in de prototypeketen (bijvoorbeeld een Chicken-object dat een layEgg-gedrag delegeert aan een hoger prototype Bird-object .) Neem, voordat u een ontwerppatroon selecteert, even de tijd om te overwegen of u verwacht dat een hiërarchische structuur nodig is, en zo ja, welk gedrag op welke objecttypen moet worden geplaatst.

En met die paar korte aanbevelingen compleet, laten we eens kijken naar de meest voorkomende ontwerppatronen die u waarschijnlijk zult tegenkomen.

Patroon maken van fabrieksobjecten

Het Factory Object Creation Pattern, of simpelweg het Factory Pattern, gebruikt zogenaamde "fabrieksfuncties" om objecten van hetzelfde type te maken. Elk object dat door een dergelijke functie is gemaakt, heeft dezelfde eigenschappen, zowel status als gedrag. Neem bijvoorbeeld het volgende:

Hier hebben we een functie, makeRobot (), die twee parameters (naam en taak) gebruikt en deze gebruikt om de status toe te wijzen aan een letterlijk object binnen de functie, die het vervolgens retourneert. Bovendien definieert de functie een methode, introduce (), op hetzelfde object. In dit voorbeeld instantiëren we twee robotobjecten, die beide dezelfde eigenschappen hebben (zij het met verschillende waarden). Als we zouden willen, zouden we duizenden meer robots op precies dezelfde manier kunnen maken en op betrouwbare wijze voorspellen wat hun eigenschappen elke keer zouden zijn.

Hoewel het fabriekspatroon nuttig is voor het maken van soortgelijke objecten, heeft het twee belangrijke nadelen. Ten eerste is er geen manier om te controleren of een bepaald object door een bepaalde fabriek is gemaakt. We kunnen bijvoorbeeld niet zoiets zeggen als Bender instanceof makeRobot om erachter te komen hoe Bender is gemaakt. Ten tweede deelt het fabriekspatroon geen gedragingen, maar creëert het gewoon nieuwe versies van een gedrag telkens wanneer het wordt aangeroepen en voegt het toe aan het object dat wordt gemaakt. Als gevolg hiervan worden methoden opnieuw herhaald op elk object dat door de fabrieksfunctie is gemaakt, waardoor waardevolle ruimte in beslag wordt genomen. In een groot programma kan dit extreem langzaam en verspillend blijken te zijn.

Constructor patroon

Een manier om enkele van de zwakke punten van het fabriekspatroon aan te pakken, is door het zogenaamde constructorpatroon te gebruiken. In dit patroon gebruiken we een 'constructorfunctie', die eigenlijk gewoon een normale functie is die wordt opgeroepen met het nieuwe trefwoord. Door het nieuwe trefwoord te gebruiken, vertellen we JavaScript de functie op een speciale manier uit te voeren, en er zullen vier belangrijke dingen gebeuren:

  1. De functie zal onmiddellijk een nieuw object maken.
  2. De functie-uitvoeringscontext (deze) wordt ingesteld als het nieuwe object.
  3. De functiecode wordt uitgevoerd binnen de uitvoeringscontext van het nieuwe object.
  4. De functie retourneert impliciet het nieuwe object, zonder enige andere expliciete terugkeer.

Laten we ons vorige voorbeeld wijzigen en proberen enkele robots te maken met behulp van het constructorpatroon.

Dit fragment lijkt veel op het vorige, behalve deze keer dat we het trefwoord this in de functie gebruiken om naar een nieuw object te verwijzen, een staat en eigenschappen erop in te stellen en vervolgens impliciet terug te keren wanneer de functie is voltooid. Omwille van de conventie (geen feitelijke syntactische reden) hebben we onze functie eenvoudigweg Robot met een hoofdletter "R" genoemd. En, in tegenstelling tot het fabriekspatroon, kunnen we zelfs controleren of een bepaald object door de robotfunctie is gebouwd binnen de stand ervan.

Je zou in de verleiding kunnen komen om hieraan te denken alsof we een "klasse" van de robot hadden gecreëerd, maar het is belangrijk om te onthouden dat we geen kopieën van de robot maken zoals we dat in een echte klastaal kunnen zijn. In plaats daarvan maken we gebruik van een koppeling die is gemaakt tussen het prototype van het nieuw geïnstantieerde object en het prototype van de bijbehorende constructorfunctie, wat de prototypische delegatie mogelijk maakt. We hebben echter niet echt gebruik gemaakt van die functionaliteit in het bovenstaande fragment, omdat we nog steeds een nieuwe methode introduce () maken voor elke nieuwe robot. Laten we kijken of we dat kunnen oplossen.

Pseudo-klassiek patroon

Tot nu toe hebben we niet echt de prototypische delegatie onderzocht, behalve om kort te vermelden dat deze bestaat. Nu is het tijd om het in actie te zien en tegelijkertijd wat redundantie van de code te elimineren. Objectprototypes en hun delegatiegedrag verdienen een hele blogpost, maar we kunnen hier op zijn minst een basisfoto krijgen. Wanneer een bepaalde eigenschap op een bepaald object wordt aangeroepen, bijvoorbeeld someRobot.introduce (), gaat deze in wezen eerst naar die eigenschap. Als een dergelijke eigenschap niet bestaat, wordt vervolgens gekeken naar de beschikbare eigenschappen voor het prototypeobject, dat op zijn beurt kijkt naar het prototypeobject indien nodig, enzovoort tot aan het hoogste object Object.prototype. Met de prototypeketen kan gedrag worden gedelegeerd, waarbij we geen gedeelde methode hoeven te definiëren voor objecten van een lager niveau van hetzelfde type. In plaats daarvan kunnen we het gedrag definiëren op elk prototype dat ze allemaal delen en zo redundantie elimineren door de code slechts eenmaal te definiëren. Hier is het in actie met onze robots.

Net als in het constructorpatroon gebruiken we het nieuwe trefwoord om een ​​nieuw object te maken, een status toe te wijzen en dat object vervolgens impliciet terug te geven. In dit geval definiëren we echter niet de methode introduce () op al onze robots. In plaats daarvan definiëren we het op het Robot.prototype-object, dat, zoals we hebben gezien, fungeert als het prototype van elk nieuw object dat is gemaakt door de constructorfunctie van de robot. Wanneer we bijvoorbeeld wallE.introduce () proberen aan te roepen, ziet het wallE-object dat het geen dergelijke methode heeft en zoekt het de prototypeketen op, waarbij het snel een methode met die naam op Robot.prototype vindt. Als we het prototype van wallE controleren met Object.getPrototypeOf (), kunnen we zien dat het inderdaad Robot.prototype is.

Dit ontwerppatroon, bekend als het pseudo-klassieke patroon, lost beide problemen op die we aanvankelijk in het fabriekspatroon zagen; het biedt ons echter nog steeds de ietwat ongemakkelijke illusie van een op klassen gebaseerd systeem. Dit kan leiden tot enkele ongelukkige omwegen in ons mentale model van hoe JavaScript echt werkt, en enkele onverwachte problemen met de uitvoering van het programma. Een oplossing voor dit probleem, gepopulariseerd door Kyle Simpson, auteur van You Don't Know JS, is het object Linked to Other Object (OLOO) -patroon, dat we hierna zullen onderzoeken.

Object gekoppeld aan ander objectpatroon

Als het pseudo-klassieke patroon een voorlopige combinatie is van het constructorpatroon en de prototypische delegatie, dan zou OLOO kunnen worden beschouwd als een volledige omhelzing van het prototypesysteem van JavaScript. In dit patroon gebruiken we helemaal geen functie om objecten te maken. In plaats daarvan definiëren we een soort blauwdrukobject, dat we vervolgens expliciet gebruiken als het prototype voor alle afzonderlijke objecten die we nodig hebben. We kunnen dit in actie zien met een laatste set robots.

In dit fragment definiëren we eerst een robotobject, dat zal dienen als het prototype voor alle toekomstige robots. Het robotobject bevat alle gedragingen die we van onze robots verwachten; het stelt echter geen status in. In plaats daarvan definiëren we een init () -methode op Robot, die we zullen gebruiken om de status van toekomstige robots in te stellen. Over toekomstige robots gesproken, in plaats van ze met een functie te maken, doen we dit door de methode Object.create () te gebruiken, die een prototype als argument accepteert. Door Robot door te geven aan de methode Object.create (), zorgen we ervoor dat het resulterende object Robot als prototype heeft. We roepen vervolgens de methode init () op onze afzonderlijke robots aan om de benodigde status in te stellen. We kunnen zelfs controleren of een bepaald object van een bepaald type is met behulp van de handige methode Object.getPrototypeOf (), zoals we in eerdere fragmenten deden.

OLOO stelt ons in staat om hetzelfde gedrag te delen en het type individuele objecten te controleren, terwijl we tegelijkertijd de klasse-illusies vermijden die inherent zijn aan de constructor en pseudo-klassieke patronen. Voor veel ontwikkelaars heeft deze methode de voorkeur omdat deze zorgt voor gemakkelijk te begrijpen code die ook efficiënt en schoon is.

Welke patronen voor het maken van objecten u uiteindelijk kiest, is aan u, maar hopelijk is dit een goede introductie geweest voor enkele van de beschikbare opties. Objecten in JavaScript zijn ongelooflijk krachtig, vooral in combinatie met effectief gebruik van objectprototypes - en we zijn nog niet eens begonnen met het verkennen van de ons ter beschikking gestelde opties door het volledig benutten van meerdere stappen in de prototypeketen. Dat is echter een onderwerp voor een andere dag. Tot die tijd gelukkig coderen!