Funzione Class vs Factory: esplorare la strada da percorrere

Scopri Functional JavaScript è stato nominato da BookAuthority uno dei migliori nuovi libri di Programmazione funzionale!

ECMAScript 2015 (aka ES6) viene fornito con la sintassi della classe, quindi ora abbiamo due modelli concorrenti per la creazione di oggetti. Per confrontarli, creerò la stessa definizione di oggetto (TodoModel) come classe e quindi come funzione di fabbrica.

TodoModel come classe

class TodoModel {
    costruttore(){
        this.todos = [];
        this.lastChange = null;
    }
    
    addToPrivateList () {
        console.log ( "addToPrivateList");
    }
    add () {console.log ("add"); }
    ricaricare(){}
}

TodoModel come funzione di fabbrica

funzione TodoModel () {
    var todos = [];
    var lastChange = null;
        
    funzione addToPrivateList () {
        console.log ( "addToPrivateList");
    }
    funzione add () {console.log ("add"); }
    function reload () {}
    
    return Object.freeze ({
        Inserisci,
        ricaricare
    });
}

incapsulamento

La prima cosa che notiamo è che tutti i membri, i campi e i metodi di un oggetto classe sono pubblici.

var todoModel = new TodoModel ();
console.log (todoModel.todos); // []
console.log (todoModel.lastChange) // null
todoModel.addToPrivateList (); // addToPrivateList

La mancanza di incapsulamento può creare problemi di sicurezza. Prendi l'esempio di un oggetto globale che può essere modificato direttamente dalla Console per gli sviluppatori.

Quando si utilizza la funzione di fabbrica, solo i metodi che esponiamo sono pubblici, tutto il resto è incapsulato.

var todoModel = TodoModel ();
console.log (todoModel.todos); //non definito
console.log (todoModel.lastChange) // undefined
todoModel.addToPrivateList (); //taskModel.addToPrivateList
                                    non è una funzione

Questo

questi problemi di contesto perdente sono ancora presenti quando si utilizza class. Ad esempio, questo sta perdendo il contesto nelle funzioni nidificate. Non è solo fastidioso durante la codifica, ma è anche una costante fonte di bug.

class TodoModel {
    costruttore(){
        this.todos = [];
    }
    
    ricaricare(){
        setTimeout (log delle funzioni () {
           console.log (this.todos); //non definito
        }, 0);
    }
}
todoModel.reload (); //non definito

oppure questo perde contesto quando il metodo viene utilizzato come callback, come in un evento DOM.

. $ ( "# BTN") clicca (todoModel.reload); //non definito

Non ci sono tali problemi quando si utilizza una funzione di fabbrica, in quanto non lo utilizza affatto.

funzione TodoModel () {
    var todos = [];
        
    function reload () {
        setTimeout (log delle funzioni () {
           console.log (todos); // []
       }, 0);
    }
}
todoModel.reload (); // []
. $ ( "# BTN") clicca (todoModel.reload); // []

questa e la funzione freccia

La funzione freccia risolve parzialmente i problemi di contesto perdenti nelle classi, ma allo stesso tempo crea un nuovo problema:

  • questo non perde più il contesto nelle funzioni nidificate
  • questo sta perdendo contesto quando il metodo viene utilizzato come callback
  • la funzione freccia promuove l'uso di funzioni anonime

Ho refactored TodoModel usando la funzione freccia. È importante notare che nel processo di refactoring della funzione freccia possiamo perdere qualcosa di molto importante per la leggibilità, il nome della funzione. Guarda ad esempio:

// usa il nome della funzione per esprimere l'intento
setTimeout (funzione renderTodosForReview () {
      /* codice */
}, 0);
// rispetto all'utilizzo di una funzione anonima
setTimeout (() => {
      /* codice */
}, 0);

API immutabili

Una volta creato l'oggetto, mi aspetto che la sua API sia immutabile. Posso facilmente cambiare l'implementazione di un metodo pubblico per fare qualcos'altro quando è stato creato usando una classe.

todoModel.reload = function () {console.log ("una nuova ricarica"); }
todoModel.reload (); // una nuova ricarica

Questo problema può essere risolto chiamando Object.freeze (TodoModel.prototype) dopo la definizione della classe.

L'API dell'oggetto creato utilizzando una funzione factory è immutabile. Notare l'uso di Object.freeze () sull'oggetto restituito contenente solo i metodi pubblici. I dati privati ​​dell'oggetto possono essere modificati, ma solo attraverso questi metodi pubblici.

todoModel.reload = function () {console.log ("una nuova ricarica"); }
todoModel.reload (); //ricaricare

nuovo

nuovo dovrebbe essere usato quando si creano oggetti usando le classi.

nuovo non è richiesto durante la creazione di oggetti con funzioni di fabbrica, ma se ciò lo rende più leggibile, puoi farlo, non c'è nulla di male.

var todoModel = new TodoModel ();

L'uso di new con una funzione factory restituirà semplicemente l'oggetto creato dalla factory.

Composizione su eredità

Le classi supportano sia l'ereditarietà che la composizione.

Di seguito è riportato un esempio di ereditarietà in cui la classe SpecialService eredita dalla classe Service:

servizio di classe {
  doSomething () {console.log ("doSomething"); }
}
class SpecialService estende il servizio {
  doSomethingElse () {console.log ("doSomethingElse"); }
}
var specialService = new SpecialService ();
specialService.doSomething ();
specialService.doSomethingElse ();

Ecco un altro esempio in cui SpecialService riutilizza il membro del servizio utilizzando la composizione:

servizio di classe {
  doSomething () {console.log ("doSomething"); }
}
class SpecialService {
  costruttore (args) {
    this.service = args.service;
  }
  doSomething () {this.service.doSomething (); }
  
  doSomethingElse () {console.log ("doSomethingElse"); }
}
var specialService = new SpecialService ({
   servizio: nuovo servizio ()
});
specialService.doSomething ();
specialService.doSomethingElse ();

Le funzioni di fabbrica promuovono la composizione rispetto all'eredità. Dai un'occhiata al prossimo esempio in cui SpecialService riutilizza i membri del servizio:

funzione Service () {
  funzione doSomething () {console.log ("doSomething"); }
  return Object.freeze ({
    fare qualcosa
  });
}
funzione SpecialService (args) {
  var service = args.service;
  funzione doSomethingElse () {console.log ("doSomethingElse"); }
  return Object.freeze ({
    doSomething: service.doSomething,
    Fai qualcos'altro
  });
}
var specialService = SpecialService ({
   servizio: servizio ()
});
specialService.doSomething ();
specialService.doSomethingElse ();

Memoria

Le classi sono migliori nella conservazione della memoria, poiché sono implementate sul sistema prototipo. Tutti i metodi verranno creati una sola volta nell'oggetto prototipo e condivisi da tutte le istanze.

Il costo della memoria della funzione di fabbrica è evidente quando si creano migliaia dello stesso oggetto.

Ecco la pagina utilizzata per testare il costo della memoria quando si utilizza la funzione di fabbrica.

Il costo della memoria (in Chrome)
+ ----------- + ------------ + ------------ +
| Istanze | 10 metodi | 20 metodi |
+ ----------- + --------------- + --------- +
| 10 | 0 | 0 |
| 100 | 0.1Mb ​​| 0.1Mb ​​|
| 1000 | 0.7Mb | 1.4Mb |
| 10000 | 7.3Mb | 14.2Mb |
+ ----------- + ------------ + ------------ +

Oggetti vs Strutture dati

Prima di analizzare ulteriormente il costo della memoria, è necessario distinguere tra due tipi di oggetti:

  • Oggetti OOP
  • Oggetti dati (noti anche come Strutture dati)
Gli oggetti espongono il comportamento e nascondono i dati.
Le strutture dati espongono i dati e non hanno comportamenti significativi.
- Robert Martin "Clean Code"

Esaminerò di nuovo l'esempio di TodoModel e spiegherò questi due tipi di oggetti.

funzione TodoModel () {
    var todos = [];
           
    funzione add () {}
    function reload () {}
       
    return Object.freeze ({
        Inserisci,
        ricaricare
    });
}
  • TodoModel è responsabile della memorizzazione e della gestione dell'elenco dei todos. TodoModel è l'oggetto OOP, quello che espone il comportamento e nasconde i dati. Ci sarà solo un'istanza nell'applicazione, quindi non ci sono costi di memoria aggiuntivi quando si utilizza la funzione di fabbrica.
  • Gli oggetti todos rappresentano le Strutture dati. Potrebbero esserci molti di questi oggetti, ma sono solo semplici oggetti JavaScript. Non siamo interessati a mantenere privati ​​i loro metodi - piuttosto vogliamo effettivamente esporre tutti i loro dati e metodi. Quindi tutti questi oggetti saranno costruiti sul sistema prototipo e trarranno vantaggio dalla conservazione della memoria. Possono essere creati usando un semplice oggetto letterale o Object.create ().

Componenti dell'interfaccia utente

Nell'applicazione, potrebbero esserci centinaia o migliaia di istanze di un componente UI. Questa è una situazione in cui dobbiamo fare un compromesso tra incapsulamento e conservazione della memoria.

I componenti saranno costruiti secondo la pratica del framework dei componenti. Ad esempio, i valori letterali degli oggetti verranno utilizzati per Vue o le classi per React. I membri di ciascun componente saranno pubblici, ma trarranno vantaggio dalla conservazione della memoria del sistema prototipo.

Conclusione

I punti di forza della classe sono la sua familiarità per le persone provenienti da un background di classe e la sua sintassi migliore rispetto al sistema prototipo. Tuttavia, i suoi problemi di sicurezza e l'utilizzo di questo, una fonte continua di perdita di bug di contesto, lo rendono una seconda opzione. In via eccezionale, le classi verranno utilizzate se richiesto dal framework del componente, come nel caso di React.

La funzione Factory non è solo l'opzione migliore per creare oggetti OOP sicuri, incapsulati e flessibili, ma apre anche le porte a un nuovo paradigma di programmazione, unico per JavaScript.

Scopri Functional JavaScript è stato nominato da BookAuthority uno dei migliori nuovi libri di Programmazione funzionale!

Per ulteriori informazioni sull'applicazione delle tecniche di programmazione funzionale in React, dai un'occhiata a Functional React.

Maggiori informazioni su Vue e Vuex in Una breve introduzione ai componenti Vue.js.

Scopri come applicare i Principi dei modelli di progettazione.

Puoi trovarmi anche su Twitter.