Elegantné vzory v modernom jazyku JavaScript: Ice Factory

S JavaScriptom som pracoval a vypínal od konca deväťdesiatych rokov. Spočiatku sa mi to veľmi nepáčilo, ale po zavedení ES2015 (alias ES6) som začal oceňovať JavaScript ako vynikajúci, dynamický programovací jazyk s obrovskou expresívnou silou.

Postupom času som prijal niekoľko kódovacích vzorov, ktoré viedli k čistejšiemu, testovateľnejšiemu a expresívnejšiemu kódu. Teraz s vami zdieľam tieto vzory.

O prvom vzore - „RORO“ - som písal v článku nižšie. Nerobte si starosti, ak ste ju nečítali, môžete si ich prečítať v ľubovoľnom poradí.

Elegantné vzory v modernom JavaScripte: RORO

Prvých pár riadkov JavaScriptu som napísal krátko po vynájdení jazyka. Keby ste mi v tom čase povedali, že ... medium.freecodecamp.org

Dnes by som vám chcel predstaviť vzorec „Ľadovej továrne“.

Ľadová továreň je iba funkcia, ktorá vytvára a vracia zmrazený objekt . Toto vyhlásenie za chvíľu rozbalíme, najskôr však preskúmajme, prečo je tento model taký silný.

Triedy JavaScript nie sú také elegantné

Často má zmysel zoskupiť súvisiace funkcie do jedného objektu. Napríklad v aplikácii elektronického obchodu môžeme mať cartobjekt, ktorý odhaľuje addProductfunkciu a removeProductfunkciu. Tieto funkcie by sme potom mohli vyvolať pomocou cart.addProduct()a cart.removeProduct().

Ak pochádzate z objektovo orientovaného programovacieho jazyka zameraného na triedy, ako je Java alebo C #, bude to pravdepodobne celkom prirodzené.

Ak ste v programovaní noví - teraz, keď ste videli výrok ako cart.addProduct(). Mám podozrenie, že myšlienka zoskupenia funkcií pod jeden objekt vyzerá celkom dobre.

Ako by sme teda vytvorili tento pekný malý cartpredmet? Váš prvý inštinkt s moderným JavaScriptom môže byť použitie a class. Niečo ako:

// 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})
Poznámka : dbPre zjednodušenie používam ako parameter parameter Array . V skutočnom kóde by to bolo niečo ako Model alebo Repo, ktoré interagujú so skutočnou databázou.

Nanešťastie - aj keď to vyzerá pekne - triedy v JavaScripte sa správajú úplne inak, ako by ste čakali.

Ak si nie ste opatrní, triedy JavaScript vás uhryznú.

Napríklad objekty vytvorené pomocou newkľúčového slova sú premenlivé. Môžete teda skutočne znova priradiť metódu:

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?

Ešte horšie je, objekty vytvorené pomocou newkľúčového slova dedí prototypez class, ktorý bol použitý na ich vytvorenie. Takže zmeny v triede ' prototypeovplyvnia všetky objekty z toho vytvorené class- aj keď dôjde k zmene po vytvorení objektu!

Pozri sa na toto:

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!"

Potom je tu skutočnosť, že thisIn JavaScript je dynamicky viazaný. Takže ak obídeme metódy nášho cartobjektu, môžeme stratiť zmienku o this. Je to veľmi neintuitívne a môže nás to dostať do veľa problémov.

Bežnou pascou je priradenie inštančnej metódy k obsluhe udalosti.

Zvážte našu cart.emptymetódu.

empty () { this.db = [] }

Ak priradíme túto metódu priamo k clickudalosti tlačidla na našej webovej stránke ...

 Empty cart
---
document .querySelector('#empty') .addEventListener( 'click', cart.empty )

... keď používatelia kliknú na prázdne miesto button, ich cartzostane plné.

To sa nepodarí bezobslužne pretože thisteraz bude odkazovať na buttonmiesto toho cart. Takže naša cart.emptymetóda nakoniec priradí novú vlastnosť nášmu buttonvolanému dba nastaví túto vlastnosť []namiesto ovplyvnenia cartobjektu db.

Toto je druh chyby, ktorá vás poblázni, pretože v konzole nie je chyba a váš zdravý rozum vám povie, že by to malo fungovať, ale nie.

Aby to fungovalo, musíme urobiť:

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

Alebo:

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

Myslím si, že Mattias Petter Johansson to povedal najlepšie:

new and this [in JavaScript] are some kind of unintuitive, weird, cloud rainbow trap.”

Ice Factory to the rescue

As I said earlier, an Ice Factory is just a function that creates and returns a frozen object. With an Ice Factory our shopping cart example looks like this:

// 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})

Notice our “weird, cloud rainbow traps” are gone:

  • We no longer need new.

    We just invoke a plain old JavaScript function to create our cart object.

  • We no longer need this.

    We can access the db object directly from our member functions.

  • Our cart object is completely immutable.

    Object.freeze() freezes the cart object so that new properties can’t be added to it, existing properties can’t be removed or changed, and the prototype can’t be changed either. Just remember that Object.freeze() is shallow, so if the object we return contains an array or another object we must make sure to Object.freeze() them as well. Also, if you’re using a frozen object outside of an ES Module, you need to be in strict mode to make sure that re-assignments cause an error rather than just failing silently.

A little privacy please

Another advantage of Ice Factories is that they can have private members. For example:

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

This is made possible because of Closures in JavaScript, which you can read more about on MDN.

A little acknowledgement please

Although Factory Functions have been around JavaScript forever, the Ice Factory pattern was heavily inspired by some code that Douglas Crockford showed in this video.

Here’s Crockford demonstrating object creation with a function he calls “constructor”:

My Ice Factory version of the Crockford example above would look like this:

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

I took advantage of function hoisting to put my return statement near the top, so that readers would have a nice little summary of what’s going on before diving into the details.

I also used destructuring on the spec parameter. And I renamed the pattern to “Ice Factory” so that it’s more memorable and less easily confused with the constructor function from a JavaScript class. But it’s basically the same thing.

So, credit where credit is due, thank you Mr. Crockford.

Note: It’s probably worth mentioning that Crockford considers function “hoisting” a “bad part” of JavaScript and would likely consider my version heresy. I discussed my feelings on this in a previous article and more specifically, this comment.

What about inheritance?

If we tick along building out our little e-commerce app, we might soon realize that the concept of adding and removing products keeps cropping up again and again all over the place.

Along with our Shopping Cart, we probably have a Catalog object and an Order object. And all of these probably expose some version of `addProduct` and `removeProduct`.

We know that duplication is bad, so we’ll eventually be tempted to create something like a Product List object that our cart, catalog, and order can all inherit from.

Ale namiesto toho, aby sme rozširovali svoje objekty dedením Zoznamu produktov, môžeme namiesto toho prijať nadčasový princíp ponúkaný v jednej z najvplyvnejších kníh o programovaní, ktorá bola kedy napísaná:

"Uprednostňujte zloženie objektu pred dedičstvom triedy."

- Dizajnové vzory: Prvky opakovane použiteľného objektovo orientovaného softvéru.

Autori tejto knihy - hovorovo známej ako „The Gang of Four“ - v skutočnosti hovoria:

"... máme skúsenosti s tým, že dizajnéri nadmerne využívajú dedičstvo ako techniku ​​opätovného použitia a dizajny sa často stávajú viac opakovane použiteľnými (a jednoduchšími), pretože viac závisia od zloženia objektu."

Tu je teda náš zoznam produktov:

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

A tu je náš nákupný košík:

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

A teraz môžeme jednoducho vložiť náš Zoznam produktov do nášho nákupného košíka, napríklad takto:

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

A použite zoznam produktov cez vlastnosť `items`. Páči sa mi to:

cart.items.addProduct()

It may be tempting to subsume the entire Product List by incorporating its methods directly into the shopping cart object, like so:

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

In fact, in an earlier version of this article, I did just that. But then it was pointed out to me that this is a bit dangerous (as explained here). So, we’re better off sticking with proper object composition.

Awesome. I’m Sold!

Whenever we’re learning something new, especially something as complex as software architecture and design, we tend to want hard and fast rules. We want to hear thing like “always do this” and “ never do that.”

The longer I spend working with this stuff, the more I realize that there’s no such thing as always and never. It’s about choices and trade-offs.

Making objects with an Ice Factory is slower and takes up more memory than using a class.

In the types of use case I’ve described, this won’t matter. Even though they are slower than classes, Ice Factories are still quite fast.

If you find yourself needing to create hundreds of thousands of objects in one shot, or if you’re in a situation where memory and processing power is at an extreme premium you might need a class instead.

Just remember, profile your app first and don’t prematurely optimize. Most of the time, object creation is not going to be the bottleneck.

Despite my earlier rant, Classes are not always terrible. You shouldn’t throw out a framework or library just because it uses classes. In fact, Dan Abramov wrote pretty eloquently about this in his article, How to use Classes and Sleep at Night.

Finally, I need to acknowledge that I’ve made a bunch of opinionated style choices in the code samples I’ve presented to you:

  • I use function statements instead of function expressions.
  • I put my return statement near the top (this is made possible by my use of function statements, see above).
  • I name my factory function, makeX instead of createX or buildX or something else.
  • My factory function takes a single, destructured, parameter object.
  • I don’t use semi-colons (Crockford would also NOT approve of that)
  • and so on…

You may make different style choices, and that’s okay! The style is not the pattern.

The Ice Factory pattern is just: use a function to create and return a frozen object. Exactly how you write that function is up to you.

Ak sa vám tento článok zdal užitočný, rozdrvte túto ikonu potlesku viackrát, aby ste ju mohli šíriť ďalej. A ak sa chcete dozvedieť viac podobných vecí, zaregistrujte sa nižšie v mojom bulletine Dev Mastery. Vďaka!

AKTUALIZÁCIA 2019: Tu je video, kde tento vzor často používam!