Ghidul cuprinzător al modelelor de design JavaScript

Publicat: 2022-03-11

Ca un bun dezvoltator JavaScript, te străduiești să scrii cod curat, sănătos și care poate fi întreținut. Rezolvi provocări interesante care, deși unice, nu necesită neapărat soluții unice. Probabil că v-ați trezit scriind cod care arată similar cu soluția unei probleme complet diferite pe care ați rezolvat-o înainte. Poate că nu știți, dar ați folosit un model de design JavaScript . Modelele de design sunt soluții reutilizabile la problemele frecvente în proiectarea software.

Ghidul cuprinzător al modelelor de design JavaScript

Pe durata de viață a oricărei limbi, multe astfel de soluții reutilizabile sunt realizate și testate de un număr mare de dezvoltatori din comunitatea limbii respective. Din cauza acestei experiențe combinate a multor dezvoltatori, astfel de soluții sunt atât de utile, deoarece ne ajută să scriem codul într-un mod optimizat, rezolvând în același timp problema la îndemână.

Principalele beneficii pe care le obținem din modelele de design sunt următoarele:

  • Sunt soluții dovedite: deoarece modelele de design sunt adesea folosite de mulți dezvoltatori, puteți fi sigur că funcționează. Și nu numai atât, poți fi sigur că au fost revizuite de mai multe ori și probabil că au fost implementate optimizări.
  • Sunt ușor reutilizabile: modelele de design documentează o soluție reutilizabilă care poate fi modificată pentru a rezolva mai multe probleme particulare, deoarece nu sunt legate de o anumită problemă.
  • Sunt expresive: modelele de design pot explica o soluție mare destul de elegant.
  • Ele ușurează comunicarea: atunci când dezvoltatorii sunt familiarizați cu modelele de design, pot comunica mai ușor între ei despre potențialele soluții la o anumită problemă.
  • Acestea împiedică necesitatea refactorizării codului: dacă o aplicație este scrisă având în vedere modelele de proiectare, adesea nu va trebui să refactorizați codul mai târziu, deoarece aplicarea modelului de proiectare corect la o anumită problemă este deja o soluție optimă. soluţie.
  • Ele micșorează dimensiunea bazei de cod: deoarece modelele de design sunt de obicei soluții elegante și optime, de obicei necesită mai puțin cod decât alte soluții.

Știu că sunteți gata să interveniți în acest moment, dar înainte de a afla totul despre modelele de design, să trecem în revistă câteva elemente de bază JavaScript.

O scurtă istorie a JavaScript

JavaScript este unul dintre cele mai populare limbaje de programare pentru dezvoltarea web în prezent. A fost creat inițial ca un fel de „clei” pentru diferitele elemente HTML afișate, cunoscut sub numele de limbaj de scriptare pe partea clientului, pentru unul dintre browserele web inițiale. Numit Netscape Navigator, acesta putea afișa doar HTML static la momentul respectiv. După cum ați putea presupune, ideea unui astfel de limbaj de scripting a dus la războaie de browser între marii jucători din industria de dezvoltare a browserului de atunci, cum ar fi Netscape Communications (azi Mozilla), Microsoft și alții.

Fiecare dintre marii jucători a vrut să-și dezvolte propria implementare a acestui limbaj de scripting, așa că Netscape a făcut JavaScript (de fapt, Brendan Eich a făcut), Microsoft a făcut JScript și așa mai departe. După cum vă puteți imagina, diferențele dintre aceste implementări au fost mari, așa că dezvoltarea pentru browsere web a fost făcută pe browser, cu stickere cele mai bine vizualizate care au venit cu o pagină web. Curând a devenit clar că avem nevoie de un standard, o soluție cross-browser, care să unifice procesul de dezvoltare și să simplifice crearea paginilor web. Ceea ce au venit ei se numește ECMAScript.

ECMAScript este o specificație standardizată a limbajului de scripting pe care toate browserele moderne încearcă să o suporte și există mai multe implementări (ați putea spune dialecte) ale ECMAScript. Cel mai popular este subiectul acestui articol, JavaScript. De la lansarea sa inițială, ECMAScript a standardizat o mulțime de lucruri importante, iar pentru cei mai interesați de detalii, există o listă detaliată de articole standardizate pentru fiecare versiune a ECMAScript disponibilă pe Wikipedia. Suportul de browser pentru versiunile ECMAScript 6 (ES6) și versiunile ulterioare sunt încă incomplete și trebuie să fie transpilate în ES5 pentru a fi pe deplin acceptate.

Ce este JavaScript?

Pentru a înțelege pe deplin conținutul acestui articol, să facem o introducere la câteva caracteristici foarte importante ale limbajului de care trebuie să fim conștienți înainte de a ne aprofunda în modelele de design JavaScript. Dacă cineva te-ar întreba „Ce este JavaScript?” ați putea răspunde undeva în rândurile:

JavaScript este un limbaj de programare ușor, interpretat, orientat pe obiecte, cu funcții de primă clasă cunoscute cel mai frecvent ca limbaj de scripting pentru pagini web.

Definiția menționată mai sus înseamnă că codul JavaScript are o amprentă redusă în memorie, este ușor de implementat și ușor de învățat, cu o sintaxă similară limbajelor populare precum C++ și Java. Este un limbaj de scripting, ceea ce înseamnă că codul său este interpretat în loc de compilat. Are suport pentru stiluri de programare procedurale, orientate pe obiecte și funcționale, ceea ce îl face foarte flexibil pentru dezvoltatori.

Până acum, ne-am uitat la toate caracteristicile care sună ca multe alte limbi, așa că haideți să aruncăm o privire la ceea ce este specific despre JavaScript în ceea ce privește alte limbi. Voi enumera câteva caracteristici și voi oferi cea mai bună șansă de a explica de ce merită o atenție specială.

JavaScript acceptă funcții de primă clasă

Această caracteristică era obișnuită pentru mine să înțeleg când tocmai începeam cu JavaScript, deoarece proveneam dintr-un fundal C/C++. JavaScript tratează funcțiile ca cetățeni de primă clasă, ceea ce înseamnă că puteți trece funcții ca parametri altor funcții la fel cum ați face orice altă variabilă.

 // we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })

JavaScript este bazat pe prototip

Așa cum este cazul multor alte limbaje orientate pe obiecte, JavaScript acceptă obiecte, iar unul dintre primii termeni care vin în minte atunci când ne gândim la obiecte este clasele și moștenirea. Aici devine puțin complicat, deoarece limbajul nu acceptă clase în forma sa simplă, ci mai degrabă folosește ceva numit moștenire bazată pe prototip sau pe instanță.

Tocmai acum, în ES6, este introdusă clasa formală de termeni, ceea ce înseamnă că browserele încă nu acceptă acest lucru (dacă vă amintiți, în momentul scrierii, ultima versiune ECMAScript complet acceptată este 5.1). Este important de reținut, totuși, că, deși termenul „clasă” este introdus în JavaScript, acesta utilizează în continuare moștenirea bazată pe prototipuri sub capotă.

Programarea bazată pe prototipuri este un stil de programare orientată pe obiecte în care reutilizarea comportamentului (cunoscută sub numele de moștenire) este realizată printr-un proces de reutilizare a obiectelor existente prin delegații care servesc ca prototipuri. Vom intra în mai multe detalii cu aceasta odată ce ajungem la secțiunea de modele de design a articolului, deoarece această caracteristică este folosită în multe modele de design JavaScript.

Bucle de evenimente JavaScript

Dacă aveți experiență de lucru cu JavaScript, cu siguranță sunteți familiarizat cu termenul funcție de apel invers . Pentru cei care nu sunt familiarizați cu termenul, o funcție de apel invers este o funcție trimisă ca parametru (rețineți, JavaScript tratează funcțiile ca cetățeni de primă clasă) către o altă funcție și este executată după declanșarea unui eveniment. Acesta este de obicei folosit pentru abonarea la evenimente precum un clic de mouse sau o apăsare a unui buton de la tastatură.

Reprezentare grafică a buclei de evenimente JavaScript

De fiecare dată când se declanșează un eveniment, căruia îi este atașat un ascultător (în caz contrar, evenimentul se pierde), un mesaj este trimis într-o coadă de mesaje care sunt procesate sincron, într-o manieră FIFO (primul-intrat-primul-out). ). Aceasta se numește bucla de evenimente .

Fiecare dintre mesajele din coadă are asociată o funcție. Odată ce un mesaj este scos din coadă, runtime execută funcția complet înainte de a procesa orice alt mesaj. Aceasta înseamnă că, dacă o funcție conține alte apeluri de funcție, acestea sunt toate efectuate înainte de procesarea unui nou mesaj din coadă. Aceasta se numește run-to-completion.

 while (queue.waitForMessage()) { queue.processNextMessage(); }

queue.waitForMessage() așteaptă sincron mesaje noi. Fiecare dintre mesajele procesate are propria sa stivă și este procesat până când stiva este goală. Odată ce se termină, un nou mesaj este procesat din coadă, dacă există.

S-ar putea să fi auzit, de asemenea, că JavaScript este neblocant, ceea ce înseamnă că atunci când se efectuează o operație asincronă, programul este capabil să proceseze alte lucruri, cum ar fi primirea intrărilor utilizatorului, în timp ce așteaptă finalizarea operației asincrone, fără a bloca fir de executie. Aceasta este o proprietate foarte utilă a JavaScript și un articol întreg ar putea fi scris doar pe acest subiect; cu toate acestea, este în afara domeniului de aplicare al acestui articol.

Ce sunt modelele de design?

După cum am spus mai devreme, modelele de design sunt soluții reutilizabile la problemele frecvente în proiectarea software. Să aruncăm o privire la câteva dintre categoriile de modele de design.

Proto-modele

Cum se creează un model? Să presupunem că ați recunoscut o problemă frecventă și aveți propria dvs. soluție unică la această problemă, care nu este recunoscută și documentată la nivel global. Utilizați această soluție de fiecare dată când întâmpinați această problemă și credeți că este reutilizabilă și că comunitatea de dezvoltatori ar putea beneficia de pe urma ei.

Devine imediat un tipar? Din fericire, nu. Adesea, cineva poate avea bune practici de scriere a codului și pur și simplu greșeli ceva care arată ca un model pentru unul când, de fapt, nu este un model.

Cum poți ști când ceea ce crezi că recunoști este de fapt un model de design?

Obținând opiniile altor dezvoltatori despre acesta, cunoașterea procesului de creare a unui model în sine și făcându-vă bine familiarizat cu modelele existente. Există o fază prin care trebuie să treacă un model înainte de a deveni un model cu drepturi depline, iar acesta se numește proto-model.

Un proto-pattern este un model viitor dacă trece de o anumită perioadă de testare de către diverși dezvoltatori și scenarii în care modelul se dovedește a fi util și dă rezultate corecte. Există o cantitate destul de mare de muncă și documentație – dintre care cea mai mare parte este în afara domeniului de aplicare al acestui articol – pentru a face un model cu drepturi depline recunoscut de comunitate.

Anti-tipare

Deoarece un model de design reprezintă o bună practică, un anti-model reprezintă o practică proastă.

Un exemplu de anti-model ar fi modificarea prototipului clasei Object . Aproape toate obiectele din JavaScript moștenesc de la Object (rețineți că JavaScript folosește moștenirea bazată pe prototipuri), așa că imaginați-vă un scenariu în care ați modificat acest prototip. Modificările aduse prototipului Object ar fi văzute în toate obiectele care moștenesc de la acest prototip , care ar fi majoritatea obiectelor JavaScript . Acesta este un dezastru care așteaptă să se întâmple.

Un alt exemplu, similar cu cel menționat mai sus, este modificarea obiectelor pe care nu le dețineți. Un exemplu în acest sens ar fi suprascrierea unei funcții dintr-un obiect utilizat în multe scenarii în cadrul aplicației. Dacă lucrați cu o echipă mare, imaginați-vă confuzia pe care aceasta ar provoca-o; te-ai confrunta rapid cu coliziuni de nume, implementări incompatibile și coșmaruri de întreținere.

La fel cum este util să cunoaștem toate practicile și soluțiile bune, este, de asemenea, foarte important să știi despre cele rele. În acest fel, îi puteți recunoaște și evitați greșeala din față.

Categorizarea modelelor de design

Modelele de design pot fi clasificate în mai multe moduri, dar cel mai popular este următorul:

  • Modele de design creațional
  • Modele de proiectare structurală
  • Modele de design comportamental
  • Modele de design de concurență
  • Modele de design arhitectural

Modele de design creațional

Aceste modele se ocupă de mecanismele de creare a obiectelor care optimizează crearea de obiecte în comparație cu o abordare de bază. Forma de bază de creare a obiectelor poate duce la probleme de proiectare sau la un plus de complexitate designului. Modelele de design creațional rezolvă această problemă controlând cumva crearea obiectelor. Unele dintre modelele de design populare din această categorie sunt:

  • Metoda fabricii
  • Fabrica de abstracte
  • Constructor
  • Prototip
  • Singleton

Modele de proiectare structurală

Aceste modele se ocupă de relațiile obiectelor. Ei asigură că, dacă o parte a unui sistem se modifică, întregul sistem nu trebuie să se schimbe odată cu acesta. Cele mai populare modele din această categorie sunt:

  • Adaptor
  • Pod
  • Compozit
  • Decorator
  • Faţadă
  • Muscă
  • Proxy

Modele de design comportamental

Aceste tipuri de modele recunosc, implementează și îmbunătățesc comunicarea între obiecte disparate dintr-un sistem. Acestea ajută la asigurarea faptului că părțile disparate ale unui sistem au informații sincronizate. Exemple populare ale acestor modele sunt:

  • Lanț de responsabilitate
  • Comanda
  • Iterator
  • Mediator
  • Memento
  • Observator
  • Stat
  • Strategie
  • Vizitator

Modele de proiectare simultană

Aceste tipuri de modele de design se ocupă de paradigme de programare cu mai multe fire. Unele dintre cele populare sunt:

  • Obiect activ
  • Reacție nucleară
  • Programator

Modele de design arhitectural

Modele de proiectare care sunt utilizate în scopuri arhitecturale. Unele dintre cele mai cunoscute sunt:

  • MVC (Model-View-Controller)
  • MVP (Model-Vizualizare-Prezentator)
  • MVVM (Model-View-ViewModel)

În secțiunea următoare, vom arunca o privire mai atentă asupra unora dintre modelele de design menționate mai sus, cu exemple furnizate pentru o mai bună înțelegere.

Exemple de modele de proiectare

Fiecare dintre modelele de proiectare reprezintă un tip specific de soluție pentru un anumit tip de problemă. Nu există un set universal de modele care să se potrivească întotdeauna cel mai bine. Trebuie să aflăm când un anumit model se va dovedi util și dacă va oferi valoare reală. Odată ce suntem familiarizați cu modelele și scenariile pentru care sunt cele mai potrivite, putem determina cu ușurință dacă un model specific este sau nu potrivit pentru o anumită problemă.

Amintiți-vă, aplicarea unui model greșit la o anumită problemă poate duce la efecte nedorite, cum ar fi complexitatea inutilă a codului, suprasolicitarea inutilă asupra performanței sau chiar generarea unui nou anti-model.

Toate acestea sunt lucruri importante de luat în considerare atunci când vă gândiți la aplicarea unui model de design codului nostru. Vom arunca o privire la unele dintre modelele de design pe care le-am găsit personal utile și cred că fiecare dezvoltator senior JavaScript ar trebui să fie familiarizat.

Model constructor

Când ne gândim la limbajele clasice orientate pe obiecte, un constructor este o funcție specială dintr-o clasă care inițializează un obiect cu un set de valori implicite și/sau trimise.

Modalitățile comune de a crea obiecte în JavaScript sunt următoarele trei moduri:

 // either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();

După crearea unui obiect, există patru moduri (de la ES3) de a adăuga proprietăți acestor obiecte. Acestea sunt următoarele:

 // supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });

Cel mai popular mod de a crea obiecte este parantezele și, pentru adăugarea proprietăților, notația cu puncte sau parantezele pătrate. Oricine are experiență cu JavaScript le-a folosit.

Am menționat mai devreme că JavaScript nu acceptă clase native, dar acceptă constructori prin utilizarea unui cuvânt cheie „nou” prefixat la un apel de funcție. În acest fel, putem folosi funcția ca constructor și inițializa proprietățile ei în același mod în care am face-o cu un constructor de limbaj clasic.

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Cu toate acestea, mai este loc de îmbunătățire aici. Dacă vă amintiți, am menționat anterior că JavaScript folosește moștenirea bazată pe prototip. Problema cu abordarea anterioară este că metoda writesCode este redefinită pentru fiecare dintre instanțe ale constructorului Person . Putem evita acest lucru setând metoda în prototipul funcției:

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Acum, ambele instanțe ale constructorului Person pot accesa o instanță partajată a writesCode() .

Modelul modulului

În ceea ce privește particularitățile, JavaScript nu încetează să uimească. Un alt lucru particular pentru JavaScript (cel puțin în ceea ce privește limbajele orientate pe obiecte) este că JavaScript nu acceptă modificatori de acces. Într-un limbaj OOP clasic, un utilizator definește o clasă și determină drepturile de acces pentru membrii săi. Deoarece JavaScript în forma sa simplă nu acceptă nici clase, nici modificatori de acces, dezvoltatorii JavaScript au găsit o modalitate de a imita acest comportament atunci când este necesar.

Înainte de a intra în specificul modelului modulului, să vorbim despre conceptul de închidere. O închidere este o funcție cu acces la domeniul părinte, chiar și după ce funcția părinte s-a închis. Ele ne ajută să imităm comportamentul modificatorilor de acces prin scoping. Să arătăm asta printr-un exemplu:

 // we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());

După cum puteți vedea, folosind IIFE, am legat variabila contor de o funcție care a fost invocată și închisă, dar poate fi încă accesată de funcția copil care o incrementează. Deoarece nu putem accesa variabila contor din afara expresiei funcției, am făcut-o privată prin manipularea scoping.

Folosind închiderile, putem crea obiecte cu părți private și publice. Acestea se numesc module și sunt foarte utile ori de câte ori dorim să ascundem anumite părți ale unui obiect și să expunem doar o interfață utilizatorului modulului. Să arătăm asta într-un exemplu:

 // through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());

Cel mai util lucru pe care îl introduce acest model este separarea clară a părților private și publice ale unui obiect, care este un concept foarte asemănător cu dezvoltatorii care provin dintr-un fundal clasic orientat pe obiecte.

Cu toate acestea, nu totul este atât de perfect. Când doriți să modificați vizibilitatea unui membru, trebuie să modificați codul oriunde ați folosit acest membru, din cauza naturii diferite de accesare a părților publice și private. De asemenea, metodele adăugate la obiect după crearea lor nu pot accesa membrii privați ai obiectului.

Revelarea modelului modulului

Acest model este o îmbunătățire adusă modelului modulului, așa cum este ilustrat mai sus. Principala diferență este că scriem întreaga logică a obiectului în domeniul privat al modulului și apoi pur și simplu expunem părțile pe care dorim să fie publice, returnând un obiect anonim. De asemenea, putem schimba denumirea membrilor privați atunci când mapăm membrii privați cu membrii publici corespunzători.

 // we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());

Modelul de modul revelator este unul dintre cel puțin trei moduri în care putem implementa un model de modul. Diferențele dintre modelul de modul revelator și celelalte variante ale modelului de modul sunt în primul rând în modul în care sunt referiți membrii publici. Ca rezultat, modelul modulului revelator este mult mai ușor de utilizat și modificat; cu toate acestea, se poate dovedi fragilă în anumite scenarii, cum ar fi utilizarea obiectelor RMP ca prototipuri într-un lanț de moștenire. Situațiile problematice sunt următoarele:

  1. Dacă avem o funcție privată care se referă la o funcție publică, nu putem suprascrie funcția publică, deoarece funcția privată va continua să se refere la implementarea privată a funcției, introducând astfel un bug în sistemul nostru.
  2. Dacă avem un membru public care indică o variabilă privată și încercăm să suprascriem membrul public din afara modulului, celelalte funcții s-ar referi în continuare la valoarea privată a variabilei, introducând un bug în sistemul nostru.

Model Singleton

Modelul singleton este folosit în scenarii când avem nevoie de exact o instanță a unei clase. De exemplu, trebuie să avem un obiect care conține o configurație pentru ceva. În aceste cazuri, nu este necesar să se creeze un nou obiect ori de câte ori obiectul de configurare este necesar undeva în sistem.

 var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);

După cum puteți vedea în exemplu, numărul aleator generat este întotdeauna același, precum și valorile de configurare trimise.

Este important de menționat că punctul de acces pentru preluarea valorii singleton trebuie să fie doar unul și foarte bine cunoscut. Un dezavantaj al folosirii acestui model este că este destul de dificil de testat.

Model de observator

Modelul de observator este un instrument foarte util atunci când avem un scenariu în care trebuie să îmbunătățim comunicarea între părți disparate ale sistemului nostru într-un mod optimizat. Promovează cuplarea liberă între obiecte.

Există diferite versiuni ale acestui model, dar în forma sa cea mai de bază, avem două părți principale ale modelului. Primul este un subiect, iar al doilea este observatori.

Un subiect se ocupă de toate operațiunile referitoare la o anumită temă la care sunt abonați observatorii. Aceste operațiuni abonează un observator la un anumit subiect, dezabonează un observator de la un anumit subiect și notifică observatorii despre un anumit subiect atunci când un eveniment este publicat.

Cu toate acestea, există o variație a acestui model numită model editor/abonat, pe care o voi folosi ca exemplu în această secțiune. Principala diferență dintre un model de observator clasic și modelul de editor/abonat este că editorul/abonatul promovează o cuplare și mai slabă decât o face modelul de observator.

În modelul observator, subiectul deține referințele la observatorii abonați și apelează metode direct din obiectele în sine, în timp ce, în modelul editor/abonat, avem canale, care servesc ca punte de comunicare între un abonat și un editor. Editorul declanșează un eveniment și pur și simplu execută funcția de apel invers trimisă pentru acel eveniment.

Voi afișa un scurt exemplu de model editor/abonat, dar pentru cei interesați, un exemplu clasic de model observator poate fi găsit cu ușurință online.

 var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});

Acest model de proiectare este util în situațiile în care trebuie să efectuăm mai multe operațiuni pe un singur eveniment declanșat. Imaginați-vă că aveți un scenariu în care trebuie să facem mai multe apeluri AJAX către un serviciu back-end și apoi să efectuăm alte apeluri AJAX, în funcție de rezultat. Ar trebui să înghesuiți apelurile AJAX unul în celălalt, posibil intrând într-o situație cunoscută sub numele de iadul callback. Utilizarea modelului editor/abonat este o soluție mult mai elegantă.

Un dezavantaj al utilizării acestui model este testarea dificilă a diferitelor părți ale sistemului nostru. Nu există o modalitate elegantă de a ști dacă părțile abonate ale sistemului se comportă sau nu așa cum era de așteptat.

Model mediator

Vom acoperi pe scurt un model care este, de asemenea, foarte util atunci când vorbim despre sisteme decuplate. Când avem un scenariu în care mai multe părți ale unui sistem trebuie să comunice și să fie coordonate, poate o soluție bună ar fi introducerea unui mediator.

Un mediator este un obiect care este folosit ca punct central pentru comunicarea între părțile disparate ale unui sistem și gestionează fluxul de lucru dintre ele. Acum, este important să subliniem că se ocupă de fluxul de lucru. De ce este acest lucru important?

Pentru că există o mare similitudine cu modelul editor/abonat. S-ar putea să vă întrebați, OK, așa că aceste două modele ajută ambele să implementeze o comunicare mai bună între obiecte... Care este diferența?

Diferența este că un mediator se ocupă de fluxul de lucru, în timp ce editorul/abonatul folosește ceva numit tip de comunicare „fog și uită”. Editorul/abonatul este pur și simplu un agregator de evenimente, ceea ce înseamnă că pur și simplu se ocupă de declanșarea evenimentelor și de a informa abonații corecti care evenimente au fost declanșate. Agregatorului de evenimente nu îi pasă ce se întâmplă odată cu declanșarea unui eveniment, ceea ce nu este cazul unui mediator.

Un exemplu frumos de mediator este interfața de tip vrăjitor. Să presupunem că aveți un proces mare de înregistrare pentru un sistem la care ați lucrat. Adesea, atunci când sunt necesare multe informații de la un utilizator, este o bună practică să împărțiți acest lucru în mai mulți pași.

În acest fel, codul va fi mult mai curat (mai ușor de întreținut) și utilizatorul nu este copleșit de cantitatea de informații care este solicitată doar pentru a finaliza înregistrarea. Un mediator este un obiect care s-ar ocupa de etapele de înregistrare, luând în considerare diferitele posibile fluxuri de lucru care s-ar putea întâmpla datorită faptului că fiecare utilizator ar putea avea un proces unic de înregistrare.

Beneficiul evident al acestui model de design este comunicarea îmbunătățită între diferite părți ale unui sistem, care acum toate comunică prin mediator și baza de cod mai curată.

Un dezavantaj ar fi că acum am introdus un singur punct de eșec în sistemul nostru, adică dacă mediatorul nostru eșuează, întregul sistem ar putea înceta să funcționeze.

Model prototip

După cum am menționat deja pe parcursul articolului, JavaScript nu acceptă clase în forma sa nativă. Moștenirea între obiecte este implementată folosind programarea bazată pe prototip.

Ne permite să creăm obiecte care pot servi drept prototip pentru alte obiecte create. Obiectul prototip este folosit ca model pentru fiecare obiect creat de constructor.

Așa cum am vorbit deja despre acest lucru în secțiunile anterioare, să arătăm un exemplu simplu despre cum ar putea fi utilizat acest model.

 var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

Command Pattern

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));

Facade Pattern

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

Pasii urmatori

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

Înrudit: În calitate de dezvoltator JS, acesta este ceea ce mă ține treaz noaptea / Înțelegând confuzia clasei ES6