Lucrez cu JavaScript activat și dezactivat de la sfârșitul anilor nouăzeci. La început nu mi-a plăcut, dar după introducerea ES2015 (alias ES6), am început să apreciez JavaScript ca un limbaj de programare remarcabil, dinamic, cu o putere expresivă enormă.

De-a lungul timpului, am adoptat mai multe modele de codificare care au dus la un cod mai curat, mai testabil și mai expresiv. Acum, vă împărtășesc aceste modele.

Am scris despre primul model – „RORO” – în articolul de mai jos. Nu vă faceți griji dacă nu l-ați citit, le puteți citi în orice ordine.

Modele elegante în JavaScript modern: RORO
Am scris primele mele rânduri de JavaScript la scurt timp după inventarea limbii. Dacă mi-ai spus atunci că eu …medium.freecodecamp.org

Astăzi, aș dori să vă prezint modelul „Fabrica de gheață”.

O fabrică de gheață este doar o funcție care creează și returnează un obiect înghețat. Vom despacheta această afirmație într-o clipă, dar mai întâi să explorăm de ce acest tipar este atât de puternic.

Clasele JavaScript nu sunt atât de elegante

De multe ori are sens să grupăm funcțiile legate într-un singur obiect. De exemplu, într-o aplicație de comerț electronic, am putea avea un cart obiect care expune un addProduct funcție și a removeProduct funcţie. Apoi am putea invoca aceste funcții cu cart.addProduct() și cart.removeProduct().

Dacă provii dintr-un limbaj de programare orientat spre obiect, orientat pe obiecte, cum ar fi Java sau C #, probabil că acest lucru se simte destul de natural.

Dacă sunteți nou în programare – acum că ați văzut o afirmație de genul cart.addProduct(). Bănuiesc că ideea grupării funcțiilor sub un singur obiect arată destul de bine.

Deci, cum am crea acest mic frumos cart obiect? Primul tău instinct cu JavaScript modern ar putea fi să folosești un class. Ceva asemănător cu:

// ShoppingCart.js
export default class ShoppingCart {  constructor({db}) {    this.db = db  }    addProduct (product) {    this.db.push(product)  }    empty () {    this.db = []  }
  get products () {    return Object      .freeze([...this.db])  }
  removeProduct (id) {    // remove a product   }
  // other methods
}
// someOtherModule.js
const db = [] const cart = new ShoppingCart({db})cart.addProduct({   name: 'foo',   price: 9.99})

Notă: Folosesc un Array pentru db parametru pentru simplitate. În codul real, acest lucru ar fi ceva de genul Model sau Repo care interacționează cu o bază de date reală.

Din păcate – chiar dacă pare frumos – clasele în JavaScript se comportă destul de diferit de ceea ce v-ați putea aștepta.

Clasele JavaScript te vor mușca dacă nu ești atent.

De exemplu, obiecte create folosind new cuvintele cheie sunt modificabile. Deci, puteți de fapt reatribuiți o metodă:

const db = []const cart = new ShoppingCart({db})
cart.addProduct = () => 'nope!' // No Error on the line above!
cart.addProduct({   name: 'foo',   price: 9.99}) // output: "nope!" FTW?

Chiar mai rău, obiectele create folosind new cuvânt cheie moștenește prototype din class care a fost folosit pentru a le crea. Deci, schimbările într-o clasă prototype a afecta toate obiecte create din asta class – chiar dacă se face o schimbare după obiectul a fost creat!

Uita-te la asta:

const cart = new ShoppingCart({db: []})const other = new ShoppingCart({db: []})
ShoppingCart.prototype  .addProduct = () => ‘nope!’// No Error on the line above!
cart.addProduct({   name: 'foo',   price: 9.99}) // output: "nope!"
other.addProduct({   name: 'bar',   price: 8.88}) // output: "nope!"

Apoi este faptul că this În JavaScript este legat dinamic. Deci, dacă trecem în jurul metodelor noastre cart obiect, putem pierde referința la this. Acest lucru este foarte contra-intuitiv și ne poate pune într-o mulțime de probleme.

O capcană obișnuită este atribuirea unei metode de instanță unui gestionar de evenimente.

Luați în considerare a noastră cart.empty metodă.

empty () {    this.db = []  }

Dacă atribuim această metodă direct la click în cazul unui buton pe pagina noastră web …

<button id="empty">  Empty cart</button>
---
document  .querySelector('#empty')  .addEventListener(    'click',     cart.empty  )

… Când utilizatorii fac clic pe gol button, al lor cart va rămâne plin.

Aceasta eșuează în tăcere deoarece this se va referi acum la button in loc de cart. Deci, a noastră cart.empty metoda sfârșește prin atribuirea unei noi proprietăți button numit db și setarea acelei proprietăți la [] în loc să afecteze cart obiecte db.

Acesta este genul de bug care te va înnebuni, deoarece nu există nicio eroare în consolă și bunul tău simț îți va spune că ar trebui să funcționeze, dar nu.

Pentru ca acesta să funcționeze trebuie să facem:

document  .querySelector("#empty")  .addEventListener(    "click",     () => cart.empty()  )

Sau:

document  .querySelector("#empty")  .addEventListener(    "click",     cart.empty.bind(cart)  )

Cred că Mattias Petter Johansson a spus-o cel mai bine:

new și this [in JavaScript] sunt un fel de capcană curcubeu neintuitivă, ciudată, cu nori. ”

Fabrica de gheață în salvare

După cum am spus mai devreme, o fabrică de gheață este doar o funcție care creează și returnează un obiect înghețat. Cu o fabrică de gheață, exemplul coșului nostru de cumpărături arată astfel:

// makeShoppingCart.js
export default function makeShoppingCart({  db}) {  return Object.freeze({    addProduct,    empty,    getProducts,    removeProduct,    // others  })
  function addProduct (product) {    db.push(product)  }    function empty () {    db = []  }
  function getProducts () {    return Object      .freeze([...db])  }
  function removeProduct (id) {    // remove a product  }
  // other functions}
// someOtherModule.js
const db = []const cart = makeShoppingCart({ db })cart.addProduct({   name: 'foo',   price: 9.99})

Observați că „capcanele ciudate, curcubeu cu nori” au dispărut:

  • Nu mai avem nevoie new.
    Invocăm doar o funcție JavaScript veche simplă pentru a ne crea cart obiect.
  • Nu mai avem nevoie this.
    Putem accesa db obiectează direct din funcțiile noastre de membru.
  • Al nostru cart obiectul este complet imuabil.
    Object.freeze() îngheață cart obiect astfel încât să nu i se poată adăuga noi proprietăți, proprietățile existente să nu poată fi eliminate sau modificate și nici prototipul să nu poată fi modificat. Amintiți-vă asta Object.freeze() este superficial, deci dacă obiectul pe care îl returnăm conține un array sau alt object trebuie să ne asigurăm că Object.freeze() ei de asemenea. De asemenea, dacă utilizați un obiect înghețat în afara unui Modulul ES, trebuie să fii în modul strict pentru a vă asigura că realocările provoacă o eroare, mai degrabă decât să eșueze în tăcere.

Un pic de intimitate, vă rog

Un alt avantaj al fabricilor de gheață este că pot avea membri privați. De exemplu:

function makeThing(spec) {  const secret="shhh!"
  return Object.freeze({    doStuff  })
  function doStuff () {    // We can use both spec    // and secret in here   }}
// secret is not accessible out here
const thing = makeThing()thing.secret // undefined

Acest lucru este posibil datorită închiderilor în JavaScript, despre care puteți citi mai multe MDN.

O mică recunoaștere, vă rog

Deși funcțiile din fabrică au existat pentru totdeauna JavaScript, modelul Ice Factory a fost puternic inspirat de un cod care Douglas Crockford a arătat în acest video.

Iată Crockford care demonstrează crearea obiectelor cu o funcție pe care el o numește „constructor”:

Modele elegante in JavaScript ul modern Ice Factory
Douglas Crockford demonstrând codul care m-a inspirat.

Versiunea My Ice Factory a exemplului Crockford de mai sus ar arăta astfel:

function makeSomething({ member }) {  const { other } = makeSomethingElse()     return Object.freeze({     other,    method  }) 
  function method () {    // code that uses "member"  }}

Am profitat de ridicarea funcției pentru a-mi pune declarația de returnare aproape de vârf, astfel încât cititorii să aibă un mic rezumat frumos a ceea ce se întâmplă înainte de a intra în detalii.

De asemenea, am folosit destructurarea pe spec parametru. Și am redenumit modelul în „Fabrica de gheață”, astfel încât să fie mai memorabil și mai puțin ușor de confundat cu constructor funcție dintr-un JavaScript class. Dar este practic același lucru.

Deci, credit în cazul în care se datorează creditul, mulțumesc domnule Crockford.

Notă: Probabil că merită menționat faptul că Crockford consideră funcția „ridicare” o „parte proastă” a JavaScript și ar considera probabil erezia mea versiunii. Am discutat despre sentimentele mele în acest sens într-un articolul anterior și mai concret, acest comentariu.

Dar moștenirea?

Dacă bifăm de-a lungul construirii micii noastre aplicații de comerț electronic, ne-am putea da seama curând că conceptul de adăugare și eliminare a produselor continuă să apară din nou și din nou peste tot.

Împreună cu Coșul de cumpărături, avem probabil un obiect Catalog și un obiect Comandă. Și toate acestea expun probabil o versiune a „addProduct” și „removeProduct”.

Știm că duplicarea este proastă, așa că vom fi în cele din urmă tentați să creăm ceva de genul unui obiect din lista de produse din care să poată moșteni coșul, catalogul și comanda noastră.

Dar, în loc să ne extindem obiectele prin moștenirea unei liste de produse, putem adopta în schimb principiul atemporal oferit într-una dintre cele mai influente cărți de programare scrise vreodată:

„Favorizați compoziția obiectelor în locul moștenirii clasei.”
– Modele de proiectare: elemente ale software-urilor reutilizabile orientate pe obiecte.

De fapt, autorii acelei cărți – cunoscut în mod colocvial sub numele de „The Gang of Four” – continuă spunând:

„… Experiența noastră este că proiectanții folosesc în exces moștenirea ca tehnică de reutilizare, iar desenele și modelele sunt adesea mai reutilizabile (și mai simple) depinzând mai mult de compoziția obiectului.”

Deci, iată lista noastră de produse:

function makeProductList({ productDb }) {  return Object.freeze({    addProduct,    empty,    getProducts,    removeProduct,    // others  )}   // definitions for   // addProduct, etc…}

Și iată coșul nostru de cumpărături:

function makeShoppingCart(productList) {  return Object.freeze({    items: productList,    someCartSpecificMethod,    // …)}
function someCartSpecificMethod () {  // code   }}

Și acum putem doar să ne injectăm lista de produse în coșul de cumpărături, astfel:

const productDb = []const productList = makeProductList({ productDb })
const cart = makeShoppingCart(productList)

Și utilizați lista de produse prin intermediul proprietății „articole”. Ca:

cart.items.addProduct()

Poate fi tentant să subsumăm întreaga Listă de produse prin încorporarea metodelor sale direct în obiectul coșului de cumpărături, astfel:

function makeShoppingCart({   addProduct,  empty,  getProducts,  removeProduct,  …others}) {  return Object.freeze({    addProduct,    empty,    getProducts,    removeProduct,    someOtherMethod,    …others)}
function someOtherMethod () {  // code   }}

De fapt, într-o versiune anterioară a acestui articol, am făcut exact asta. Dar apoi mi s-a subliniat că acest lucru este cam periculos (așa cum sa explicat aici). Deci, este mai bine să rămânem cu o compoziție corectă a obiectelor.

Minunat. Sunt vândut!

Modele elegante in JavaScript ul modern Ice Factory
Atent

Ori de câte ori învățăm ceva nou, în special ceva la fel de complex ca arhitectura și designul software-ului, avem tendința de a ne dori reguli dure și rapide. Vrem să auzim lucruri de genul „mereu fă asta ”și„ nu fa aia.”

Cu cât petrec mai mult timp lucrând cu aceste lucruri, cu atât îmi dau seama că nu există așa ceva mereu și nu. Este vorba despre alegeri și compromisuri.

Realizarea obiectelor cu o fabrică de gheață este mai lentă și ocupă mai multă memorie decât folosirea unei clase.

În tipurile de caz de utilizare pe care le-am descris, acest lucru nu va conta. Chiar dacă sunt mai lente decât clasele, fabricile de gheață sunt încă destul de rapide.

Dacă aveți nevoie să creați sute de mii de obiecte dintr-o singură fotografie sau dacă vă aflați într-o situație în care memoria și puterea de procesare sunt extrem de mari, este posibil să aveți nevoie de o clasă.

Amintiți-vă, profilați mai întâi aplicația dvs. și nu optimizați prematur. De cele mai multe ori, crearea obiectelor nu va fi blocajul.

În ciuda discursurilor mele anterioare, cursurile nu sunt întotdeauna teribile. Nu ar trebui să aruncați un cadru sau o bibliotecă doar pentru că folosește clase. De fapt, Dan Abramov a scris destul de elocvent despre asta în articolul său, Cum să utilizați cursurile și să dormiți noaptea.

În cele din urmă, trebuie să recunosc că am făcut o grămadă de alegeri de stil opinionale în exemplele de cod pe care vi le-am prezentat:

Puteți face diferite alegeri de stil și este în regulă! Stilul nu este tiparul.

Modelul fabricii de gheață este doar: utilizați o funcție pentru a crea și a returna un obiect înghețat. Depinde de dvs. exact cum scrieți această funcție.

Dacă ați găsit util acest articol, vă rugăm să spargeți pictograma de aplauze de o grămadă de ori pentru a ajuta la răspândirea cuvântului. Și dacă doriți să aflați mai multe lucruri de acest gen, vă rugăm să vă înscrieți la newsletter-ul meu Dev Mastery de mai jos. Mulțumiri!

ACTUALIZARE 2019: Iată un videoclip în care folosesc mult acest model!