JavaScript-designmønstre

Den ultimative guide til de mest nyttige designmønstre

UPDATE BEMÆRK: Opdateret Proxy-mønstereksempel til brug af ES6 Proxy og Reflect. Erstattede billeder af kildekodestykker med GitHub-bjælker.

I denne artikel skal vi tale om designmønstre, der kan og bør bruges til at skrive en bedre, vedligeholdelig JavaScript-kode. Jeg antager, at du har en grundlæggende forståelse af JavaScript og koncepter som klasser (klasser i JavaScript kan være vanskelige), objekter, prototype arv, lukninger osv.

Denne artikel er længe læst som helhed på grund af emnets art, så jeg har forsøgt at holde sektionerne selvstændige. Så du som læser kan vælge og vælge specifikke dele (eller i dette tilfælde specifikke mønstre) og ignorere dem, du ikke er interesseret i eller er godt bevandret i. Lad os komme i gang.

Bemærk: Kildekode til implementering af alle de designmønstre, der er forklaret her, findes på GitHub.

Introduktion

Vi skriver kode for at løse problemer. Disse problemer har normalt mange ligheder, og når vi prøver at løse dem, bemærker vi flere almindelige mønstre. Det er her designmønstre kommer ind.

Et designmønster er et udtryk, der bruges i software engineering til en generel, genanvendelig løsning på et almindeligt forekommende problem inden for software design.

Det underliggende koncept med designmønstre har eksisteret i softwareingeniørbranchen helt fra begyndelsen, men de var virkelig ikke så formaliserede. Designmønstre: Elementer af genanvendelig objektorienteret software skrevet af Erich Gamma, Richard Helm, Ralph Johnson og John Vlissides - den berømte Gang of Four (GoF) - var medvirkende til at skubbe til det formaliserede koncept med designmønstre inden for software engineering. Nu er designmønstre en væsentlig del af softwareudviklingen og har været det i lang tid.

Der var 23 designmønstre introduceret i den originale bog.

De klassiske 23 mønstre introduceret af GoF

Designmønstre er gavnlige af forskellige grunde. Det er gennemprøvede løsninger, som brancheveteraner har prøvet og testet. De er solide tilgange, der løser problemer på en bredt accepteret måde og afspejler erfaringerne og indsigterne fra de brancheførende udviklere, der var med til at definere dem. Mønstre gør også din kode mere genanvendelig og læsbar, mens du fremskynder udviklingsprocessen enormt.

Designmønstre er på ingen måde færdige løsninger. De giver os kun tilgange eller ordninger til løsning af et problem.

Bemærk: I denne artikel vil vi hovedsageligt tale om designmønstre fra et objektorienteret synspunkt og i sammenhæng med deres anvendelighed i moderne JavaScript. Derfor kan mange klassiske mønstre fra GoF udelades, og nogle moderne mønstre fra kilder som Addy Osmanis Lær JavaScript-designmønstre vil blive inkluderet. Eksemplerne holdes enkle for lettere at forstå og er derfor ikke den mest optimerede implementering af deres respektive designmønstre.

Kategorier af designmønstre

Designmønstre kategoriseres normalt i tre hovedgrupper.

Kreative designmønstre

Som navnet antyder, er disse mønstre til håndtering af objektoprettelsesmekanismer. Et kreativt designmønster løser dybest set et problem ved at kontrollere oprettelsesprocessen for et objekt.

Vi vil drøfte følgende mønstre detaljeret: Konstruktormønster, fabriksmønster, prototypemønster og Singletonmønster.

Strukturelle designmønstre

Disse mønstre drejer sig om klasse- og objektsammensætning. De hjælper med at strukturere eller omstrukturere en eller flere dele uden at påvirke hele systemet. Med andre ord hjælper de med at få nye funktionaliteter uden at manipulere med de eksisterende.

Vi diskuterer følgende mønstre detaljeret: Adaptermønster, sammensat mønster, dekoratormønster, facademønster, flyvemønster og proxy-mønster.

Adfærdsmæssige designmønstre

Disse mønstre vedrører forbedring af kommunikationen mellem forskellige objekter.

Vi vil drøfte følgende mønstre detaljeret: Kæde af ansvarsmønster, kommandomønster, Iteratormønster, Mediatormønster, Observatormønster, tilstandsmønster, strategimønster og skabelonmønster.

Konstruktørmønster

Dette er et klassebaseret kreativt designmønster. Konstruktører er specielle funktioner, der kan bruges til at instantisere nye objekter med metoder og egenskaber, der er defineret af den funktion.

Det er ikke et af de klassiske designmønstre. Faktisk er det mere en grundlæggende sprogkonstruktion end et mønster i de fleste objektorienterede sprog. Men i JavaScript kan objekter oprettes på farten uden nogen konstruktørfunktioner eller "klasse" -definition. Derfor synes jeg det er vigtigt at lægge grundlaget for, at andre mønstre kommer med denne enkle.

Konstruktormønster er et af de mest almindeligt anvendte mønstre i JavaScript til oprettelse af nye objekter af en given art.

I dette eksempel definerer vi en helteklasse med attributter som navn og specialegenskaber og metoder som getDetails. Derefter instantierer vi et objekt IronMan ved at påberåbe konstruktørmetoden med det nye nøgleord, der indtaster værdierne for de respektive attributter som argumenter.

Fabriksmønster

Fabriksmønster er et andet klassebaseret kreativt mønster. I dette tilvejebringer vi en generisk grænseflade, der delegerer ansvaret for objektinformation til dets underklasser.

Dette mønster bruges ofte, når vi er nødt til at styre eller manipulere samlinger af objekter, der er forskellige, men som stadig har mange lignende egenskaber.

I dette eksempel opretter vi en fabriksklasse ved navn BallFactory, der har en metode, der tager parametre, og afhængigt af parametrene delegerer den objektets instantieringsansvar til den respektive klasse. Hvis typeparameteren er "fodbold" eller "fodbold", objekt-instantiering håndteres af fodboldklasse, men hvis det er "basketball", håndteres objekt-instantiering af basketball-klassen.

Prototypemønster

Dette mønster er et objektbaseret kreativt designmønster. I dette bruger vi en slags et "skelet" af et eksisterende objekt til at oprette eller instantisere nye objekter.

Dette mønster er specifikt vigtigt og gavnligt for JavaScript, fordi det bruger prototype arv i stedet for en klassisk objektorienteret arv. Derfor spiller det til JavaScript's styrke og har oprindelig support.

I dette eksempel har vi et bilobjekt, som vi bruger som prototype til at oprette et andet objekt myCar med JavaScript's Object.create-funktion og definere en ekstra egenskabsejer på det nye objekt.

Singleton mønster

Singleton er et specielt kreativt designmønster, hvor kun et eksempel på en klasse kan eksistere. Det fungerer sådan - hvis der ikke findes nogen forekomst af singleton-klassen, oprettes og returneres en ny instans, men hvis der allerede findes en forekomst, returneres henvisningen til den eksisterende instans.

Et perfekt eksempel på det virkelige liv ville være mongoose (det berømte Node.js ODM-bibliotek til MongoDB). Den bruger singleton-mønsteret.

I dette eksempel har vi en databaseklasse, der er en singleton. Først opretter vi en objektmongo ved at bruge den nye operatør til at påberåbe sig databaseklasse-konstruktøren. Denne gang er et objekt instantieret, fordi der ikke allerede findes noget. Anden gang, når vi opretter mysql-objektet, er der ikke noget nyt objekt, der instantieres, men i stedet returneres henvisningen til det objekt, der blev instantieret tidligere, dvs. mongo-objektet.

Adaptermønster

Dette er et strukturelt mønster, hvor grænsefladen fra en klasse oversættes til en anden. Dette mønster lader klasser arbejde sammen, der ellers ikke kunne på grund af inkompatible grænseflader.

Dette mønster bruges ofte til at oprette indpakninger til nye refakturerede API'er, så andre eksisterende gamle API'er stadig kan arbejde med dem. Dette gøres normalt, når nye implementeringer eller kodeforarbejdning (udført af grunde som præstationsgevinster) resulterer i et andet offentligt API, mens de andre dele af systemet stadig bruger det gamle API og skal tilpasses til at arbejde sammen.

I dette eksempel har vi en gammel API, dvs. OldCalculator-klasse og en ny API, dvs. NewCalculator-klasse. OldCalculator-klassen giver en driftsmetode til både tilføjelse og subtraktion, mens NewCalculator leverer separate metoder til tilføjelse og subtraktion. Adapterklassen CalcAdapter bryder NewCalculator for at tilføje driftsmetoden til det offentligt stillede API, mens den bruger sin egen tilføjelse og subtraktion implementering under hætten.

Sammensat mønster

Dette er et strukturelt designmønster, der komponerer objekter i trælignende strukturer til at repræsentere hierarkier i en helhed. I dette mønster kan hver knude i den trælignende struktur enten være et individuelt objekt eller en sammensat samling af objekter. Uanset hvad behandles hver knude ensartet.

En menustruktur på flere niveauer

Det er lidt kompliceret at visualisere dette mønster. Den nemmeste måde at tænke over dette er med eksemplet på en menu på flere niveauer. Hver knude kan være en særlig mulighed, eller det kan være en menu i sig selv, der har flere indstillinger som sit barn. En nodekomponent med børn er en sammensat komponent, mens en nodekomponent uden noget barn er en bladkomponent.

I dette eksempel opretter vi en baseklasse af komponent, der implementerer de nødvendige funktionaliteter og abstraherer de andre nødvendige metoder. Baseklassen har også en statisk metode, der bruger rekursion til at krydse en sammensat træstruktur lavet med dens underklasser. Derefter opretter vi to underklasser, der udvider baseklassen - Blad, der ikke har nogen børn og komposit, der kan få børn - og dermed har metoder til at håndtere tilføjelse, søgning og fjernelse af børns funktionaliteter. De to underklasser bruges til at skabe en sammensat struktur - et træ i dette tilfælde.

Dekorationsmønster

Dette er også et strukturelt designmønster, der fokuserer på evnen til at tilføje adfærd eller funktionaliteter til eksisterende klasser dynamisk. Det er et andet bæredygtigt alternativ til underklasse.

Opføreren af ​​dekoratortypen er meget let at implementere i JavaScript, fordi JavaScript tillader os at tilføje metoder og egenskaber til objektiv dynamisk. Den enkleste fremgangsmåde ville være at blot tilføje en egenskab til et objekt, men det vil ikke kunne genbruges effektivt.

Der er faktisk et forslag om at tilføje dekoratører til JavaScript-sproget. Se på Addy Osmanis indlæg om dekoratører i JavaScript.

Hvis du vil læse om selve forslaget, er du velkommen.

I dette eksempel opretter vi en bogklasse. Vi opretter yderligere to dekoratorfunktioner, der accepterer et bogobjekt og returnerer et "dekoreret" bogobjekt - gaveWrap, der tilføjer en ny attribut og en ny funktion og hardbindBook, der tilføjer en ny attribut og redigerer værdien af ​​en eksisterende attribut.

Fasademønster

Dette er et strukturelt designmønster, der er vidt brugt i JavaScript-bibliotekerne. Det bruges til at tilvejebringe en samlet og enklere, offentligt vendt grænseflade for brugervenlighed, der afskærmer sig fra kompleksiteten i dets eksisterende undersystemer eller underklasser.

Brugen af ​​dette mønster er meget almindeligt i biblioteker som jQuery.

I dette eksempel opretter vi et offentligt API med klassen ComplaintRegistry. Den udsætter kun én metode, der skal bruges af klienten, dvs. registerComplaint. Den håndterer internt inddrivning af krævede objekter i enten ProductComplaint eller ServiceComplaint baseret på type-argumentet. Det håndterer også alle de andre komplekse funktionaliteter som at generere et unikt ID, gemme klagen i hukommelsen osv. Men alle disse kompleksiteter skjules væk ved hjælp af facademønsteret.

Flyvemønster

Dette er et strukturelt designmønster, der er fokuseret på effektiv datadeling gennem finkornede objekter. Det bruges til effektivitet og hukommelse bevarelse.

Dette mønster kan bruges til enhver form for cache-formål. Faktisk bruger moderne browsere en variant af et flyvemønster for at forhindre indlæsning af de samme billeder to gange.

I dette eksempel opretter vi en finkornet flyvevægtklasse til at dele data om is-smag og en fabriksklasse IcecreamFactory for at oprette disse flyvevægtige objekter. Til hukommelsesbevaring genvindes objekterne, hvis den samme genstand bliver instantiseret to gange. Dette er et simpelt eksempel på implementering af flyvevægt.

Proxy-mønster

Dette er et strukturelt designmønster, der fungerer nøjagtigt som navnet antyder. Det fungerer som surrogat eller pladsholder for et andet objekt til at kontrollere adgangen til det.

Det bruges normalt i situationer, hvor et målobjekt er under begrænsninger og muligvis ikke er i stand til at håndtere alt dets ansvar effektivt. En proxy giver i dette tilfælde normalt den samme grænseflade til klienten og tilføjer et niveau af indirektion til at understøtte kontrolleret adgang til målobjektet for at undgå unødigt pres på det.

Proxy-mønsteret kan være meget nyttigt, når man arbejder med netværksanmodningstunge applikationer for at undgå unødvendige eller overflødige netværksanmodninger.

I dette eksempel bruger vi to nye ES6-funktioner, Proxy og Reflect. Et Proxy-objekt bruges til at definere brugerdefineret opførsel til grundlæggende operationer af et JavaScript-objekt (husk, funktion og arrays er også objekt i JavaScript). Det er en konstruktormetode, der kan bruges til at oprette et Proxy-objekt. Det accepterer et målobjekt, der skal proxys, og et behandlingsobjekt, der definerer den nødvendige tilpasning. Håndteringsobjektet giver mulighed for at definere nogle fældefunktioner som get, sæt, har, anvende osv., Der bruges til at tilføje brugerdefineret opførsel knyttet til deres brug. Reflect er på den anden side et indbygget objekt, der leverer lignende metoder, der understøttes af handlerobjektet Proxy som statiske metoder i sig selv. Det er ikke en konstruktør; dens statiske metoder bruges til at opfanges af JavaScript-operationer.

Nu opretter vi en funktion, der kan tænkes som en netværksanmodning. Vi kaldte det som netværksfetch. Den accepterer en URL og svarer i overensstemmelse hermed. Vi ønsker at implementere en proxy, hvor vi kun får svaret fra netværket, hvis det ikke er tilgængeligt i vores cache. Ellers returnerer vi bare et svar fra cachen.

Den globale variabel cache gemmer vores cache-svar. Vi opretter en proxy ved navn proxiedNetworkFetch med vores originale netværksfetch som mål og brug-anvendelsesmetode i vores behandlerobjekt til proxy af funktionskaldelsen. Anvendelsesmetoden overføres på selve objektet. Denne værdi som thisArg og argumenterne sendes til den i en array-lignende struktur args.

Vi kontrollerer, om det vedlagte url-argument er i cachen. Hvis den findes i cachen, returnerer vi svaret derfra og påkalder vi aldrig den originale målfunktion. Hvis det ikke gør det, bruger vi Reflect.apply-metoden til at påkalde målfunktionen med thisArg (selvom det ikke har nogen betydning i vores tilfælde her) og de argumenter, den har givet.

Kæde af ansvarsmønster

Dette er et adfærdsmæssigt designmønster, der giver en kæde af løst koblede objekter. Hver af disse objekter kan vælge at handle på eller håndtere anmodning fra klienten.

Et godt eksempel på kæden med ansvarsmønster er den begivenhed, der bobler i DOM, hvor en begivenhed udbreder sig gennem en række indlejrede DOM-elementer, hvoraf den ene kan have en "begivenhedslytter" knyttet til at lytte til og handle på begivenheden.

I dette eksempel opretter vi en klasse CumulativeSum, som kan instantieres med en valgfri initialValue. Det har en metode tilføjelse, der tilføjer den videregivne værdi til summen attribut for objektet og returnerer selve objektet for at tillade kæde af opkald til metodemetode.

Dette er et almindeligt mønster, der også kan ses i jQuery, hvor næsten ethvert metodekald på et jQuery-objekt returnerer et jQuery-objekt, så metodekald kan kædes sammen.

Kommandomønster

Dette er et adfærdsdesignmønster, der sigter mod at indkapslere handlinger eller handlinger som objekter. Dette mønster tillader løs kobling af systemer og klasser ved at adskille objekter, der anmoder om en operation eller påkalde en metode fra dem, der udfører eller behandler den faktiske implementering.

Klippebordets interaktion API ligner noget kommandomønsteret. Hvis du er en Redux-bruger, er du allerede stødt på kommandomønsteret. De handlinger, der tillader den fantastiske tid-rejse fejlsøgningsfunktion er intet andet end indkapslede operationer, der kan spores for at gentage eller fortryde operationer. Derfor blev tidsrejser muliggjort.

I dette eksempel har vi en klasse kaldet SpecialMath, der har flere metoder og en kommandoklasse, der indkapsler kommandoer, der skal udføres på dets emne, dvs. et objekt i klassen SpecialMath. Kommandoklassen holder også styr på alle de udførte kommandoer, som kan bruges til at udvide dens funktionalitet til at omfatte fortrydelse og forny operationer.

Iteratormønster

Det er et adfærdsdesignmønster, der giver en måde at få adgang til elementerne i et samlet objekt efter hinanden uden at afsløre dets underliggende repræsentation.

Iteratorer har en speciel form for opførsel, hvor vi går gennem et ordnet sæt værdier ad gangen ved at ringe til næste (), indtil vi når slutningen. Introduktionen af ​​Iterator og Generatorer i ES6 gjorde implementeringen af ​​iteratormønsteret ekstremt ligetil.

Vi har to eksempler nedenfor. Først bruger den ene IteratorClass iteratorspecifikation, mens den anden en iteratorUsingGenerator bruger generatorfunktioner.

Symbol.iterator (symbol - en ny type primitiv datatype) bruges til at specificere standard iteratoren for et objekt. Det skal defineres for en samling for at være i stand til at anvende til ... for looping-konstruktion. I det første eksempel definerer vi konstruktøren til at gemme en vis indsamling af data og definerer derefter Symbol.iterator, der returnerer et objekt med den næste metode til iteration.

I det andet tilfælde definerer vi en generatorfunktion, der sender den en række data og returnerer dens elementer iterativt ved hjælp af næste og udbytte. En generatorfunktion er en speciel type funktion, der fungerer som en fabrik for iteratorer og eksplicit kan opretholde sin egen interne tilstand og give værdier iterativt. Det kan pause og genoptage sin egen eksekveringscyklus.

Mægler mønster

Det er et adfærdsdesignmønster, der indkapsler hvordan et sæt objekter interagerer med hinanden. Det giver den centrale myndighed over en gruppe af objekter ved at fremme løs kobling og forhindre genstande i at henvise til hinanden eksplicit.

I dette eksempel har vi TrafficTower som mægler, der styrer måden, hvor flyobjekter interagerer med hinanden. Alle fly-objekter registrerer sig selv med et TrafficTower-objekt, og det er mæglerklasse-objektet, der håndterer, hvordan et fly-objekt modtager koordinatedata for alle de andre fly-objekter.

Observatørmønster

Det er et afgørende adfærdsdesignmønster, der definerer en-til-mange-afhængighed mellem objekter, så når et objekt (udgiver) ændrer sin tilstand, bliver alle de andre afhængige objekter (abonnenter) underrettet og opdateret automatisk. Dette kaldes også PubSub (udgiver / abonnenter) eller mønster for begivenhedssender / lyttere. Udgiveren kaldes undertiden emnet, og abonnenterne kaldes undertiden observatører.

Chancerne er, at du allerede er noget fortrolig med dette mønster, hvis du har brugt addEventListener eller jQuery's. Til at skrive jævnhåndteringskode. Det har også indflydelse på reaktiv programmering (tænk RxJS).

I eksemplet opretter vi en simpel Emne-klasse, der har metoder til at tilføje og fjerne objekter af Observer-klasse fra abonnentindsamling. Også en firemethode til at udbrede eventuelle ændringer i Subject class-objektet til de abonnerede observatører. Observer-klassen har derimod sin interne tilstand og en metode til at opdatere sin interne tilstand baseret på ændringen, der er propageret fra det Emne, den har abonneret på.

Tilstandsmønster

Det er et adfærdsdesignmønster, der tillader et objekt at ændre dets adfærd baseret på ændringer i dets interne tilstand. Det genstand, der returneres af en tilstandsmønsterklasse ser ud til at ændre sin klasse. Det giver tilstandsspecifik logik til et begrænset sæt objekter, hvor hver objekttype repræsenterer en bestemt tilstand.

Vi vil tage et simpelt eksempel på et trafiklys for at forstå dette mønster. TrafficLight-klassen ændrer det objekt, det returnerer, baseret på dets interne tilstand, som er et objekt af rød, gul eller grøn klasse.

Strategimønster

Det er et adfærdsdesignmønster, der tillader indkapsling af alternative algoritmer til en bestemt opgave. Den definerer en familie af algoritmer og indkapsler dem på en sådan måde, at de kan udskiftes ved kørsel uden klientinterferens eller viden.

I eksemplet herunder opretter vi en klasse pendling til indkapsling af alle mulige strategier for pendling til arbejde. Derefter definerer vi tre strategier, nemlig Bus, PersonalCar og Taxi. Ved hjælp af dette mønster kan vi bytte implementering til brug for rejsemetoden for Commute-objektet under kørsel.

Skabelonmønster

Dette er et adfærdsdesignmønster, der er baseret på at definere skeletet til algoritmen eller implementering af en operation, men udskyde nogle trin til underklasser. Det lader underklasser omdefinere visse trin i en algoritme uden at ændre algoritmens udadgående struktur.

I dette eksempel har vi en medarbejder i skabelonklasse, der delvist implementerer arbejdsmetoden. Det er til underklasser at implementere ansvarsmetoden for at få den til at fungere som en helhed. Vi opretter derefter to underklasser Developer og Tester, der udvider skabelonklassen og implementerer den krævede metode til at udfylde implementeringsgabet.

Konklusion

Designmønstre er afgørende for software-engineering og kan være meget nyttige til at løse almindelige problemer. Men dette er et meget stort emne, og det er simpelthen ikke muligt at inkludere alt om dem i et kort stykke. Derfor tog jeg valget om kun kort og præcist at tale om dem, som jeg synes kan være rigtig gode til at skrive moderne JavaScript. For at dykke dybere foreslår jeg, at du kigger på disse bøger:

  1. Designmønstre: Elements Of Genanvendelig Objektorienteret software af Erich Gamma, Richard Helm, Ralph Johnson og John Vlissides (Gang of Four)
  2. Lær JavaScript-designmønstre af Addy Osmani
  3. JavaScript-mønstre af Stoyan Stefanov