Förstå designmönster i JavaScript

Idag ska vi sätta på våra datavetenskapshattar när vi lär oss om några vanliga mönster. Designmönster erbjuder utvecklare sätt att lösa tekniska problem på ett återanvändbart och elegant sätt. Intresserad av att bli en bättre JavaScript-utvecklare? Läs sedan vidare.

Publicerad handledning

Varje par veckor besöker vi några av våra läsares favoritinlägg från hela webbplatsens historia. Denna handledning publicerades först i juli 2012.


Introduktion

Fasta designmönster är det grundläggande byggstenen för underhållbara program. Om du någonsin deltagit i en teknisk intervju har du gärna blivit frågad om dem. I den här handledningen tar vi en titt på några mönster som du kan börja använda idag.


Vad är ett designmönster?

Ett designmönster är en återanvändbar mjukvarulösning

Enkelt uttryckt är ett designmönster en återanvändbar programvarulösning för en viss typ av problem som uppstår ofta när man utvecklar programvara. Under många års praktiserande mjukvaruutveckling har experter funnit sätt att lösa liknande problem. Dessa lösningar har inkapslats i designmönster. Så:

  • mönster är bevisade lösningar på problem med mjukvaruutveckling
  • Mönster är skalbara eftersom de vanligtvis är strukturerade och har regler som du bör följa
  • mönster är återanvändbara för liknande problem

Vi kommer in på några exempel på designmönster längre fram i handledningen.


Typer av designmönster

I mjukvaruutveckling är designmönster vanligtvis grupperade i några kategorier. Vi täcker de tre viktigaste i denna handledning. De förklaras kortfattat nedan:

  1. Skapande mönster fokuserar på sätt att skapa objekt eller klasser. Det här låter enkelt (och det är i vissa fall), men stora applikationer behöver styra skapandet av objekt.
  2. Strukturell designmönster fokuserar på sätt att hantera relationer mellan objekt så att din applikation är byggd på ett skalbart sätt. En viktig aspekt av strukturella mönster är att se till att en ändring i en del av din ansökan inte påverkar alla andra delar.
  3. Behavioral mönster fokuserar på kommunikation mellan objekt.

Du kan fortfarande få frågor efter att ha läst dessa korta beskrivningar. Det här är naturligt, och saker kommer att klara sig när vi tittar på några designmönster i djup nedan. Så läs vidare!


En anteckning om klasser i JavaScript

När du läser om designmönster ser du ofta referenser till klasser och objekt. Detta kan vara förvirrande, eftersom JavaScript inte verkligen har konstruktionen av "klass"; en mer korrekt term är "datatyp".

Datatyper i JavaScript

JavaScript är ett objektorienterat språk där objekt ärver från andra objekt i ett koncept som kallas prototypiskt arv. En datatyp kan skapas genom att definiera vad som kallas en konstruktörfunktion, så här:

funktion Person (config) this.name = config.name; this.age = config.age;  Person.prototype.getAge = function () returnera this.age; ; var tilo = ny person (namn: "Tilo", ålder: 23); console.log (tilo.getAge ());

Notera användningen av prototyp när man definierar metoder på Person data typ. Eftersom flera Person objekt kommer att referera till samma prototyp, vilket tillåter getAge () Metod som ska delas av alla instanser av Person datatyp, snarare än att omdefiniera den för varje instans. Dessutom, vilken datatyp som ärver från Person kommer att ha tillgång till getAge () metod.

Hantering av sekretess

Ett annat vanligt problem i JavaScript är att det inte finns någon sann känsla för privata variabler. Vi kan dock använda stängningar för att något simulera privatlivet. Tänk på följande kod:

var retinaMacbook = (funktion () // Privata variabler var RAM, addRAM; RAM = 4; // Privat metod addRAM = funktion (additionalRAM) RAM + = additionalRAM;; return // Offentliga variabler och metoder USB: undefined , insertUSB: funktion (enhet) this.USB = device;, removeUSB: function () var enhet = this.USB; this.USB = odefinierad; retur enhet;;) ();

I exemplet ovan skapade vi en retinaMacbook objekt, med offentliga och privata variabler och metoder. Så här skulle vi använda det:

retinaMacbook.insertUSB ( "myUSB"); console.log (retinaMacbook.USB); // loggar ut "myUSB" console.log (retinaMacbook.RAM) // loggar ut odefinierade

Det finns mycket mer som vi kan göra med funktioner och stängningar i JavaScript, men vi kommer inte att komma in i allt i den här handledningen. Med den här lilla lektionen om JavaScript-datatyper och integritet bakom oss kan vi fortsätta att lära oss om designmönster.


Creation Design Patterns

Det finns många olika slags skapande designmönster men vi kommer att täcka två av dem i denna handledning: Builder och Prototype. Jag tycker att dessa används ofta tillräckligt för att motivera uppmärksamheten.

Byggmönster

Builder Pattern används ofta i webbutveckling, och du har förmodligen använt det tidigare utan att förstå det. Enkelt uttryckt kan detta mönster definieras som följande:

Genom att använda byggmönstret kan vi bygga objekt genom att bara ange typ och innehåll för objektet. Vi behöver inte explicit skapa objektet.

Du har till exempel gjort det här otaliga gånger i jQuery:

var myDiv = $ ('
Detta är en div.
'); // myDiv representerar nu ett jQuery-objekt som refererar till en DOM-nod. var someText = $ ('

'); // someText är ett jQuery-objekt som refererar till en HTMLParagraphElement var input = $ ('');

Ta en titt på de tre exemplen ovan. I den första passerade vi i en

element med lite innehåll. I det andra passerade vi i en tom

märka. I den sista passerade vi i en element. Resultatet av alla tre var detsamma: vi returnerades ett jQuery-objekt som hänvisade till en DOM-nod.

De $ variabel adopterar byggmönstret i jQuery. I varje exempel returnerades vi ett jQuery DOM-objekt och hade tillgång till alla metoder som tillhandahålls av jQuery-biblioteket, men vi ringde inte uttryckligen document.createElement. JS-biblioteket hanterade allt detta under huven.

Föreställ dig hur mycket arbete det skulle vara om vi var tvungna att skapa DOM-elementet och infoga innehåll i det! Genom att utnyttja byggmönstret kan vi fokusera på typen och innehållet i objektet snarare än att skapa det explicit.

Prototypmönster

Tidigare gick vi igenom hur man definierar datatyper i JavaScript genom funktioner och lägger till metoder för objektets prototyp. Prototypmönstret gör det möjligt för föremål att ärva från andra objekt via sina prototyper.

Prototypmönstret är ett mönster där objekt skapas baserat på en mall av ett befintligt objekt genom kloning.

Detta är ett enkelt och naturligt sätt att genomföra arv i JavaScript. Till exempel:

var Person = numFeet: 2, numHeads: 1, numHands: 2; //Object.create tar sitt första argument och tillämpar det på prototypen för ditt nya objekt. var tilo = Object.create (Person); console.log (tilo.numHeads); // utgångar 1 tilo.numHeads = 2; console.log (tilo.numHeads) // utgångar 2

Egenskaperna (och metoderna) i Person objektet appliceras på prototypen av tilo objekt. Vi kan omdefiniera egenskaperna på tilo objekt om vi vill att dom ska vara annorlunda.

I exemplet ovan använde vi Object.create (). Internet Explorer 8 stöder emellertid inte den nya metoden. I dessa fall kan vi simulera sitt beteende:

var vehiclePrototype = init: funktion (carModel) this.model = carModel; , getModel: function () console.log ("Modellen för detta fordon är" + this.model); ; funktionskärl (modell) funktion F () ; F. prototyp = vehikelprototyp; var f = ny F (); f.init (modell); returnera f;  varbil = fordon ("Ford Escort"); car.getModel ();

Den enda nackdelen med den här metoden är att du inte kan ange skrivskyddade egenskaper, som kan anges när du använder Object.create (). Ändå visar prototypmönstret hur objekt kan arva från andra objekt.


Strukturella designmönster

Strukturella mönstren är verkligen till hjälp när man bestämmer hur ett system ska fungera. De tillåter våra applikationer att skala enkelt och förbli underhållsbar. Vi ska titta på följande mönster i denna grupp: Komposit och fasad.

Sammansatt mönster

Kompositmönstret är ett annat mönster som du förmodligen har använt tidigare utan någon realisering.

Kompositmönstret säger att en grupp av objekt kan behandlas på samma sätt som ett enskilt objekt hos gruppen.

Så vad betyder detta? Tja, betrakta detta exempel i jQuery (de flesta JS-bibliotek kommer att motsvara detta):

$ ( 'MyList ') addClass (' vald.'); . $ ( '# MyItem) addClass (utvalda'); // gör det inte på stora bord, det är bara ett exempel. $ ("# dataTable tbody tr"). på ("klicka", funktion (händelse) alert ($ (this) .text ());); $ ('# myButton'). på ("klicka", funktion (händelse) alert ("Clicked.";;);

De flesta JavaScript-bibliotek ger ett konsekvent API oberoende av om vi hanterar ett enda DOM-element eller en rad DOM-element. I det första exemplet kan vi lägga till vald klass till alla föremål som tas upp av .min lista väljare, men vi kan använda samma metod när det gäller ett singulärt DOM-element, #myItem. På samma sätt kan vi bifoga händelsehanterare med hjälp av på() metod på flera noder, eller på en enda nod genom samma API.

Genom att utnyttja kompositmönstret ger jQuery (och många andra bibliotek) oss ett förenklat API.

Kompositmönstret kan ibland orsaka problem också. I ett löst typat språk som JavaScript kan det ofta vara användbart att veta om vi hanterar ett enda element eller flera element. Eftersom kompositmönstret använder samma API för båda, kan vi ofta misstaka varandra och sluta med oväntade buggar. Vissa libaries, som YUI3, erbjuder två separata metoder för att få element (Y.one () mot Y.all ()).

Fasadmönster

Här är ett annat vanligt mönster som vi tar för givet. Faktum är att den här är en av mina favoriter eftersom det är enkelt, och jag har sett att den används överallt för att hjälpa till med webbläsarens inkonsekvenser. Här är vad fasadmönstret handlar om:

Fasadmönstret ger användaren ett enkelt gränssnitt, samtidigt som den döljer den underliggande komplexiteten.

Fasadmönstret förbättrar nästan alltid användbarheten hos ett program. Med hjälp av jQuery som ett exempel igen är en av de mer populära metoderna i biblioteket redo() metod:

$ (dokument) .ready (funktion () // all din kod går här ...);

De redo() Metoden implementerar faktiskt en fasad. Om du tittar på källan, här är vad du hittar:

redo: (funktion () ... // Mozilla, Opera och Webkit om (document.addEventListener) document.addEventListener ("DOMContentLoaded", idempotent_fn, false); ... // IE händelsemodell annars om (document.attachEvent) // säkerställa avfyring före onload, kanske sen men säkert även för iframes document.attachEvent ("onreadystatechange", idempotent_fn); // En återgång till window.onload, som alltid fungerar window.attachEvent ("onload", idempotent_fn); ...)

Under huven, den redo() Metoden är inte så enkelt. jQuery normaliserar webbläsarens inkonsekvenser för att säkerställa det redo() avfyras vid lämplig tidpunkt. Men som utvecklare presenteras du med ett enkelt gränssnitt.

De flesta exemplen på fasadmönstret följer denna princip. När vi implementerar en brukar vi förlita oss på villkorliga uttalanden under huven, men presentera det som ett enkelt gränssnitt till användaren. Andra metoder som implementerar detta mönster inkluderar animera() och css (). Kan du tänka på varför dessa skulle använda ett fasadmönster?


Behavioral Design Patterns

Alla objektorienterade mjukvarusystem kommer att ha kommunikation mellan objekt. Att inte organisera den kommunikationen kan leda till buggar som är svåra att hitta och fixa. Behavioral designmönster föreskriver olika metoder för att organisera kommunikationen mellan objekt. I det här avsnittet ska vi titta på Observer och Mediator mönster.

Observer Mönster

Observermönstret är det första av de två beteendemönster som vi ska gå igenom. Här är vad det står:

I observatörsmönstret kan ett ämne ha en lista över observatörer som är intresserade av sin livscykel. När som helst ämnet gör något intressant, skickar det en anmälan till dess observatörer. Om en observatör inte längre är intresserad av att lyssna på ämnet kan ämnet ta bort det från listan.

Låter ganska enkelt, eller hur? Vi behöver tre metoder för att beskriva detta mönster:

  • publicerar (data): Kallas av ämnet när det har en anmälan att göra. Vissa data kan överföras med den här metoden.
  • prenumerera (observatör): Uppmanad av ämnet att lägga till en observatör på dess lista över observatörer.
  • unsubscribe (observatör): Uppmanad av ämnet att ta bort en observatör från listan över observatörer.

Tja, det visar sig att de flesta moderna JavaScript-bibliotek stöder dessa tre metoder som en del av sin anpassade händelseinfrastruktur. Vanligtvis finns det en på() eller fästa() metod, a utlösaren () eller brand() metod och an av() eller lösgöra() metod. Tänk på följande kod:

// Vi skapar bara en koppling mellan jQuery-händelsemetoderna
// och de som föreskrivs av observatörsmönstret men du behöver inte göra det. var o = $ (); $ .subscribe = o.on.bind (o); $ .unsubscribe = o.off.bind (o); $ .publish = o.trigger.bind (o); // Användning document.on ('tweetsReceived', funktion (tweets) // utföra några åtgärder, brand sedan en händelse $ .publish ('tweetsShow', tweets);); // Vi kan prenumerera på den här händelsen och sedan skjuta vår egen händelse. $ .subscribe ('tweetsShow', function () // visa tweets på något sätt ... // publicera en åtgärd när de visas. $ .public ('tweetsDisplayed);); $ .subscribe ('tweetsDisplayed, function () ...);

Observer-mönstret är ett av de enklaste mönstren att implementera, men det är mycket kraftfullt. JavaScript är väl lämpat för att anta detta mönster eftersom det är naturligt händelsesbaserat. Nästa gång du utvecklar webbapplikationer, tänk på att utveckla moduler som är löstkopplade med varandra och anta Observer-mönstret som ett kommunikationsmedel. Observatörsmönstret kan bli problematiskt om det finns för många ämnen och observatörer som är involverade. Detta kan ske i storskaliga system, och nästa mönster vi tittar på försöker lösa detta problem.

Mediator Mönster

Det sista mönstret vi ska titta på är medlarmönstret. Det liknar Observer mönstret men med några anmärkningsvärda skillnader.

Mediatormönstret främjar användningen av ett enda gemensamt ämne som hanterar kommunikation med flera objekt. Alla objekt kommunicerar med varandra genom medlaren.

En bra real-world-analogi skulle vara ett Air Traffic Tower, som hanterar kommunikation mellan flygplatsen och flygningarna. I världen av mjukvaruutveckling används mediatormönstret ofta som ett system blir alltför komplicerat. Genom att placera medlare kan kommunikation hanteras genom ett enda objekt, snarare än att ha flera objekt som kommunicerar med varandra. I detta avseende kan ett mediatormönster användas för att ersätta ett system som implementerar observatörsmönstret.

Det finns ett förenklat genomförande av mediatormönstret av Addy Osmani i detta läs. Låt oss prata om hur du kan använda den. Tänk dig att du har en webapp som låter användare klicka på ett album och spela musik från den. Du kan skapa en medlare så här:

$ ('# album'). På ('klick', funktion (e) e.preventDefault (); var albumId = $ (detta) .id (); mediator.publish ("playAlbum", albumId);) ; var playAlbum = funktion (id) ... mediator.publish ("albumStartedPlaying", songList: [...], currentSong: "Without You"); ; var logAlbumPlayed = funktion (id) // Logga in albumet i backend; var updateUserInterface = funktion (album) // Uppdatera användargränssnitt för att reflektera vad som spelas upp; // Mediatorabonnemang mediator.subscribe ("playAlbum", playAlbum); mediator.subscribe ("playAlbum", logAlbumPlayed); mediator.subscribe ("albumStartedPlaying", updateUserInterface);

Fördelen med detta mönster över Observer-mönstret är att ett enda objekt är ansvarig för kommunikation, medan i observatörsmönstret kan flera objekt lyssna och prenumerera på varandra.

I Observer-mönstret finns inget enskilt objekt som inkapslar en begränsning. Istället måste observatören och ämnet samarbeta för att upprätthålla begränsningen. Kommunikationsmönster bestäms av det sätt som observatörer och ämnen är sammanlänkade: ett enskilt ämne har vanligtvis många observatörer, och ibland är observatören av ett subjekt föremål för en annan observatör.


Slutsats

Någon har redan tillämpat det framgångsrikt tidigare.

Det fina med designmönster är att någon redan har tillämpat den framgångsrikt tidigare. Det finns massor av öppen källkod som implementerar olika mönster i JavaScript. Som utvecklare måste vi vara medvetna om vilka mönster som finns där ute och när de ska tillämpas. Jag hoppas att denna handledning hjälpte dig att ta ett steg till att svara på dessa frågor.


Ytterligare läsning

Mycket av innehållet från den här artikeln finns i den utmärkta Learning JavaScript Design Patterns-boken, av Addy Osmani. Det är en online-bok som släpptes gratis under ett Creative Commons-licens. Boken täcker teori och implementering av många olika mönster, både i vanilj JavaScript och olika JS-bibliotek. Jag uppmanar dig att undersöka den som en referens när du börjar ditt nästa projekt.